From 63f46a3b3188f0b2cb036273dbb6b7ccfcd98337 Mon Sep 17 00:00:00 2001 From: Bengbengbalabalabeng Date: Sun, 17 May 2026 23:06:16 +0800 Subject: [PATCH 1/3] feat: add composable annotation support (task-1). Task-1: Collect all annotation (include composite) map when initializing the read/write head property. --- .../fesod/sheet/annotation/AliasFor.java | 58 + .../annotation/AnnotationAttributes.java | 82 ++ .../fesod/sheet/annotation/AnnotationMap.java | 98 ++ .../sheet/annotation/AnnotationMetadata.java | 48 + .../annotation/AnnotationMetadataReader.java | 55 + .../AnnotationMetadataResolver.java | 60 + .../DefaultAnnotationMetadataResolver.java | 187 +++ .../fesod/sheet/annotation/ExcelProperty.java | 2 +- .../fesod/sheet/annotation/FesodMarked.java | 59 + .../HierarchicalAnnotationScanner.java | 143 +++ .../annotation/format/DateTimeFormat.java | 2 +- .../sheet/annotation/format/NumberFormat.java | 2 +- .../annotation/write/style/ColumnWidth.java | 2 +- .../write/style/ContentFontStyle.java | 2 +- .../write/style/ContentLoopMerge.java | 2 +- .../write/style/ContentRowHeight.java | 2 +- .../annotation/write/style/ContentStyle.java | 2 +- .../annotation/write/style/HeadFontStyle.java | 2 +- .../annotation/write/style/HeadRowHeight.java | 2 +- .../annotation/write/style/HeadStyle.java | 2 +- .../write/style/OnceAbsoluteMerge.java | 2 +- .../org/apache/fesod/sheet/metadata/Head.java | 9 + .../metadata/property/ExcelHeadProperty.java | 31 +- .../annotation/AnnotationAttributesTest.java | 320 +++++ .../sheet/annotation/AnnotationMapTest.java | 442 +++++++ .../AnnotationMetadataReaderTest.java | 196 +++ .../AnnotationMetadataResolverTest.java | 302 +++++ .../annotation/AnnotationMetadataTest.java | 220 ++++ .../composite/CompositeAnnotationTest.java | 1054 +++++++++++++++++ .../composite/DirectAnnotationTest.java | 664 +++++++++++ 30 files changed, 4038 insertions(+), 14 deletions(-) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMap.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadata.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolver.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/FesodMarked.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationAttributesTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMapTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolverTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java new file mode 100644 index 000000000..6a93873e6 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import lombok.Getter; + +/** + * A value object representing a declarative attribute aliasing instruction. + */ +@Getter +public class AliasFor { + + /** + * The source annotation that declares the alias. + */ + private final Class marked; + + /** + * The target meta-annotation being aliased. + */ + private final Class target; + + /** + * The name of the attribute in the target annotation to be overridden. + */ + private final String attribute; + + /** + * The value of the attribute in the target annotation to be overridden. + */ + private final Object value; + + public AliasFor( + Class marked, Class target, String attribute, Object value) { + this.marked = marked; + this.target = target; + this.attribute = attribute; + this.value = value; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java new file mode 100644 index 000000000..7d93d39c9 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.Validate; + +/** + * Implement key-value pairs of annotation attributes based on {@link LinkedHashMap}. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class AnnotationAttributes extends LinkedHashMap { + + private final Class annotationType; + private final String annotationName; + + @Setter + private int distance; + + public AnnotationAttributes(Class annotationType) { + this.annotationType = annotationType; + this.annotationName = annotationType.getCanonicalName(); + this.distance = 0; + } + + public AnnotationAttributes(Class annotationType, Map attrs) { + super(attrs); + this.annotationType = annotationType; + this.annotationName = annotationType.getCanonicalName(); + this.distance = 0; + } + + @SuppressWarnings("unchecked") + public T getRequiredAttribute(String attrName, Class type) { + Validate.notBlank(attrName, "attributeName must not be null or blank"); + Object result = get(attrName); + + if (Objects.isNull(result)) { + throw new IllegalArgumentException( + String.format("Attribute '%s' not found for annotation '%s'", attrName, annotationName)); + } + if (!type.isInstance(result) + && type.isArray() + && type.getComponentType().isInstance(result)) { + Object array = Array.newInstance(type.getComponentType(), 1); + Array.set(array, 0, result); + result = array; + } + if (!type.isInstance(result)) { + throw new IllegalArgumentException(String.format( + "Attribute '%s' is of type %s, but %s was expected for annotation [%s]", + attrName, result.getClass().getSimpleName(), type.getSimpleName(), annotationName)); + } + + return (T) result; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMap.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMap.java new file mode 100644 index 000000000..2b505aa69 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMap.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Map; +import lombok.EqualsAndHashCode; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.Validate; +import org.ehcache.impl.internal.concurrent.ConcurrentHashMap; + +/** + * A wrapper class for all annotation (include composable annotation) attribute key-value pairs + * associated with {@link AnnotatedElement}. + */ +@EqualsAndHashCode +public class AnnotationMap { + + private final Map, AnnotationAttributes> annotations; + + public AnnotationMap(Map, AnnotationAttributes> annotations) { + this.annotations = annotations; + } + + public boolean isEmpty() { + return MapUtils.isEmpty(annotations); + } + + public int size() { + return annotations.size(); + } + + public boolean hasAnnotation(Class annotationType) { + return !isEmpty() && annotations.containsKey(annotationType); + } + + public AnnotationAttributes getAttributes(Class annotationType) { + if (isEmpty()) { + return null; + } + return annotations.get(annotationType); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Map, AnnotationAttributes> ann; + + public Builder() { + this.ann = new ConcurrentHashMap<>(8); + } + + public Builder put(Class annotationType, AnnotationAttributes attributes) { + Validate.notNull(annotationType, "annotationType must not be null"); + Validate.notNull(attributes, "attributes must not be null"); + + ann.put(annotationType, attributes); + return this; + } + + public Builder merge(Class annotationType, AnnotationAttributes attributes) { + Validate.notNull(annotationType, "annotationType must not be null"); + Validate.notNull(attributes, "attributes must not be null"); + + AnnotationAttributes oldAttrs = ann.get(annotationType); + if (oldAttrs == null) { + ann.put(annotationType, attributes); + } else if (attributes.getDistance() < oldAttrs.getDistance()) { + oldAttrs.putAll(attributes); + } + return this; + } + + public AnnotationMap build() { + return new AnnotationMap(ann); + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadata.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadata.java new file mode 100644 index 000000000..3ed99de0c --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadata.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * A wrapper class for resolved annotation instance. + */ +@EqualsAndHashCode +@Getter +public class AnnotationMetadata { + + private final AnnotationAttributes attributes; + private final List aliases; + + public AnnotationMetadata(AnnotationAttributes attributes, List aliases) { + this.attributes = attributes; + this.aliases = aliases; + } + + public void addTo(List aliases) { + aliases.addAll(this.aliases); + } + + public void setDistance(int distance) { + attributes.setDistance(distance); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java new file mode 100644 index 000000000..d4598a60b --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.util.Map; +import org.apache.commons.collections4.map.ConcurrentReferenceHashMap; + +/** + * A coordinator for discovering and reading annotation metadata from {@link AnnotatedElement}s. + */ +public class AnnotationMetadataReader extends HierarchicalAnnotationScanner { + + private final Map elementAnnotation; + + public AnnotationMetadataReader() { + this( + new DefaultAnnotationMetadataResolver(), + ConcurrentReferenceHashMap.builder() + .get()); + } + + public AnnotationMetadataReader( + AnnotationMetadataResolver resolver, Map elementAnnotation) { + super(resolver); + this.elementAnnotation = elementAnnotation; + } + + /** + * Read the merged annotation metadata for the given element. + * + * @param element the class, field + * @return the resolved {@link AnnotationMap} + */ + public AnnotationMap read(AnnotatedElement element) { + return elementAnnotation.computeIfAbsent(element, super::scan); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolver.java new file mode 100644 index 000000000..4a969023c --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolver.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; + +/** + * Strategy interface for resolving and introspecting annotation metadata. + */ +public interface AnnotationMetadataResolver { + + /** + * Determine if the given annotation type should be ignored by the scanner. + * + * @param type the type to check + * @return {@code true} if the annotation should be skipped + */ + boolean shouldIgnore(Class type); + + /** + * Determine if the annotation is a framework-intrinsic "Inner" annotation. + * + * @param ann the annotation instance to check + * @return {@code true} if it is a framework-internal + */ + boolean isInnerAnnotated(Annotation ann); + + /** + * Determine if the annotation is marked with the core meta-protocol. + * + * @param ann the annotation instance to check + * @return {@code true} if it is a composable meta-annotation + */ + boolean isMetaMarked(Annotation ann); + + /** + * Resolve a raw {@link Annotation} into a {@link AnnotationMetadata} object. + * + * @param ann the annotation instance to resolve + * @return the resolved metadata + */ + AnnotationMetadata resolve(Annotation ann); +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java new file mode 100644 index 000000000..d009ba4a7 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Native; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.ehcache.impl.internal.concurrent.ConcurrentHashMap; + +/** + * Default implementation of the {@link AnnotationMetadataResolver} interface, + * providing introspection and resolution of annotation metadata. + */ +public class DefaultAnnotationMetadataResolver implements AnnotationMetadataResolver { + + private static final Set> IGNORE_ANNOTATIONS; + private static final Set> INNER_ANNOTATIONS; + + private final Map, Boolean> metaMakedMap = new ConcurrentHashMap<>(); + private final Map metaAliasMap = new ConcurrentHashMap<>(); + + static { + Set> ignoreTmp = new HashSet<>(); + ignoreTmp.add(Target.class); + ignoreTmp.add(Retention.class); + ignoreTmp.add(Documented.class); + ignoreTmp.add(Repeatable.class); + ignoreTmp.add(Native.class); + ignoreTmp.add(Inherited.class); + IGNORE_ANNOTATIONS = Collections.unmodifiableSet(ignoreTmp); + + Set> innerTmp = new HashSet<>(); + innerTmp.add(ExcelProperty.class); + innerTmp.add(ExcelIgnoreUnannotated.class); + innerTmp.add(ExcelIgnore.class); + innerTmp.add(DateTimeFormat.class); + innerTmp.add(NumberFormat.class); + innerTmp.add(ColumnWidth.class); + innerTmp.add(ContentFontStyle.class); + innerTmp.add(ContentLoopMerge.class); + innerTmp.add(ContentRowHeight.class); + innerTmp.add(ContentStyle.class); + innerTmp.add(HeadFontStyle.class); + innerTmp.add(HeadRowHeight.class); + innerTmp.add(HeadStyle.class); + innerTmp.add(OnceAbsoluteMerge.class); + INNER_ANNOTATIONS = Collections.unmodifiableSet(innerTmp); + } + + /** + * Determine if the given annotation type should be ignored by the scanner. + * used to filter out JDK-standard meta-annotations such as {@code @Target} or {@code @Retention}. + * + * @param type the type to check + * @return {@code true} if the annotation should be skipped + */ + @Override + public boolean shouldIgnore(Class type) { + return IGNORE_ANNOTATIONS.contains(type); + } + + /** + * Determine if the annotation is a framework-intrinsic "Inner" annotation. + * Such as {@code ExcelProperty} or {@code DateTimeFormat}... + * + * @param ann the annotation instance to check + * @return {@code true} if it is a framework-internal + */ + @Override + public boolean isInnerAnnotated(Annotation ann) { + return INNER_ANNOTATIONS.contains(ann.annotationType()); + } + + /** + * Determine if the annotation is marked ({@code @FesodMarked}) with the core meta-protocol. + * + * @param ann the annotation instance to check + * @return {@code true} if it is a composable meta-annotation + */ + @Override + public boolean isMetaMarked(Annotation ann) { + Class type = ann.annotationType(); + return metaMakedMap.computeIfAbsent(type, k -> type.getAnnotation(FesodMarked.class) != null); + } + + /** + * Resolve a raw {@link Annotation} into a {@link AnnotationMetadata} object. + * + * @param ann the annotation instance to resolve + * @return the resolved metadata + */ + @Override + public AnnotationMetadata resolve(Annotation ann) { + Set markedAnnNames = new HashSet<>(); + if (isMetaMarked(ann)) { + Annotation[] annotations = ann.annotationType().getAnnotations(); + for (Annotation markedAnn : annotations) { + markedAnnNames.add(markedAnn.annotationType().getName()); + } + } + + Method[] methods = ann.annotationType().getDeclaredMethods(); + + List aliases = new ArrayList<>(); + Map attr = Arrays.stream(methods) + .filter(this::isEffectMethod) + .collect(Collectors.toMap(Method::getName, method -> { + try { + Object result = Optional.ofNullable(method.invoke(ann)).orElseGet(method::getDefaultValue); + + // Handle @FesodMarked.AliasFor + if (isMetaAlias(method)) { + FesodMarked.AliasFor aliasFor = method.getAnnotation(FesodMarked.AliasFor.class); + if (!markedAnnNames.contains(aliasFor.annotation().getName())) { + throw new IllegalStateException(String.format( + "The alias annotation '%s' is not marked on the custom-annotation '%s'", + aliasFor.annotation().getName(), + ann.annotationType().getName())); + } + + aliases.add(new AliasFor( + ann.annotationType(), aliasFor.annotation(), aliasFor.attribute(), result)); + } + return result; + } catch (IllegalAccessException | InvocationTargetException ex) { + throw new IllegalStateException( + String.format( + "Failed to invoke annotation [%s] method [%s]", + ann.annotationType().getName(), method.getName()), + ex); + } + })); + return new AnnotationMetadata(new AnnotationAttributes(ann.annotationType(), attr), aliases); + } + + private boolean isEffectMethod(Method method) { + return method.getParameterCount() == 0 && method.getReturnType() != void.class; + } + + private boolean isMetaAlias(AnnotatedElement element) { + return metaAliasMap.computeIfAbsent(element, k -> element.getAnnotation(FesodMarked.AliasFor.class) != null); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/ExcelProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/ExcelProperty.java index 1cb75c9db..c43ed3f7d 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/ExcelProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/ExcelProperty.java @@ -36,7 +36,7 @@ /** * */ -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ExcelProperty { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/FesodMarked.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/FesodMarked.java new file mode 100644 index 000000000..327df9748 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/FesodMarked.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code FesodMarked} is a meta-annotation (annotations used on other annotations) + * used for indicating that instead of using target annotation + * (annotation annotated with this annotation), + * Fesod should use meta-annotations it has. + * This can be useful in creating "Composable Annotations" by having + * a container annotation, which needs to be annotated with this + * annotation as well as all annotations it 'contains'. + */ +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FesodMarked { + + /** + * {@code @AliasFor} is an annotation that is used to declare aliases for + * annotation attributes. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface AliasFor { + + /** + * The type of annotation in which the aliased attribute() is declared. + */ + Class annotation(); + + /** + * The name of the attribute that this attribute is an alias for. + */ + String attribute(); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java new file mode 100644 index 000000000..8626ad9ca --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; + +/** + * Abstract base class for scanning and storing composable annotations. + */ +public abstract class HierarchicalAnnotationScanner { + + protected final AnnotationMetadataResolver metadataResolver; + + protected HierarchicalAnnotationScanner(AnnotationMetadataResolver metadataResolver) { + this.metadataResolver = metadataResolver; + } + + protected AnnotationMap scan(AnnotatedElement element) { + Annotation[] annotations = element.getAnnotations(); + if (ArrayUtils.isEmpty(annotations)) { + return null; + } + + AnnotationMap.Builder builder = AnnotationMap.builder(); + + Queue queue = new LinkedList<>(Arrays.asList(annotations)); + // Record visited annotation, to avoid circular dependencies (like: @A -> @B, @B -> @A) + Set> visited = new HashSet<>(); + List aliases = new ArrayList<>(); + int distance = 0; + + while (!queue.isEmpty()) { + int currLevelSize = queue.size(); + + for (int i = 0; i < currLevelSize; i++) { + Annotation ann = queue.poll(); + Class type = ann.annotationType(); + + if (metadataResolver.shouldIgnore(type) || !visited.add(type)) { + continue; + } + + // Handle fesod-sheet inner annotations (high-level attribute value) + if (metadataResolver.isInnerAnnotated(ann)) { + AnnotationMetadata metadata = metadataResolver.resolve(ann); + metadata.setDistance(distance); + + builder.merge(type, metadata.getAttributes()); + } + + // Handle composable-annotations (low-level attribute value) + if (metadataResolver.isMetaMarked(ann)) { + AnnotationMetadata metadata = metadataResolver.resolve(ann); + metadata.addTo(aliases); + metadata.setDistance(distance); + + builder.put(type, metadata.getAttributes()); + + for (Annotation metaAnn : type.getAnnotations()) { + if (metadataResolver.shouldIgnore(metaAnn.annotationType())) { + continue; + } + queue.add(metaAnn); + } + } + } + + distance++; + } + + AnnotationMap annotationMap = builder.build(); + + // Handle alias + synthesize(annotationMap, aliases); + + return annotationMap; + } + + /** + * Handle the mapping and overriding logic of annotation attribute aliases (AliasFor). + *

+ * Attribute Override Policy: Annotations closer to the annotated target (with a smaller distance) have higher attribute priority + * and can override the properties aliased in their meta-annotations (with a larger distance). + *

+ * The judgment logic for distance is as follows: + *

    + *
  • marked distance == target distance: + * Both are at the same level (for example, peer declarations are made on the same target). In this case, + * there is no hierarchical override relationship between them.
  • + *
  • marked distance < target distance: + * The annotation that declares an alias (marked) is closer to the target (i.e., at the child annotation level) and + * has higher priority. In this case, the attribute values in the child annotation (marked) will override/sync to + * the corresponding attributes in the target meta-annotation (target).
  • + *
+ * + * @param annotationMap A collection of annotation attributes + * @param aliases Alias mapping list + */ + private void synthesize(AnnotationMap annotationMap, List aliases) { + if (CollectionUtils.isEmpty(aliases)) { + return; + } + + for (AliasFor alias : aliases) { + AnnotationAttributes marked = annotationMap.getAttributes(alias.getMarked()); + AnnotationAttributes target = annotationMap.getAttributes(alias.getTarget()); + + if (marked == null || target == null) { + continue; + } + if ((marked.getDistance() + 1) <= target.getDistance()) { + target.put(alias.getAttribute(), marked.get(alias.getAttribute())); + } + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/DateTimeFormat.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/DateTimeFormat.java index e579f451a..f5308880c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/DateTimeFormat.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/DateTimeFormat.java @@ -42,7 +42,7 @@ * * */ -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DateTimeFormat { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/NumberFormat.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/NumberFormat.java index f6178b085..ac8956de3 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/NumberFormat.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/format/NumberFormat.java @@ -42,7 +42,7 @@ * * */ -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface NumberFormat { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ColumnWidth.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ColumnWidth.java index e929b0412..377abeb42 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ColumnWidth.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ColumnWidth.java @@ -36,7 +36,7 @@ * * */ -@Target({ElementType.FIELD, ElementType.TYPE}) +@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ColumnWidth { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentFontStyle.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentFontStyle.java index bf129bc92..15600bbe0 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentFontStyle.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentFontStyle.java @@ -41,7 +41,7 @@ * * */ -@Target({ElementType.FIELD, ElementType.TYPE}) +@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ContentFontStyle { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentLoopMerge.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentLoopMerge.java index ac037130a..9b9a1fc05 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentLoopMerge.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentLoopMerge.java @@ -36,7 +36,7 @@ * * */ -@Target({ElementType.FIELD}) +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ContentLoopMerge { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentRowHeight.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentRowHeight.java index f19e39191..dd5a72a5a 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentRowHeight.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentRowHeight.java @@ -36,7 +36,7 @@ * * */ -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ContentRowHeight { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentStyle.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentStyle.java index 722ac5661..73a41b88e 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentStyle.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/ContentStyle.java @@ -45,7 +45,7 @@ * * */ -@Target({ElementType.FIELD, ElementType.TYPE}) +@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ContentStyle { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadFontStyle.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadFontStyle.java index a37659b8f..10ce79056 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadFontStyle.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadFontStyle.java @@ -41,7 +41,7 @@ * * */ -@Target({ElementType.FIELD, ElementType.TYPE}) +@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HeadFontStyle { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadRowHeight.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadRowHeight.java index b20ef5775..7c95abe0a 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadRowHeight.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadRowHeight.java @@ -36,7 +36,7 @@ * * */ -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HeadRowHeight { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadStyle.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadStyle.java index 4a66f9ad2..7f446d396 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadStyle.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/HeadStyle.java @@ -45,7 +45,7 @@ * * */ -@Target({ElementType.FIELD, ElementType.TYPE}) +@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HeadStyle { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/OnceAbsoluteMerge.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/OnceAbsoluteMerge.java index c1525d001..905c74e24 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/OnceAbsoluteMerge.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/style/OnceAbsoluteMerge.java @@ -36,7 +36,7 @@ * * */ -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface OnceAbsoluteMerge { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java index 85e008428..dc54c9307 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java @@ -31,8 +31,10 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.annotation.AnnotationMap; import org.apache.fesod.sheet.exception.ExcelGenerateException; import org.apache.fesod.sheet.metadata.property.ColumnWidthProperty; +import org.apache.fesod.sheet.metadata.property.ExcelHeadProperty; import org.apache.fesod.sheet.metadata.property.FontProperty; import org.apache.fesod.sheet.metadata.property.LoopMergeProperty; import org.apache.fesod.sheet.metadata.property.StyleProperty; @@ -89,11 +91,17 @@ public class Head { */ private FontProperty headFontProperty; + /** + * Custom class field-level annotation attribute key-value pairs. (Used in conjunction with {@link ExcelHeadProperty#headClazz}) + */ + private AnnotationMap annotationMap; + public Head( Integer columnIndex, Field field, String fieldName, List headNameList, + AnnotationMap annotationMap, Boolean forceIndex, Boolean forceName) { this.columnIndex = columnIndex; @@ -109,6 +117,7 @@ public Head( } } } + this.annotationMap = annotationMap; this.forceIndex = forceIndex; this.forceName = forceName; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java index 7c4903b62..90e9b4928 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java @@ -35,6 +35,8 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.fesod.common.util.StringUtils; +import org.apache.fesod.sheet.annotation.AnnotationMap; +import org.apache.fesod.sheet.annotation.AnnotationMetadataReader; import org.apache.fesod.sheet.enums.HeadKindEnum; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.metadata.FieldCache; @@ -61,6 +63,11 @@ public class ExcelHeadProperty { * The types of head */ private HeadKindEnum headKind; + + /** + * Custom class-level annotation attribute key-value pairs. (Used in conjunction with {@link ExcelHeadProperty#headClazz}) + */ + private AnnotationMap headClazzAnnotationMap; /** * The number of rows in the line with the most rows */ @@ -70,6 +77,8 @@ public class ExcelHeadProperty { */ private Map headMap; + private AnnotationMetadataReader metadataReader = new AnnotationMetadataReader(); + public ExcelHeadProperty(ConfigurationHolder configurationHolder, Class headClazz, List> head) { this.headClazz = headClazz; headMap = new TreeMap<>(); @@ -83,7 +92,16 @@ public ExcelHeadProperty(ConfigurationHolder configurationHolder, Class headC continue; } } - headMap.put(headIndex, new Head(headIndex, null, null, head.get(i), Boolean.FALSE, Boolean.TRUE)); + headMap.put( + headIndex, + new Head( + headIndex, + null, + null, + head.get(i), + AnnotationMap.builder().build(), + Boolean.FALSE, + Boolean.TRUE)); headIndex++; } headKind = HeadKindEnum.STRING; @@ -121,6 +139,7 @@ private void initColumnProperties(ConfigurationHolder configurationHolder) { if (headClazz == null) { return; } + headClazzAnnotationMap = metadataReader.read(headClazz); FieldCache fieldCache = ClassUtils.declaredFields(headClazz, configurationHolder); for (Map.Entry entry : @@ -155,7 +174,15 @@ private void initOneColumnProperty(int index, FieldWrapper field, Boolean forceI Collections.addAll(tmpHeadList, field.getHeads()); } } - Head head = new Head(index, field.getField(), field.getFieldName(), tmpHeadList, forceIndex, !notForceName); + AnnotationMap fieldAnnotationMap = metadataReader.read(field.getField()); + Head head = new Head( + index, + field.getField(), + field.getFieldName(), + tmpHeadList, + fieldAnnotationMap, + forceIndex, + !notForceName); headMap.put(index, head); } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationAttributesTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationAttributesTest.java new file mode 100644 index 000000000..3c74fee23 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationAttributesTest.java @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.fesod.sheet.enums.BooleanEnum; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotationAttributes} + */ +class AnnotationAttributesTest { + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface TestAnnotation {} + + // ---- Constructor tests ---- + + @Test + void shouldSetAnnotationTypeAndName_whenConstructedWithTypeOnly() { + // given + Class type = TestAnnotation.class; + + // when + AnnotationAttributes attrs = new AnnotationAttributes(type); + + // then + Assertions.assertSame(type, attrs.getAnnotationType()); + Assertions.assertEquals(type.getCanonicalName(), attrs.getAnnotationName()); + Assertions.assertEquals(0, attrs.getDistance()); + Assertions.assertTrue(attrs.isEmpty()); + } + + @Test + void shouldContainAllEntries_whenConstructedWithMap() { + // given + Map map = new LinkedHashMap<>(); + map.put("value", "test"); + map.put("index", 5); + + // when + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class, map); + + // then + Assertions.assertEquals(2, attrs.size()); + Assertions.assertEquals("test", attrs.get("value")); + Assertions.assertEquals(5, attrs.get("index")); + Assertions.assertEquals(0, attrs.getDistance()); + } + + @Test + void shouldBeIndependentFromSourceMap_whenConstructedWithMap() { + // given + Map map = new LinkedHashMap<>(); + map.put("key", "original"); + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class, map); + + // when - modify source map after construction + map.put("key", "modified"); + map.put("extra", "value"); + + // then - attrs should not be affected + Assertions.assertEquals("original", attrs.get("key")); + Assertions.assertEquals(1, attrs.size()); + } + + // ---- getRequiredAttribute tests ---- + + @Test + void shouldReturnValue_whenAttributePresentAndTypeMatches() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("name", "hello"); + + // when + String result = attrs.getRequiredAttribute("name", String.class); + + // then + Assertions.assertEquals("hello", result); + } + + @Test + void shouldReturnInteger_whenAttributeIsInteger() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("count", 42); + + // when + Integer result = attrs.getRequiredAttribute("count", Integer.class); + + // then + Assertions.assertEquals(42, result); + } + + @Test + void shouldReturnShort_whenAttributeIsShort() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("height", (short) 14); + + // when + Short result = attrs.getRequiredAttribute("height", Short.class); + + // then + Assertions.assertEquals((short) 14, result); + } + + @Test + void shouldReturnEnum_whenAttributeIsEnum() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("bold", BooleanEnum.TRUE); + + // when + BooleanEnum result = attrs.getRequiredAttribute("bold", BooleanEnum.class); + + // then + Assertions.assertEquals(BooleanEnum.TRUE, result); + } + + @Test + void shouldReturnStringArray_whenAttributeIsStringArray() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", new String[] {"first", "second"}); + + // when + String[] result = attrs.getRequiredAttribute("value", String[].class); + + // then + Assertions.assertArrayEquals(new String[] {"first", "second"}, result); + } + + @Test + void shouldWrapSingleValueIntoArray_whenArrayTypeRequestedButScalarStored() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", "single"); + + // when + String[] result = attrs.getRequiredAttribute("value", String[].class); + + // then + Assertions.assertArrayEquals(new String[] {"single"}, result); + } + + @Test + void shouldThrow_whenAttributeNotFound() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + + // when / then + IllegalArgumentException ex = Assertions.assertThrows( + IllegalArgumentException.class, () -> attrs.getRequiredAttribute("missing", String.class)); + Assertions.assertTrue(ex.getMessage().contains("missing")); + Assertions.assertTrue(ex.getMessage().contains(TestAnnotation.class.getCanonicalName())); + } + + @Test + void shouldThrow_whenTypeMismatch() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", "string-value"); + + // when / then + IllegalArgumentException ex = Assertions.assertThrows( + IllegalArgumentException.class, () -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertTrue(ex.getMessage().contains("value")); + Assertions.assertTrue(ex.getMessage().contains("String")); + Assertions.assertTrue(ex.getMessage().contains("Integer")); + } + + @Test + void shouldThrow_whenAttrNameIsNull() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", "test"); + + // when / then + Assertions.assertThrows(Exception.class, () -> attrs.getRequiredAttribute(null, String.class)); + } + + @Test + void shouldThrow_whenAttrNameIsBlank() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", "test"); + + // when / then + Assertions.assertThrows(Exception.class, () -> attrs.getRequiredAttribute(" ", String.class)); + } + + // ---- Distance tests ---- + + @Test + void shouldReturnDefaultDistance_whenConstructed() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + + // when / then + Assertions.assertEquals(0, attrs.getDistance()); + } + + @Test + void shouldUpdateDistance_whenSetDistanceCalled() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + + // when + attrs.setDistance(3); + + // then + Assertions.assertEquals(3, attrs.getDistance()); + } + + // ---- Map operation tests ---- + + @Test + void shouldSupportPutAndGet_whenUsedAsMap() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + + // when + attrs.put("key1", "value1"); + attrs.put("key2", 100); + + // then + Assertions.assertEquals(2, attrs.size()); + Assertions.assertEquals("value1", attrs.get("key1")); + Assertions.assertEquals(100, attrs.get("key2")); + } + + @Test + void shouldOverwriteValue_whenSameKeyPutTwice() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("key", "first"); + + // when + attrs.put("key", "second"); + + // then + Assertions.assertEquals(1, attrs.size()); + Assertions.assertEquals("second", attrs.get("key")); + } + + @Test + void shouldReturnNull_whenKeyNotPresent() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + + // when / then + Assertions.assertNull(attrs.get("nonexistent")); + } + + // ---- Equality tests ---- + + @Test + void shouldBeEqual_whenSameTypeSameEntriesSameDistance() { + // given + Map map = new LinkedHashMap<>(); + map.put("value", "test"); + map.put("index", 1); + + AnnotationAttributes attrs1 = new AnnotationAttributes(TestAnnotation.class, map); + AnnotationAttributes attrs2 = new AnnotationAttributes(TestAnnotation.class, map); + + // when / then + Assertions.assertEquals(attrs1, attrs2); + Assertions.assertEquals(attrs1.hashCode(), attrs2.hashCode()); + } + + @Test + void shouldNotBeEqual_whenDifferentEntries() { + // given + AnnotationAttributes attrs1 = new AnnotationAttributes(TestAnnotation.class); + attrs1.put("value", "a"); + + AnnotationAttributes attrs2 = new AnnotationAttributes(TestAnnotation.class); + attrs2.put("value", "b"); + + // when / then + Assertions.assertNotEquals(attrs1, attrs2); + } + + @Test + void shouldNotBeEqual_whenDifferentAnnotationType() { + // given + AnnotationAttributes attrs1 = new AnnotationAttributes(TestAnnotation.class); + AnnotationAttributes attrs2 = new AnnotationAttributes(Target.class); + + // when / then + Assertions.assertNotEquals(attrs1, attrs2); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMapTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMapTest.java new file mode 100644 index 000000000..7bac6cfc5 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMapTest.java @@ -0,0 +1,442 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotationMap} + */ +class AnnotationMapTest { + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface FirstAnnotation {} + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface SecondAnnotation {} + + // ---- isEmpty tests ---- + + @Test + void shouldBeEmpty_whenConstructedWithEmptyMap() { + // given + AnnotationMap map = new AnnotationMap(Collections.emptyMap()); + + // when / then + Assertions.assertTrue(map.isEmpty()); + } + + @Test + void shouldBeEmpty_whenConstructedWithNullMap() { + // given + AnnotationMap map = new AnnotationMap(null); + + // when / then + Assertions.assertTrue(map.isEmpty()); + } + + @Test + void shouldNotBeEmpty_whenContainingAnnotations() { + // given + Map, AnnotationAttributes> data = new LinkedHashMap<>(); + data.put(FirstAnnotation.class, new AnnotationAttributes(FirstAnnotation.class)); + AnnotationMap map = new AnnotationMap(data); + + // when / then + Assertions.assertFalse(map.isEmpty()); + } + + // ---- size tests ---- + + @Test + void shouldReturnZero_whenEmpty() { + // given + AnnotationMap map = new AnnotationMap(Collections.emptyMap()); + + // when / then + Assertions.assertEquals(0, map.size()); + } + + @Test + void shouldReturnCorrectSize_whenContainingAnnotations() { + // given + Map, AnnotationAttributes> data = new LinkedHashMap<>(); + data.put(FirstAnnotation.class, new AnnotationAttributes(FirstAnnotation.class)); + data.put(SecondAnnotation.class, new AnnotationAttributes(SecondAnnotation.class)); + AnnotationMap map = new AnnotationMap(data); + + // when / then + Assertions.assertEquals(2, map.size()); + } + + // ---- hasAnnotation tests ---- + + @Test + void shouldReturnTrue_whenAnnotationPresent() { + // given + Map, AnnotationAttributes> data = new LinkedHashMap<>(); + data.put(FirstAnnotation.class, new AnnotationAttributes(FirstAnnotation.class)); + AnnotationMap map = new AnnotationMap(data); + + // when / then + Assertions.assertTrue(map.hasAnnotation(FirstAnnotation.class)); + } + + @Test + void shouldReturnFalse_whenAnnotationAbsent() { + // given + Map, AnnotationAttributes> data = new LinkedHashMap<>(); + data.put(FirstAnnotation.class, new AnnotationAttributes(FirstAnnotation.class)); + AnnotationMap map = new AnnotationMap(data); + + // when / then + Assertions.assertFalse(map.hasAnnotation(SecondAnnotation.class)); + } + + @Test + void shouldReturnFalse_whenMapIsEmpty() { + // given + AnnotationMap map = new AnnotationMap(Collections.emptyMap()); + + // when / then + Assertions.assertFalse(map.hasAnnotation(FirstAnnotation.class)); + } + + // ---- getAttributes tests ---- + + @Test + void shouldReturnAttributes_whenAnnotationPresent() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(FirstAnnotation.class); + attrs.put("value", "test"); + Map, AnnotationAttributes> data = new LinkedHashMap<>(); + data.put(FirstAnnotation.class, attrs); + AnnotationMap map = new AnnotationMap(data); + + // when + AnnotationAttributes result = map.getAttributes(FirstAnnotation.class); + + // then + Assertions.assertSame(attrs, result); + Assertions.assertEquals("test", result.get("value")); + } + + @Test + void shouldReturnNull_whenAnnotationAbsent() { + // given + Map, AnnotationAttributes> data = new LinkedHashMap<>(); + data.put(FirstAnnotation.class, new AnnotationAttributes(FirstAnnotation.class)); + AnnotationMap map = new AnnotationMap(data); + + // when / then + Assertions.assertNull(map.getAttributes(SecondAnnotation.class)); + } + + @Test + void shouldReturnNull_whenMapIsEmpty() { + // given + AnnotationMap map = new AnnotationMap(Collections.emptyMap()); + + // when / then + Assertions.assertNull(map.getAttributes(FirstAnnotation.class)); + } + + // ---- Builder.put tests ---- + + @Test + void shouldPutAnnotation_whenUsingBuilder() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(FirstAnnotation.class); + attrs.put("value", "hello"); + + // when + AnnotationMap map = + AnnotationMap.builder().put(FirstAnnotation.class, attrs).build(); + + // then + Assertions.assertTrue(map.hasAnnotation(FirstAnnotation.class)); + Assertions.assertEquals( + "hello", map.getAttributes(FirstAnnotation.class).get("value")); + } + + @Test + void shouldOverwrite_whenPutSameTypeTwice() { + // given + AnnotationAttributes first = new AnnotationAttributes(FirstAnnotation.class); + first.put("value", "first"); + AnnotationAttributes second = new AnnotationAttributes(FirstAnnotation.class); + second.put("value", "second"); + + // when + AnnotationMap map = AnnotationMap.builder() + .put(FirstAnnotation.class, first) + .put(FirstAnnotation.class, second) + .build(); + + // then + Assertions.assertEquals( + "second", map.getAttributes(FirstAnnotation.class).get("value")); + } + + @Test + void shouldPutMultiple_whenDifferentTypes() { + // given + AnnotationAttributes attrs1 = new AnnotationAttributes(FirstAnnotation.class); + AnnotationAttributes attrs2 = new AnnotationAttributes(SecondAnnotation.class); + + // when + AnnotationMap map = AnnotationMap.builder() + .put(FirstAnnotation.class, attrs1) + .put(SecondAnnotation.class, attrs2) + .build(); + + // then + Assertions.assertEquals(2, map.size()); + Assertions.assertTrue(map.hasAnnotation(FirstAnnotation.class)); + Assertions.assertTrue(map.hasAnnotation(SecondAnnotation.class)); + } + + @Test + void shouldThrow_whenPutNullAnnotationType() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(FirstAnnotation.class); + + // when / then + Assertions.assertThrows( + NullPointerException.class, () -> AnnotationMap.builder().put(null, attrs)); + } + + @Test + void shouldThrow_whenPutNullAttributes() { + // when / then + Assertions.assertThrows( + NullPointerException.class, () -> AnnotationMap.builder().put(FirstAnnotation.class, null)); + } + + // ---- Builder.merge tests ---- + + @Test + void shouldAddWhenNoExisting_whenMerge() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(FirstAnnotation.class); + attrs.setDistance(0); + + // when + AnnotationMap map = + AnnotationMap.builder().merge(FirstAnnotation.class, attrs).build(); + + // then + Assertions.assertTrue(map.hasAnnotation(FirstAnnotation.class)); + Assertions.assertSame(attrs, map.getAttributes(FirstAnnotation.class)); + } + + @Test + void shouldMerge_whenNewDistanceLowerThanExisting() { + // given + AnnotationAttributes existing = new AnnotationAttributes(FirstAnnotation.class); + existing.put("value", "original"); + existing.setDistance(2); + + AnnotationAttributes incoming = new AnnotationAttributes(FirstAnnotation.class); + incoming.put("value", "override"); + incoming.put("extra", "added"); + incoming.setDistance(1); + + // when + AnnotationMap map = AnnotationMap.builder() + .put(FirstAnnotation.class, existing) + .merge(FirstAnnotation.class, incoming) + .build(); + + // then + AnnotationAttributes result = map.getAttributes(FirstAnnotation.class); + Assertions.assertEquals("override", result.get("value")); + Assertions.assertEquals("added", result.get("extra")); + } + + @Test + void shouldNotMerge_whenNewDistanceHigherThanExisting() { + // given + AnnotationAttributes existing = new AnnotationAttributes(FirstAnnotation.class); + existing.put("value", "original"); + existing.setDistance(1); + + AnnotationAttributes incoming = new AnnotationAttributes(FirstAnnotation.class); + incoming.put("value", "should-not-override"); + incoming.setDistance(2); + + // when + AnnotationMap map = AnnotationMap.builder() + .put(FirstAnnotation.class, existing) + .merge(FirstAnnotation.class, incoming) + .build(); + + // then + Assertions.assertEquals( + "original", map.getAttributes(FirstAnnotation.class).get("value")); + } + + @Test + void shouldNotMerge_whenNewDistanceSameAsExisting() { + // given + AnnotationAttributes existing = new AnnotationAttributes(FirstAnnotation.class); + existing.put("value", "original"); + existing.setDistance(1); + + AnnotationAttributes incoming = new AnnotationAttributes(FirstAnnotation.class); + incoming.put("value", "should-not-override"); + incoming.setDistance(1); + + // when + AnnotationMap map = AnnotationMap.builder() + .put(FirstAnnotation.class, existing) + .merge(FirstAnnotation.class, incoming) + .build(); + + // then + Assertions.assertEquals( + "original", map.getAttributes(FirstAnnotation.class).get("value")); + } + + @Test + void shouldMergeMultipleSteps_whenDistanceProgressivelyLower() { + // given + AnnotationAttributes dist2 = new AnnotationAttributes(FirstAnnotation.class); + dist2.put("a", "from-dist2"); + dist2.setDistance(2); + + AnnotationAttributes dist1 = new AnnotationAttributes(FirstAnnotation.class); + dist1.put("a", "from-dist1"); + dist1.put("b", "from-dist1"); + dist1.setDistance(1); + + AnnotationAttributes dist0 = new AnnotationAttributes(FirstAnnotation.class); + dist0.put("c", "from-dist0"); + dist0.setDistance(0); + + // when + AnnotationMap map = AnnotationMap.builder() + .merge(FirstAnnotation.class, dist2) + .merge(FirstAnnotation.class, dist1) + .merge(FirstAnnotation.class, dist0) + .build(); + + // then + AnnotationAttributes result = map.getAttributes(FirstAnnotation.class); + Assertions.assertEquals("from-dist1", result.get("a")); + Assertions.assertEquals("from-dist1", result.get("b")); + Assertions.assertEquals("from-dist0", result.get("c")); + } + + @Test + void shouldThrow_whenMergeNullAnnotationType() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(FirstAnnotation.class); + + // when / then + Assertions.assertThrows( + NullPointerException.class, () -> AnnotationMap.builder().merge(null, attrs)); + } + + @Test + void shouldThrow_whenMergeNullAttributes() { + // when / then + Assertions.assertThrows( + NullPointerException.class, () -> AnnotationMap.builder().merge(FirstAnnotation.class, null)); + } + + // ---- Builder.build tests ---- + + @Test + void shouldBuildEmptyMap_whenNoPuts() { + // when + AnnotationMap map = AnnotationMap.builder().build(); + + // then + Assertions.assertTrue(map.isEmpty()); + Assertions.assertEquals(0, map.size()); + } + + @Test + void shouldBuildWithAllEntries_whenMultiplePuts() { + // given + AnnotationAttributes attrs1 = new AnnotationAttributes(FirstAnnotation.class); + attrs1.put("key1", "val1"); + AnnotationAttributes attrs2 = new AnnotationAttributes(SecondAnnotation.class); + attrs2.put("key2", "val2"); + + // when + AnnotationMap map = AnnotationMap.builder() + .put(FirstAnnotation.class, attrs1) + .put(SecondAnnotation.class, attrs2) + .build(); + + // then + Assertions.assertEquals(2, map.size()); + Assertions.assertEquals("val1", map.getAttributes(FirstAnnotation.class).get("key1")); + Assertions.assertEquals( + "val2", map.getAttributes(SecondAnnotation.class).get("key2")); + } + + // ---- Equality tests ---- + + @Test + void shouldBeEqual_whenSameAnnotations() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(FirstAnnotation.class); + Map, AnnotationAttributes> data1 = new LinkedHashMap<>(); + data1.put(FirstAnnotation.class, attrs); + Map, AnnotationAttributes> data2 = new LinkedHashMap<>(); + data2.put(FirstAnnotation.class, attrs); + + AnnotationMap map1 = new AnnotationMap(data1); + AnnotationMap map2 = new AnnotationMap(data2); + + // when / then + Assertions.assertEquals(map1, map2); + Assertions.assertEquals(map1.hashCode(), map2.hashCode()); + } + + @Test + void shouldNotBeEqual_whenDifferentAnnotations() { + // given + Map, AnnotationAttributes> data1 = new LinkedHashMap<>(); + data1.put(FirstAnnotation.class, new AnnotationAttributes(FirstAnnotation.class)); + Map, AnnotationAttributes> data2 = new LinkedHashMap<>(); + data2.put(SecondAnnotation.class, new AnnotationAttributes(SecondAnnotation.class)); + + AnnotationMap map1 = new AnnotationMap(data1); + AnnotationMap map2 = new AnnotationMap(data2); + + // when / then + Assertions.assertNotEquals(map1, map2); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java new file mode 100644 index 000000000..f67d1051b --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotationMetadataReader} + */ +class AnnotationMetadataReaderTest { + + // ---- Test annotation definitions ---- + + @FesodMarked + @ColumnWidth(35) + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposableColumnWidth { + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int value() default 25; + } + + // ---- Annotated elements ---- + + @ColumnWidth(20) + static String columnWidthField; + + @ComposableColumnWidth(15) + static String composableField; + + static String plainField; + + @ColumnWidth(30) + static class ClassWithColumnWidth {} + + private AnnotationMetadataReader reader; + + @BeforeEach + void setUp() { + reader = new AnnotationMetadataReader(); + } + + private Field getField(String name) { + try { + return AnnotationMetadataReaderTest.class.getDeclaredField(name); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + // ---- read tests ---- + + @Test + void shouldReadInnerAnnotation_fromAnnotatedField() { + // given + Field field = getField("columnWidthField"); + + // when + AnnotationMap result = reader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + AnnotationAttributes attrs = result.getAttributes(ColumnWidth.class); + + Integer actualValue = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(20, actualValue); + } + + @Test + void shouldReadInnerAnnotation_fromAnnotatedClass() { + // given + Class clazz = ClassWithColumnWidth.class; + + // when + AnnotationMap result = reader.read(clazz); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + AnnotationAttributes attrs = result.getAttributes(ColumnWidth.class); + + Integer actualValue = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(30, actualValue); + } + + @Test + void shouldSynthesizeAlias_fromComposableAnnotation() { + // given + Field field = getField("composableField"); + + // when + AnnotationMap result = reader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + // AliasFor overrides ColumnWidth.value from 25 (on meta-annotation) to 15 (from composable) + AnnotationAttributes attrs = result.getAttributes(ColumnWidth.class); + + Integer actualValue = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(15, actualValue); + } + + @Test + void shouldContainComposableAnnotation_inAnnotationMap() { + // given + Field field = getField("composableField"); + + // when + AnnotationMap result = reader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ComposableColumnWidth.class)); + AnnotationAttributes attrs = result.getAttributes(ComposableColumnWidth.class); + + Integer actualValue = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(15, actualValue); + } + + @Test + void shouldReturnNull_fromUnannotatedField() { + // given + Field field = getField("plainField"); + + // when + AnnotationMap result = reader.read(field); + + // then + Assertions.assertNull(result); + } + + // ---- Caching tests ---- + + @Test + void shouldReturnSameInstance_whenReadTwiceOnSameElement() { + // given + Field field = getField("columnWidthField"); + + // when + AnnotationMap first = reader.read(field); + AnnotationMap second = reader.read(field); + + // then + Assertions.assertSame(first, second); + } + + @Test + void shouldReturnIndependentResults_forDifferentElements() { + // given + Field field = getField("columnWidthField"); + Class clazz = ClassWithColumnWidth.class; + + // when + AnnotationMap fieldResult = reader.read(field); + AnnotationMap classResult = reader.read(clazz); + + // then + Assertions.assertNotSame(fieldResult, classResult); + AnnotationAttributes fieldAttrs = fieldResult.getAttributes(ColumnWidth.class); + AnnotationAttributes classAttrs = classResult.getAttributes(ColumnWidth.class); + + Integer fieldValue = + Assertions.assertDoesNotThrow(() -> fieldAttrs.getRequiredAttribute("value", Integer.class)); + Integer classValue = + Assertions.assertDoesNotThrow(() -> classAttrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(20, fieldValue); + Assertions.assertEquals(30, classValue); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolverTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolverTest.java new file mode 100644 index 000000000..a0baf1221 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataResolverTest.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Native; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotationMetadataResolver}, {@link DefaultAnnotationMetadataResolver} + */ +class AnnotationMetadataResolverTest { + + // ---- Test annotation definitions ---- + + @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface PlainAnnotation { + String name() default ""; + + int value() default 0; + } + + @FesodMarked + @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface MarkedAnnotation { + String name() default ""; + } + + @FesodMarked + @ColumnWidth(25) + @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface AliasAnnotation { + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int width() default 25; + } + + @FesodMarked + @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface BadAliasAnnotation { + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int width() default 20; + } + + // ---- Annotated fields for retrieving annotation instances ---- + + @PlainAnnotation(name = "test", value = 42) + static String plainField; + + @MarkedAnnotation(name = "marked") + static String markedField; + + @MarkedAnnotation + static String defaultMarkedField; + + @AliasAnnotation(width = 30) + static String aliasField; + + @BadAliasAnnotation(width = 15) + static String badAliasField; + + @HeadStyle + static String headStyleField; + + private DefaultAnnotationMetadataResolver resolver; + + @BeforeEach + void setUp() { + resolver = new DefaultAnnotationMetadataResolver(); + } + + private A getFieldAnnotation(String fieldName, Class type) { + try { + Field field = AnnotationMetadataResolverTest.class.getDeclaredField(fieldName); + return field.getAnnotation(type); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + // ---- shouldIgnore tests ---- + + @Test + void shouldIgnore_javaLangMetaAnnotations() { + // when / then + Assertions.assertTrue(resolver.shouldIgnore(Target.class)); + Assertions.assertTrue(resolver.shouldIgnore(Retention.class)); + Assertions.assertTrue(resolver.shouldIgnore(Documented.class)); + Assertions.assertTrue(resolver.shouldIgnore(Repeatable.class)); + Assertions.assertTrue(resolver.shouldIgnore(Native.class)); + Assertions.assertTrue(resolver.shouldIgnore(Inherited.class)); + } + + @Test + void shouldNotIgnore_innerAnnotations() { + // when / then + Assertions.assertFalse(resolver.shouldIgnore((ExcelProperty.class))); + Assertions.assertFalse(resolver.shouldIgnore((ExcelIgnoreUnannotated.class))); + Assertions.assertFalse(resolver.shouldIgnore((ExcelIgnore.class))); + Assertions.assertFalse(resolver.shouldIgnore((DateTimeFormat.class))); + Assertions.assertFalse(resolver.shouldIgnore((NumberFormat.class))); + Assertions.assertFalse(resolver.shouldIgnore((ColumnWidth.class))); + Assertions.assertFalse(resolver.shouldIgnore((ContentFontStyle.class))); + Assertions.assertFalse(resolver.shouldIgnore((ContentLoopMerge.class))); + Assertions.assertFalse(resolver.shouldIgnore((ContentRowHeight.class))); + Assertions.assertFalse(resolver.shouldIgnore((ContentStyle.class))); + Assertions.assertFalse(resolver.shouldIgnore((HeadFontStyle.class))); + Assertions.assertFalse(resolver.shouldIgnore((HeadRowHeight.class))); + Assertions.assertFalse(resolver.shouldIgnore((HeadStyle.class))); + Assertions.assertFalse(resolver.shouldIgnore((OnceAbsoluteMerge.class))); + } + + @Test + void shouldNotIgnore_customAnnotations() { + // when / then + Assertions.assertFalse(resolver.shouldIgnore(PlainAnnotation.class)); + Assertions.assertFalse(resolver.shouldIgnore(MarkedAnnotation.class)); + } + + // ---- isInnerAnnotated tests ---- + + @Test + void shouldBeInnerAnnotated_forColumnWidth() { + // given + ColumnWidth cw = AliasAnnotation.class.getAnnotation(ColumnWidth.class); + + // when / then + Assertions.assertTrue(resolver.isInnerAnnotated(cw)); + } + + @Test + void shouldBeInnerAnnotated_forHeadStyle() { + // given + HeadStyle hs = getFieldAnnotation("headStyleField", HeadStyle.class); + + // when / then + Assertions.assertTrue(resolver.isInnerAnnotated(hs)); + } + + @Test + void shouldNotBeInnerAnnotated_forPlainAnnotation() { + // given + PlainAnnotation ann = getFieldAnnotation("plainField", PlainAnnotation.class); + + // when / then + Assertions.assertFalse(resolver.isInnerAnnotated(ann)); + } + + @Test + void shouldNotBeInnerAnnotated_forJavaLangAnnotation() { + // given + Target target = PlainAnnotation.class.getAnnotation(Target.class); + + // when / then + Assertions.assertFalse(resolver.isInnerAnnotated(target)); + } + + // ---- isMetaMarked tests ---- + + @Test + void shouldBeMetaMarked_forFesodMarkedAnnotation() { + // given + MarkedAnnotation ann = getFieldAnnotation("markedField", MarkedAnnotation.class); + + // when / then + Assertions.assertTrue(resolver.isMetaMarked(ann)); + } + + @Test + void shouldNotBeMetaMarked_forPlainAnnotation() { + // given + PlainAnnotation ann = getFieldAnnotation("plainField", PlainAnnotation.class); + + // when / then + Assertions.assertFalse(resolver.isMetaMarked(ann)); + } + + @Test + void shouldNotBeMetaMarked_forInnerAnnotation() { + // given + ColumnWidth cw = AliasAnnotation.class.getAnnotation(ColumnWidth.class); + + // when / then + Assertions.assertFalse(resolver.isMetaMarked(cw)); + } + + // ---- resolve tests ---- + + @Test + void shouldResolveAttributes_forPlainAnnotation() { + // given + PlainAnnotation ann = getFieldAnnotation("plainField", PlainAnnotation.class); + + // when + AnnotationMetadata metadata = resolver.resolve(ann); + + // then + Assertions.assertEquals(PlainAnnotation.class, metadata.getAttributes().getAnnotationType()); + Assertions.assertEquals("test", metadata.getAttributes().get("name")); + Assertions.assertEquals(42, metadata.getAttributes().get("value")); + Assertions.assertTrue(metadata.getAliases().isEmpty()); + } + + @Test + void shouldResolveAttributes_forMarkedAnnotationWithoutAlias() { + // given + MarkedAnnotation ann = getFieldAnnotation("markedField", MarkedAnnotation.class); + + // when + AnnotationMetadata metadata = resolver.resolve(ann); + + // then + Assertions.assertEquals(MarkedAnnotation.class, metadata.getAttributes().getAnnotationType()); + Assertions.assertEquals("marked", metadata.getAttributes().get("name")); + Assertions.assertTrue(metadata.getAliases().isEmpty()); + } + + @Test + void shouldResolveDefaultValues_whenNotExplicitlySet() { + // given + MarkedAnnotation ann = getFieldAnnotation("defaultMarkedField", MarkedAnnotation.class); + + // when + AnnotationMetadata metadata = resolver.resolve(ann); + + // then + Assertions.assertEquals("", metadata.getAttributes().get("name")); + Assertions.assertTrue(metadata.getAliases().isEmpty()); + } + + @Test + void shouldResolveAlias_forAliasAnnotation() { + // given + AliasAnnotation ann = getFieldAnnotation("aliasField", AliasAnnotation.class); + + // when + AnnotationMetadata metadata = resolver.resolve(ann); + + // then + Assertions.assertEquals(AliasAnnotation.class, metadata.getAttributes().getAnnotationType()); + Assertions.assertEquals(30, metadata.getAttributes().get("width")); + Assertions.assertEquals(1, metadata.getAliases().size()); + + AliasFor alias = metadata.getAliases().get(0); + Assertions.assertEquals(AliasAnnotation.class, alias.getMarked()); + Assertions.assertEquals(ColumnWidth.class, alias.getTarget()); + Assertions.assertEquals("value", alias.getAttribute()); + Assertions.assertEquals(30, alias.getValue()); + } + + @Test + void shouldThrow_whenAliasTargetNotPresentOnMarkedAnnotation() { + // given + BadAliasAnnotation ann = getFieldAnnotation("badAliasField", BadAliasAnnotation.class); + + // when / then + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> resolver.resolve(ann)); + Assertions.assertTrue(ex.getMessage().contains("ColumnWidth")); + Assertions.assertTrue(ex.getMessage().contains("BadAliasAnnotation")); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataTest.java new file mode 100644 index 000000000..9a5df6105 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataTest.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotationMetadata} + */ +class AnnotationMetadataTest { + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation {} + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface TargetAnnotation {} + + // ---- Constructor / getter tests ---- + + @Test + void shouldStoreAttributesAndAliases_whenConstructed() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", "test"); + List aliases = + Collections.singletonList(new AliasFor(TestAnnotation.class, TargetAnnotation.class, "value", "test")); + + // when + AnnotationMetadata metadata = new AnnotationMetadata(attrs, aliases); + + // then + Assertions.assertSame(attrs, metadata.getAttributes()); + Assertions.assertSame(aliases, metadata.getAliases()); + Assertions.assertEquals("test", metadata.getAttributes().get("value")); + } + + @Test + void shouldStoreEmptyAliases_whenConstructedWithEmptyList() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + + // when + AnnotationMetadata metadata = new AnnotationMetadata(attrs, Collections.emptyList()); + + // then + Assertions.assertTrue(metadata.getAliases().isEmpty()); + } + + @Test + void shouldStoreMultipleAliases_whenConstructedWithMultiple() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + List aliases = Arrays.asList( + new AliasFor(TestAnnotation.class, TargetAnnotation.class, "value", "a"), + new AliasFor(TestAnnotation.class, TargetAnnotation.class, "index", 1)); + + // when + AnnotationMetadata metadata = new AnnotationMetadata(attrs, aliases); + + // then + Assertions.assertEquals(2, metadata.getAliases().size()); + Assertions.assertEquals("value", metadata.getAliases().get(0).getAttribute()); + Assertions.assertEquals("index", metadata.getAliases().get(1).getAttribute()); + } + + // ---- addTo tests ---- + + @Test + void shouldAddAllAliases_whenAddToTargetList() { + // given + AliasFor alias1 = new AliasFor(TestAnnotation.class, TargetAnnotation.class, "value", "a"); + AliasFor alias2 = new AliasFor(TestAnnotation.class, TargetAnnotation.class, "index", 1); + AnnotationMetadata metadata = + new AnnotationMetadata(new AnnotationAttributes(TestAnnotation.class), Arrays.asList(alias1, alias2)); + List target = new ArrayList<>(); + + // when + metadata.addTo(target); + + // then + Assertions.assertEquals(2, target.size()); + Assertions.assertSame(alias1, target.get(0)); + Assertions.assertSame(alias2, target.get(1)); + } + + @Test + void shouldNotModifyTarget_whenAliasesEmpty() { + // given + AnnotationMetadata metadata = + new AnnotationMetadata(new AnnotationAttributes(TestAnnotation.class), Collections.emptyList()); + List target = new ArrayList<>(); + + // when + metadata.addTo(target); + + // then + Assertions.assertTrue(target.isEmpty()); + } + + @Test + void shouldAppendToExisting_whenTargetNotEmpty() { + // given + AliasFor existingAlias = new AliasFor(TestAnnotation.class, TargetAnnotation.class, "existing", "val"); + AliasFor newAlias = new AliasFor(TestAnnotation.class, TargetAnnotation.class, "added", "val"); + AnnotationMetadata metadata = new AnnotationMetadata( + new AnnotationAttributes(TestAnnotation.class), Collections.singletonList(newAlias)); + List target = new ArrayList<>(); + target.add(existingAlias); + + // when + metadata.addTo(target); + + // then + Assertions.assertEquals(2, target.size()); + Assertions.assertSame(existingAlias, target.get(0)); + Assertions.assertSame(newAlias, target.get(1)); + } + + // ---- setDistance tests ---- + + @Test + void shouldDelegateToAttributes_whenSetDistance() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + AnnotationMetadata metadata = new AnnotationMetadata(attrs, Collections.emptyList()); + + // when + metadata.setDistance(3); + + // then + Assertions.assertEquals(3, attrs.getDistance()); + Assertions.assertEquals(3, metadata.getAttributes().getDistance()); + } + + @Test + void shouldUpdateDistance_whenSetDistanceMultipleTimes() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + AnnotationMetadata metadata = new AnnotationMetadata(attrs, Collections.emptyList()); + + // when + metadata.setDistance(5); + metadata.setDistance(0); + + // then + Assertions.assertEquals(0, attrs.getDistance()); + } + + // ---- Equality tests ---- + + @Test + void shouldBeEqual_whenSameAttributesAndAliases() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + attrs.put("value", "test"); + AliasFor alias = new AliasFor(TestAnnotation.class, TargetAnnotation.class, "value", "test"); + + AnnotationMetadata meta1 = new AnnotationMetadata(attrs, Collections.singletonList(alias)); + AnnotationMetadata meta2 = new AnnotationMetadata(attrs, Collections.singletonList(alias)); + + // when / then + Assertions.assertEquals(meta1, meta2); + Assertions.assertEquals(meta1.hashCode(), meta2.hashCode()); + } + + @Test + void shouldNotBeEqual_whenDifferentAttributes() { + // given + AnnotationAttributes attrs1 = new AnnotationAttributes(TestAnnotation.class); + AnnotationAttributes attrs2 = new AnnotationAttributes(TestAnnotation.class); + attrs2.put("value", "different"); + + AnnotationMetadata meta1 = new AnnotationMetadata(attrs1, Collections.emptyList()); + AnnotationMetadata meta2 = new AnnotationMetadata(attrs2, Collections.emptyList()); + + // when / then + Assertions.assertNotEquals(meta1, meta2); + } + + @Test + void shouldNotBeEqual_whenDifferentAliases() { + // given + AnnotationAttributes attrs = new AnnotationAttributes(TestAnnotation.class); + AliasFor alias = new AliasFor(TestAnnotation.class, TargetAnnotation.class, "value", "a"); + + AnnotationMetadata meta1 = new AnnotationMetadata(attrs, Collections.singletonList(alias)); + AnnotationMetadata meta2 = new AnnotationMetadata(attrs, Collections.emptyList()); + + // when / then + Assertions.assertNotEquals(meta1, meta2); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java new file mode 100644 index 000000000..c16caf730 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java @@ -0,0 +1,1054 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation.composite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.RoundingMode; +import lombok.Data; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; +import org.apache.fesod.sheet.annotation.AnnotationMap; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.FesodMarked; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.apache.fesod.sheet.enums.BooleanEnum; +import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.metadata.ConfigurationHolder; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.Head; +import org.apache.fesod.sheet.metadata.property.ExcelHeadProperty; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for composable-annotation initialization analysis. + *

+ * Covered inner annotations: + *

    + *
  • {@link ExcelProperty}
  • + *
  • {@link DateTimeFormat}
  • + *
  • {@link NumberFormat}
  • + *
  • {@link ColumnWidth}
  • + *
  • {@link HeadStyle}
  • + *
  • {@link HeadFontStyle}
  • + *
  • {@link ContentStyle}
  • + *
  • {@link ContentFontStyle}
  • + *
  • {@link ContentLoopMerge}
  • + *
  • {@link HeadRowHeight}
  • + *
  • {@link ContentRowHeight}
  • + *
  • {@link OnceAbsoluteMerge}
  • + *
+ *

+ * Covered test scenarios: + *

    + *
  • Field-level composable — partial {@code @AliasFor}, full {@code @AliasFor}, + * no-methods grouping, priority when direct and composable coexist
  • + *
  • Class-level composable — {@code @AliasFor} with value propagation, + * no-methods grouping (single and multi-annotation presets)
  • + *
  • Mixed-level composable — class + field composable simultaneously, + * multiple fields with independent composable annotations per field
  • + *
  • Error cases — {@code @FesodMarked} with invalid {@code @AliasFor} target, + * mixed valid/invalid {@code @AliasFor} targets on the same composable annotation
  • + *
+ */ +@ExtendWith(MockitoExtension.class) +class CompositeAnnotationTest { + + @Mock + private ConfigurationHolder configurationHolder; + + @Mock + private GlobalConfiguration globalConfiguration; + + @BeforeEach + void setup() { + Mockito.lenient().when(configurationHolder.globalConfiguration()).thenReturn(globalConfiguration); + Mockito.lenient().when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + } + + // ---- Custom composable annotations ---- + + /** + * A composable annotation with {@code @FesodMarked} but missing the target meta-annotation + * that the {@code @AliasFor} points to. Used to verify error handling. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @Inherited + public @interface CustomExcelProperty1 { + + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value") + String[] value() default {"Default"}; + } + + /** + * A composable annotation with a single {@code @AliasFor} for {@code @ExcelProperty.value}. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ExcelProperty + @Inherited + public @interface ComposableExcelProperty { + + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value") + String[] value() default {""}; + } + + /** + * A composable annotation where ALL attributes of {@code @ExcelProperty} are aliased. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ExcelProperty + @Inherited + public @interface FullyComposableExcelProperty { + + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value") + String[] value() default {""}; + + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "index") + int index() default -1; + + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "order") + int order() default Integer.MAX_VALUE; + } + + /** + * A composable annotation for ColumnWidth usable on both TYPE and FIELD. + */ + @Target({ElementType.TYPE, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ColumnWidth + @Inherited + public @interface ComposableColumnWidth { + + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int value() default -1; + } + + /** + * A composable annotation with no methods — groups {@code @ColumnWidth} and {@code @HeadStyle} + * as a class-level style preset. + */ + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ColumnWidth(value = 10) + @HeadStyle(fillForegroundColor = 10) + @Inherited + public @interface ComposableAnnotationWithCommonStyle {} + + /** + * A composable annotation with no methods, meta-annotated with {@code @ExcelProperty}. + * Used to verify that when both the original annotation and the composable annotation + * coexist at the same level, the original annotation has higher priority. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ExcelProperty(value = {"Full Name"}) + @Inherited + public @interface ComposableExcelPropertyPreset {} + + /** + * Composable annotation with {@code @AliasFor} for {@code @NumberFormat.value}. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @NumberFormat + @Inherited + public @interface ComposableNumberFormat { + + @FesodMarked.AliasFor(annotation = NumberFormat.class, attribute = "value") + String value() default ""; + } + + /** + * Composable annotation with no methods — groups {@code @ContentStyle} and {@code @ContentFontStyle} + * as a content style preset. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentStyle(wrapped = BooleanEnum.TRUE, fillForegroundColor = 10) + @ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = BooleanEnum.TRUE) + @Inherited + public @interface ComposableContentStylePreset {} + + /** + * Composable annotation with no methods — groups {@code @HeadRowHeight}, {@code @ContentRowHeight}, + * and {@code @OnceAbsoluteMerge} as a table style preset for class-level use. + */ + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadRowHeight(30) + @ContentRowHeight(20) + @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3) + @Inherited + public @interface ComposableTableStylePreset {} + + /** + * Composable annotation with {@code @AliasFor} for {@code @DateTimeFormat.value}. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @DateTimeFormat + @Inherited + public @interface ComposableDateTimeFormat { + + @FesodMarked.AliasFor(annotation = DateTimeFormat.class, attribute = "value") + String value() default ""; + } + + /** + * Composable annotation with {@code @AliasFor} for both attributes of {@code @ContentLoopMerge}. + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentLoopMerge + @Inherited + public @interface ComposableContentLoopMerge { + + @FesodMarked.AliasFor(annotation = ContentLoopMerge.class, attribute = "eachRow") + int eachRow() default 1; + + @FesodMarked.AliasFor(annotation = ContentLoopMerge.class, attribute = "columnExtend") + int columnExtend() default 1; + } + + /** + * Composable annotation with no methods — groups {@code @HeadStyle} and {@code @HeadFontStyle} + * as a header style preset for class-level use. + */ + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadStyle(fillForegroundColor = 10) + @HeadFontStyle(fontName = "Calibri", fontHeightInPoints = 14, bold = BooleanEnum.TRUE) + @Inherited + public @interface ComposableHeaderStylePreset {} + + // ---- Model classes ---- + + @Data + static class ExcelModelAliasError { + + @CustomExcelProperty1 + private String str1; + } + + /** + * A composable annotation with mixed valid/invalid @AliasFor targets: + * valid — ExcelProperty.value (ExcelProperty IS a meta-annotation) + * invalid — ColumnWidth.value (ColumnWidth is NOT a meta-annotation) + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ExcelProperty + @Inherited + public @interface CustomExcelPropertyMixedAlias { + + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value") + String[] value() default {""}; + + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int width() default -1; + } + + @Data + static class ExcelModelMixedAliasError { + + @CustomExcelPropertyMixedAlias + private String str1; + } + + static class ExcelModelWithComposableField { + + @ComposableExcelProperty({"Custom Name"}) + private String name; + } + + static class ExcelModelWithFullyComposableField { + + @FullyComposableExcelProperty( + value = {"Full Name", "Common Config"}, + index = 2, + order = 100) + private String name; + } + + @ComposableColumnWidth(25) + static class ExcelModelWithComposableClassAnnotation { + + @ExcelProperty("Name") + private String name; + } + + @ComposableAnnotationWithCommonStyle + static class ExcelModelWithComposableGroupAnnotation { + + @ExcelProperty("Name") + private String name; + } + + static class ExcelModelWithPriorityConflict { + + @ExcelProperty(value = {"First Name"}) + @ComposableExcelPropertyPreset + private String name; + } + + static class ExcelModelWithComposableNumberFormat { + + @ComposableNumberFormat("#,##0.00") + private String amount; + } + + static class ExcelModelWithComposableContentStyle { + + @ComposableContentStylePreset + @ExcelProperty("Data") + private String data; + } + + @ComposableTableStylePreset + static class ExcelModelWithComposableTableStyle { + + @ExcelProperty("Name") + private String name; + } + + @ComposableTableStylePreset + static class ExcelModelMixedClassAndFieldComposable { + + @ComposableExcelProperty({"Mixed Name"}) + private String name; + } + + @ComposableAnnotationWithCommonStyle + static class ExcelModelMixedBothNoMethods { + + @ComposableContentStylePreset + @ExcelProperty("Data") + private String data; + } + + @ComposableColumnWidth(50) + static class ExcelModelMixedAliasForBothLevels { + + @ComposableNumberFormat("0.00%") + private String ratio; + } + + @ComposableTableStylePreset + static class ExcelModelMixedMultipleFields { + + @ComposableExcelProperty({"Name"}) + private String name; + + @ComposableNumberFormat("#,##0.00") + private String amount; + } + + static class ExcelModelWithComposableDateTimeFormat { + + @ComposableDateTimeFormat("yyyy-MM-dd HH:mm") + private String date; + } + + static class ExcelModelWithComposableContentLoopMerge { + + @ComposableContentLoopMerge(eachRow = 3, columnExtend = 2) + private String value; + } + + @ComposableHeaderStylePreset + static class ExcelModelWithComposableHeaderStyle { + + @ExcelProperty("Name") + private String name; + } + + @ComposableHeaderStylePreset + static class ExcelModelMixedHeaderStyleAndDateFormat { + + @ComposableDateTimeFormat("yyyy-MM-dd") + private String date; + } + + // ---- Tests ---- + + @Nested + class FieldLevelCompositeAnnotationTest { + + @Test + void shouldIncludeComposableAndInnerAnnotation_whenPartialAliasFor() { + // given - ExcelModelWithComposableField has @ComposableExcelProperty({"Custom Name"}) + // which only aliases "value" attribute of @ExcelProperty + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableField.class, null); + + // then + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + Assertions.assertNotNull(head); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertFalse(fieldAnnotationMap.isEmpty()); + Assertions.assertEquals(2, fieldAnnotationMap.size()); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableExcelProperty.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + + String[] expectedValue = {"Custom Name"}; + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(ComposableExcelProperty.class); + Assertions.assertArrayEquals(expectedValue, customAttrs.getRequiredAttribute("value", String[].class)); + + AnnotationAttributes targetAttrs = fieldAnnotationMap.getAttributes(ExcelProperty.class); + Assertions.assertArrayEquals(expectedValue, targetAttrs.getRequiredAttribute("value", String[].class)); + } + + @Test + void shouldIncludeAllAliasedValuesInComposable_whenAllParamsAliasFor() { + // given - ExcelModelWithFullyComposableField has @FullyComposableExcelProperty + // which aliases ALL attributes (value, index, order) of @ExcelProperty + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithFullyComposableField.class, null); + + // then + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + Assertions.assertNotNull(head); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertFalse(fieldAnnotationMap.isEmpty()); + Assertions.assertEquals(2, fieldAnnotationMap.size()); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(FullyComposableExcelProperty.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + + String[] expectedValue = {"Full Name", "Common Config"}; + int expectedIndex = 2; + int expectedOrder = 100; + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(FullyComposableExcelProperty.class); + Assertions.assertArrayEquals(expectedValue, customAttrs.getRequiredAttribute("value", String[].class)); + Assertions.assertEquals(expectedIndex, customAttrs.getRequiredAttribute("index", Integer.class)); + Assertions.assertEquals(expectedOrder, customAttrs.getRequiredAttribute("order", Integer.class)); + + AnnotationAttributes targetAttrs = fieldAnnotationMap.getAttributes(ExcelProperty.class); + Assertions.assertArrayEquals(expectedValue, targetAttrs.getRequiredAttribute("value", String[].class)); + Assertions.assertEquals(expectedIndex, targetAttrs.getRequiredAttribute("index", Integer.class)); + Assertions.assertEquals(expectedOrder, targetAttrs.getRequiredAttribute("order", Integer.class)); + } + + @Test + void shouldPreserveDirectAnnotationValue_whenOriginalAndComposableAtSameLevel() { + // given - field has both @ExcelProperty({"First Name"}) directly and + // @ComposableExcelPropertyPreset (which meta-annotates @ExcelProperty({"Full Name"})) + // At the same level, the direct annotation has higher priority + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithPriorityConflict.class, null); + + // then + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + Assertions.assertNotNull(head); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertFalse(fieldAnnotationMap.isEmpty()); + Assertions.assertEquals(2, fieldAnnotationMap.size()); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableExcelPropertyPreset.class)); + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(ComposableExcelPropertyPreset.class); + Assertions.assertNotNull(customAttrs); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes attrs = fieldAnnotationMap.getAttributes(ExcelProperty.class); + Assertions.assertArrayEquals( + new String[] {"First Name"}, attrs.getRequiredAttribute("value", String[].class)); + } + + @Test + void shouldIncludeComposableAndInnerAnnotation_whenNumberFormatWithAliasFor() { + // given - ExcelModelWithComposableNumberFormat has @ComposableNumberFormat("#,##0.00") + // which aliases "value" attribute of @NumberFormat + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableNumberFormat.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertEquals(2, annotationMap.size()); + Assertions.assertTrue(annotationMap.hasAnnotation(ComposableNumberFormat.class)); + Assertions.assertTrue(annotationMap.hasAnnotation(NumberFormat.class)); + + AnnotationAttributes customAttrs = annotationMap.getAttributes(ComposableNumberFormat.class); + Assertions.assertEquals("#,##0.00", customAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes targetAttrs = annotationMap.getAttributes(NumberFormat.class); + Assertions.assertEquals("#,##0.00", targetAttrs.getRequiredAttribute("value", String.class)); + Assertions.assertEquals( + RoundingMode.HALF_UP, targetAttrs.getRequiredAttribute("roundingMode", RoundingMode.class)); + } + + @Test + void shouldExpandAllInnerAnnotations_whenContentStylePresetNoMethods() { + // given - ExcelModelWithComposableContentStyle has @ComposableContentStylePreset + // which groups @ContentStyle and @ContentFontStyle with no methods + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableContentStyle.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertEquals(4, annotationMap.size()); + Assertions.assertTrue(annotationMap.hasAnnotation(ComposableContentStylePreset.class)); + Assertions.assertTrue(annotationMap.hasAnnotation(ContentStyle.class)); + Assertions.assertTrue(annotationMap.hasAnnotation(ContentFontStyle.class)); + Assertions.assertTrue(annotationMap.hasAnnotation(ExcelProperty.class)); + + AnnotationAttributes customAttrs = annotationMap.getAttributes(ComposableContentStylePreset.class); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes styleAttrs = annotationMap.getAttributes(ContentStyle.class); + Assertions.assertEquals(BooleanEnum.TRUE, styleAttrs.getRequiredAttribute("wrapped", BooleanEnum.class)); + Assertions.assertEquals((short) 10, styleAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + + AnnotationAttributes fontAttrs = annotationMap.getAttributes(ContentFontStyle.class); + Assertions.assertEquals("Arial", fontAttrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 12, fontAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + Assertions.assertEquals(BooleanEnum.TRUE, fontAttrs.getRequiredAttribute("bold", BooleanEnum.class)); + } + + @Test + void shouldIncludeComposableAndInnerAnnotation_whenDateTimeFormatWithAliasFor() { + // given - ExcelModelWithComposableDateTimeFormat has @ComposableDateTimeFormat("yyyy-MM-dd HH:mm") + // which aliases "value" attribute of @DateTimeFormat + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableDateTimeFormat.class, null); + + // then + Assertions.assertNotNull(property.getHeadMap()); + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertEquals(2, annotationMap.size()); + Assertions.assertTrue(annotationMap.hasAnnotation(ComposableDateTimeFormat.class)); + Assertions.assertTrue(annotationMap.hasAnnotation(DateTimeFormat.class)); + + AnnotationAttributes customAttrs = annotationMap.getAttributes(ComposableDateTimeFormat.class); + Assertions.assertEquals("yyyy-MM-dd HH:mm", customAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes targetAttrs = annotationMap.getAttributes(DateTimeFormat.class); + Assertions.assertEquals("yyyy-MM-dd HH:mm", targetAttrs.getRequiredAttribute("value", String.class)); + } + + @Test + void shouldIncludeComposableAndInnerAnnotation_whenContentLoopMergeWithAliasFor() { + // given - ExcelModelWithComposableContentLoopMerge has @ComposableContentLoopMerge(eachRow=3, + // columnExtend=2) + // which aliases both attributes of @ContentLoopMerge + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableContentLoopMerge.class, null); + + // then + Assertions.assertNotNull(property.getHeadMap()); + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertEquals(2, annotationMap.size()); + Assertions.assertTrue(annotationMap.hasAnnotation(ComposableContentLoopMerge.class)); + Assertions.assertTrue(annotationMap.hasAnnotation(ContentLoopMerge.class)); + + AnnotationAttributes customAttrs = annotationMap.getAttributes(ComposableContentLoopMerge.class); + Assertions.assertEquals(3, customAttrs.getRequiredAttribute("eachRow", Integer.class)); + Assertions.assertEquals(2, customAttrs.getRequiredAttribute("columnExtend", Integer.class)); + + AnnotationAttributes targetAttrs = annotationMap.getAttributes(ContentLoopMerge.class); + Assertions.assertEquals(3, targetAttrs.getRequiredAttribute("eachRow", Integer.class)); + Assertions.assertEquals(2, targetAttrs.getRequiredAttribute("columnExtend", Integer.class)); + } + } + + @Nested + class ClassLevelCompositeAnnotationTest { + + @Test + void shouldIncludeComposableAndInnerAnnotation_whenAliasForPresent() { + // given - ExcelModelWithComposableClassAnnotation has @ComposableColumnWidth(25) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableClassAnnotation.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertFalse(classAnnotationMap.isEmpty()); + Assertions.assertEquals(2, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableColumnWidth.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + + AnnotationAttributes customAttrs = classAnnotationMap.getAttributes(ComposableColumnWidth.class); + Assertions.assertEquals(25, customAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes targetAttrs = classAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(25, targetAttrs.getRequiredAttribute("value", Integer.class)); + } + + @Test + void shouldExpandAllInnerAnnotations_whenNoMethodsInComposable() { + // given - ExcelModelWithComposableGroupAnnotation has @ComposableAnnotationWithCommonStyle + // which has no methods, but meta-annotates @ColumnWidth(10) and @HeadStyle(fillForegroundColor=10) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableGroupAnnotation.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertFalse(classAnnotationMap.isEmpty()); + Assertions.assertEquals(3, classAnnotationMap.size()); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableAnnotationWithCommonStyle.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + + AnnotationAttributes customAttrs = + classAnnotationMap.getAttributes(ComposableAnnotationWithCommonStyle.class); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes widthAttrs = classAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(10, widthAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes styleAttrs = classAnnotationMap.getAttributes(HeadStyle.class); + Assertions.assertEquals((short) 10, styleAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + } + + @Test + void shouldExpandAllInnerAnnotations_whenTableStylePresetNoMethods() { + // given - ExcelModelWithComposableTableStyle has @ComposableTableStylePreset + // which groups @HeadRowHeight(30), @ContentRowHeight(20), @OnceAbsoluteMerge(...) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableTableStyle.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(4, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableTableStylePreset.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadRowHeight.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentRowHeight.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(OnceAbsoluteMerge.class)); + + AnnotationAttributes customAttrs = classAnnotationMap.getAttributes(ComposableTableStylePreset.class); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes headHeightAttrs = classAnnotationMap.getAttributes(HeadRowHeight.class); + Assertions.assertEquals((short) 30, headHeightAttrs.getRequiredAttribute("value", Short.class)); + + AnnotationAttributes contentHeightAttrs = classAnnotationMap.getAttributes(ContentRowHeight.class); + Assertions.assertEquals((short) 20, contentHeightAttrs.getRequiredAttribute("value", Short.class)); + + AnnotationAttributes mergeAttrs = classAnnotationMap.getAttributes(OnceAbsoluteMerge.class); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("lastRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstColumnIndex", Integer.class)); + Assertions.assertEquals(3, mergeAttrs.getRequiredAttribute("lastColumnIndex", Integer.class)); + } + + @Test + void shouldExpandAllInnerAnnotations_whenHeaderStylePresetNoMethods() { + // given - ExcelModelWithComposableHeaderStyle has @ComposableHeaderStylePreset + // which groups @HeadStyle(fillForegroundColor=10) and @HeadFontStyle(fontName="Calibri", + // fontHeightInPoints=14, bold=TRUE) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableHeaderStyle.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(3, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableHeaderStylePreset.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadFontStyle.class)); + + AnnotationAttributes customAttrs = classAnnotationMap.getAttributes(ComposableHeaderStylePreset.class); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes styleAttrs = classAnnotationMap.getAttributes(HeadStyle.class); + Assertions.assertEquals((short) 10, styleAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + + AnnotationAttributes fontAttrs = classAnnotationMap.getAttributes(HeadFontStyle.class); + Assertions.assertEquals("Calibri", fontAttrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 14, fontAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + Assertions.assertEquals(BooleanEnum.TRUE, fontAttrs.getRequiredAttribute("bold", BooleanEnum.class)); + } + } + + @Nested + class MixedLevelCompositeAnnotationTest { + + @Test + void shouldPopulateBothLevels_whenClassComposableGroupAndFieldComposableAliasFor() { + // given - class has @ComposableTableStylePreset (groups HeadRowHeight, ContentRowHeight, OnceAbsoluteMerge) + // field has @ComposableExcelProperty({"Mixed Name"}) (aliases ExcelProperty.value) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedClassAndFieldComposable.class, null); + + // then - class-level annotationMap + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(4, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableTableStylePreset.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadRowHeight.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentRowHeight.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(OnceAbsoluteMerge.class)); + + AnnotationAttributes customTypeAttrs = classAnnotationMap.getAttributes(ComposableTableStylePreset.class); + Assertions.assertTrue(customTypeAttrs.isEmpty()); + + AnnotationAttributes headHeightAttrs = classAnnotationMap.getAttributes(HeadRowHeight.class); + Assertions.assertEquals((short) 30, headHeightAttrs.getRequiredAttribute("value", Short.class)); + + AnnotationAttributes contentHeightAttrs = classAnnotationMap.getAttributes(ContentRowHeight.class); + Assertions.assertEquals((short) 20, contentHeightAttrs.getRequiredAttribute("value", Short.class)); + + AnnotationAttributes mergeAttrs = classAnnotationMap.getAttributes(OnceAbsoluteMerge.class); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("lastRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstColumnIndex", Integer.class)); + Assertions.assertEquals(3, mergeAttrs.getRequiredAttribute("lastColumnIndex", Integer.class)); + + // then - field-level annotationMap + Assertions.assertNotNull(property.getHeadMap()); + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertEquals(2, fieldAnnotationMap.size()); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableExcelProperty.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + + String[] expectedValue = {"Mixed Name"}; + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(ComposableExcelProperty.class); + Assertions.assertArrayEquals(expectedValue, customAttrs.getRequiredAttribute("value", String[].class)); + + AnnotationAttributes targetAttrs = fieldAnnotationMap.getAttributes(ComposableExcelProperty.class); + Assertions.assertArrayEquals(expectedValue, targetAttrs.getRequiredAttribute("value", String[].class)); + } + + @Test + void shouldPopulateBothLevels_whenClassAndFieldBothUseNoMethodsComposable() { + // given - class has @ComposableAnnotationWithCommonStyle (groups ColumnWidth, HeadStyle) + // field has @ComposableContentStylePreset (groups ContentStyle, ContentFontStyle) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedBothNoMethods.class, null); + + // then - class-level annotationMap + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(3, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableAnnotationWithCommonStyle.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + + AnnotationAttributes customTypeAttrs = + classAnnotationMap.getAttributes(ComposableAnnotationWithCommonStyle.class); + Assertions.assertTrue(customTypeAttrs.isEmpty()); + + AnnotationAttributes widthAttrs = classAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(10, widthAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes styleTypeAttrs = classAnnotationMap.getAttributes(HeadStyle.class); + Assertions.assertEquals( + (short) 10, styleTypeAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + + // then - field-level annotationMap + Assertions.assertNotNull(property.getHeadMap()); + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertEquals(4, fieldAnnotationMap.size()); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableContentStylePreset.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ContentStyle.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ContentFontStyle.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(ComposableContentStylePreset.class); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes styleAttrs = fieldAnnotationMap.getAttributes(ContentStyle.class); + Assertions.assertEquals(BooleanEnum.TRUE, styleAttrs.getRequiredAttribute("wrapped", BooleanEnum.class)); + Assertions.assertEquals((short) 10, styleAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + + AnnotationAttributes fontAttrs = fieldAnnotationMap.getAttributes(ContentFontStyle.class); + Assertions.assertEquals("Arial", fontAttrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 12, fontAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + Assertions.assertEquals(BooleanEnum.TRUE, fontAttrs.getRequiredAttribute("bold", BooleanEnum.class)); + } + + @Test + void shouldPopulateBothLevels_whenClassAndFieldBothUseAliasForComposable() { + // given - class has @ComposableColumnWidth(50) (aliases ColumnWidth.value) + // field has @ComposableNumberFormat("0.00%") (aliases NumberFormat.value) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedAliasForBothLevels.class, null); + + // then - class-level annotationMap + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(2, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableColumnWidth.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + + AnnotationAttributes customWidthAttrs = classAnnotationMap.getAttributes(ComposableColumnWidth.class); + Assertions.assertEquals(50, customWidthAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes targetTypeAttrs = classAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(50, targetTypeAttrs.getRequiredAttribute("value", Integer.class)); + + // then - field-level annotationMap + Head head = property.getHeadMap().get(0); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertEquals(2, fieldAnnotationMap.size()); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableNumberFormat.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(NumberFormat.class)); + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(ComposableNumberFormat.class); + Assertions.assertEquals("0.00%", customAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes targetAttrs = fieldAnnotationMap.getAttributes(NumberFormat.class); + Assertions.assertEquals("0.00%", targetAttrs.getRequiredAttribute("value", String.class)); + Assertions.assertEquals( + RoundingMode.HALF_UP, targetAttrs.getRequiredAttribute("roundingMode", RoundingMode.class)); + } + + @Test + void shouldPopulateEachFieldIndependently_whenClassComposableAndMultipleFieldsWithComposable() { + // given - class has @ComposableTableStylePreset + // field 0 has @ComposableExcelProperty({"Name"}) + // field 1 has @ComposableNumberFormat("#,##0.00") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedMultipleFields.class, null); + + // then - class-level annotationMap is shared + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(4, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableTableStylePreset.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadRowHeight.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentRowHeight.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(OnceAbsoluteMerge.class)); + + AnnotationAttributes customAttrs = classAnnotationMap.getAttributes(ComposableTableStylePreset.class); + Assertions.assertTrue(customAttrs.isEmpty()); + + AnnotationAttributes headHeightAttrs = classAnnotationMap.getAttributes(HeadRowHeight.class); + Assertions.assertEquals((short) 30, headHeightAttrs.getRequiredAttribute("value", Short.class)); + + AnnotationAttributes contentHeightAttrs = classAnnotationMap.getAttributes(ContentRowHeight.class); + Assertions.assertEquals((short) 20, contentHeightAttrs.getRequiredAttribute("value", Short.class)); + + AnnotationAttributes mergeAttrs = classAnnotationMap.getAttributes(OnceAbsoluteMerge.class); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("lastRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstColumnIndex", Integer.class)); + Assertions.assertEquals(3, mergeAttrs.getRequiredAttribute("lastColumnIndex", Integer.class)); + + // then - each field has its own independent annotationMap + Assertions.assertNotNull(property.getHeadMap()); + Assertions.assertEquals(2, property.getHeadMap().size()); + + Head nameHead = property.getHeadMap().get(0); + AnnotationMap nameAnnotationMap = nameHead.getAnnotationMap(); + Assertions.assertNotNull(nameAnnotationMap); + Assertions.assertTrue(nameAnnotationMap.hasAnnotation(ComposableExcelProperty.class)); + Assertions.assertTrue(nameAnnotationMap.hasAnnotation(ExcelProperty.class)); + + String[] expectedValue = {"Name"}; + + AnnotationAttributes customField1Attrs = nameAnnotationMap.getAttributes(ComposableExcelProperty.class); + Assertions.assertArrayEquals( + expectedValue, customField1Attrs.getRequiredAttribute("value", String[].class)); + + AnnotationAttributes targetField1Attrs = nameAnnotationMap.getAttributes(ExcelProperty.class); + Assertions.assertArrayEquals( + expectedValue, targetField1Attrs.getRequiredAttribute("value", String[].class)); + + Head amountHead = property.getHeadMap().get(1); + AnnotationMap amountAnnotationMap = amountHead.getAnnotationMap(); + Assertions.assertNotNull(amountAnnotationMap); + Assertions.assertTrue(amountAnnotationMap.hasAnnotation(ComposableNumberFormat.class)); + Assertions.assertTrue(amountAnnotationMap.hasAnnotation(NumberFormat.class)); + + AnnotationAttributes customField2Attrs = amountAnnotationMap.getAttributes(ComposableNumberFormat.class); + Assertions.assertEquals("#,##0.00", customField2Attrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes targetField2Attrs = amountAnnotationMap.getAttributes(NumberFormat.class); + Assertions.assertEquals("#,##0.00", targetField2Attrs.getRequiredAttribute("value", String.class)); + Assertions.assertEquals( + RoundingMode.HALF_UP, targetField2Attrs.getRequiredAttribute("roundingMode", RoundingMode.class)); + } + + @Test + void shouldPopulateBothLevels_whenClassHeaderStylePresetAndFieldDateTimeFormat() { + // given - class has @ComposableHeaderStylePreset (groups HeadStyle + HeadFontStyle) + // field has @ComposableDateTimeFormat("yyyy-MM-dd") (aliases DateTimeFormat.value) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedHeaderStyleAndDateFormat.class, null); + + // then - class-level annotationMap + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(3, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableHeaderStylePreset.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadFontStyle.class)); + + AnnotationAttributes customTypeAttrs = classAnnotationMap.getAttributes(ComposableHeaderStylePreset.class); + Assertions.assertTrue(customTypeAttrs.isEmpty()); + + AnnotationAttributes styleAttrs = classAnnotationMap.getAttributes(HeadStyle.class); + Assertions.assertEquals((short) 10, styleAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + + AnnotationAttributes fontAttrs = classAnnotationMap.getAttributes(HeadFontStyle.class); + Assertions.assertEquals("Calibri", fontAttrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 14, fontAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + Assertions.assertEquals(BooleanEnum.TRUE, fontAttrs.getRequiredAttribute("bold", BooleanEnum.class)); + + // then - field-level annotationMap + Head head = property.getHeadMap().get(0); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertEquals(2, fieldAnnotationMap.size()); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableDateTimeFormat.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(DateTimeFormat.class)); + + AnnotationAttributes customAttrs = fieldAnnotationMap.getAttributes(ComposableDateTimeFormat.class); + Assertions.assertEquals("yyyy-MM-dd", customAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes targetAttrs = fieldAnnotationMap.getAttributes(DateTimeFormat.class); + Assertions.assertEquals("yyyy-MM-dd", targetAttrs.getRequiredAttribute("value", String.class)); + } + } + + @Nested + class ErrorCases { + + @Test + void shouldThrow_whenMarkedAnnotationHasInvalidAliasForTarget() { + // given - ExcelModelAliasError has @FesodMarked as meta-annotation, + // with @AliasFor: one targets ExcelProperty (invalid, NOT meta-present) + + Assertions.assertThrows( + IllegalStateException.class, + () -> new ExcelHeadProperty(configurationHolder, ExcelModelAliasError.class, null)); + } + + @Test + void shouldThrow_whenMarkedAnnotationHasMixedValidAndInvalidAliasForTargets() { + // given - CustomExcelPropertyMixedAlias has @FesodMarked and @ExcelProperty as meta-annotation, + // with two @AliasFor: one targets ExcelProperty (valid, meta-present) and + // one targets ColumnWidth (invalid, NOT meta-present) + + // when / then + Assertions.assertThrows( + IllegalStateException.class, + () -> new ExcelHeadProperty(configurationHolder, ExcelModelMixedAliasError.class, null)); + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java new file mode 100644 index 000000000..62ca06749 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java @@ -0,0 +1,664 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation.composite; + +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; +import org.apache.fesod.sheet.annotation.AnnotationMap; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.apache.fesod.sheet.enums.BooleanEnum; +import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.metadata.ConfigurationHolder; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.Head; +import org.apache.fesod.sheet.metadata.property.ExcelHeadProperty; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for direct (non-composable) annotation initialization analysis. + *

+ * Covered inner annotations: + *

    + *
  • {@link ExcelProperty}
  • + *
  • {@link DateTimeFormat}
  • + *
  • {@link NumberFormat}
  • + *
  • {@link ColumnWidth}
  • + *
  • {@link HeadStyle}
  • + *
  • {@link HeadFontStyle}
  • + *
  • {@link ContentStyle}
  • + *
  • {@link ContentFontStyle}
  • + *
  • {@link ContentLoopMerge}
  • + *
  • {@link HeadRowHeight}
  • + *
  • {@link ContentRowHeight}
  • + *
  • {@link OnceAbsoluteMerge}
  • + *
+ *

+ * Covered test scenarios: + *

    + *
  • Field-level — null annotationMap, single annotation, multiple annotations per field
  • + *
  • Class-level — null headClazzAnnotationMap, single annotation, multiple annotations per class
  • + *
  • Mixed-level — class + field annotations coexisting, all 12 annotations in a single model, + * per-field independence with shared class-level annotations
  • + *
+ */ +@ExtendWith(MockitoExtension.class) +class DirectAnnotationTest { + + @Mock + private ConfigurationHolder configurationHolder; + + @Mock + private GlobalConfiguration globalConfiguration; + + @BeforeEach + void setup() { + Mockito.lenient().when(configurationHolder.globalConfiguration()).thenReturn(globalConfiguration); + Mockito.lenient().when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + } + + // ---- Model classes ---- + + static class ExcelModelWithPlainField { + + private String name; + } + + static class ExcelModelWithFieldProperty { + + @ExcelProperty("Name") + private String name; + } + + static class ExcelModelWithMultipleFieldAnnotations { + + @ExcelProperty("Date") + @DateTimeFormat("yyyy-MM-dd") + @ColumnWidth(30) + private String date; + } + + static class ExcelModelWithNumberFormat { + + @ExcelProperty("Amount") + @NumberFormat("#,##0.00") + private String amount; + } + + static class ExcelModelWithContentFontStyle { + + @ExcelProperty("Name") + @ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = BooleanEnum.TRUE) + private String name; + } + + static class ExcelModelWithContentLoopMerge { + + @ExcelProperty("Value") + @ContentLoopMerge(eachRow = 2, columnExtend = 3) + private String value; + } + + static class ExcelModelWithContentStyle { + + @ExcelProperty("Data") + @ContentStyle(wrapped = BooleanEnum.TRUE, fillForegroundColor = 10) + private String data; + } + + static class ExcelModelWithHeadFontStyle { + + @ExcelProperty("Title") + @HeadFontStyle(fontName = "Calibri", color = 10) + private String title; + } + + @Nested + class FieldLevelAnnotationTest { + + // ---- Tests ---- + + @Test + void shouldSetNullFieldAnnotationMap_whenFieldHasNoRelevantAnnotations() { + // given - ExcelModelWithPlainField has a plain field without annotations + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithPlainField.class, null); + + // then + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + Assertions.assertNotNull(head); + Assertions.assertNull(head.getAnnotationMap()); + } + + @Test + void shouldPopulateFieldAnnotationMap_withExcelProperty_whenFieldAnnotated() { + // given - ExcelModelWithFieldProperty has @ExcelProperty("Name") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithFieldProperty.class, null); + + // then + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + Assertions.assertNotNull(head); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertFalse(fieldAnnotationMap.isEmpty()); + Assertions.assertEquals(1, fieldAnnotationMap.size()); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + + AnnotationAttributes attrs = fieldAnnotationMap.getAttributes(ExcelProperty.class); + String[] value = attrs.getRequiredAttribute("value", String[].class); + Assertions.assertArrayEquals(new String[] {"Name"}, value); + } + + @Test + void shouldPopulateFieldAnnotationMap_withMultipleAnnotations_whenFieldAnnotated() { + // given - ExcelModelWithMultipleFieldAnnotations has @ExcelProperty, @DateTimeFormat, @ColumnWidth + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithMultipleFieldAnnotations.class, null); + + // then + Assertions.assertEquals(1, property.getHeadMap().size()); + + Head head = property.getHeadMap().get(0); + Assertions.assertNotNull(head); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertFalse(fieldAnnotationMap.isEmpty()); + Assertions.assertEquals(3, fieldAnnotationMap.size()); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(DateTimeFormat.class)); + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ColumnWidth.class)); + + AnnotationAttributes dtAttrs = fieldAnnotationMap.getAttributes(DateTimeFormat.class); + Assertions.assertEquals("yyyy-MM-dd", dtAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes cwAttrs = fieldAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(30, cwAttrs.getRequiredAttribute("value", Integer.class)); + } + + @Test + void shouldPopulateFieldAnnotationMap_withNumberFormat_whenFieldAnnotated() { + // given - ExcelModelWithNumberFormat has @NumberFormat("#,##0.00") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithNumberFormat.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertTrue(annotationMap.hasAnnotation(NumberFormat.class)); + + AnnotationAttributes attrs = annotationMap.getAttributes(NumberFormat.class); + Assertions.assertEquals("#,##0.00", attrs.getRequiredAttribute("value", String.class)); + Assertions.assertEquals( + RoundingMode.HALF_UP, attrs.getRequiredAttribute("roundingMode", RoundingMode.class)); + } + + @Test + void shouldPopulateFieldAnnotationMap_withContentFontStyle_whenFieldAnnotated() { + // given - ExcelModelWithContentFontStyle has @ContentFontStyle(fontName="Arial", fontHeightInPoints=12, + // bold=TRUE) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithContentFontStyle.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertTrue(annotationMap.hasAnnotation(ContentFontStyle.class)); + + AnnotationAttributes attrs = annotationMap.getAttributes(ContentFontStyle.class); + Assertions.assertEquals("Arial", attrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 12, attrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + Assertions.assertEquals(BooleanEnum.TRUE, attrs.getRequiredAttribute("bold", BooleanEnum.class)); + } + + @Test + void shouldPopulateFieldAnnotationMap_withContentLoopMerge_whenFieldAnnotated() { + // given - ExcelModelWithContentLoopMerge has @ContentLoopMerge(eachRow=2, columnExtend=3) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithContentLoopMerge.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertTrue(annotationMap.hasAnnotation(ContentLoopMerge.class)); + + AnnotationAttributes attrs = annotationMap.getAttributes(ContentLoopMerge.class); + Assertions.assertEquals(2, attrs.getRequiredAttribute("eachRow", Integer.class)); + Assertions.assertEquals(3, attrs.getRequiredAttribute("columnExtend", Integer.class)); + } + + @Test + void shouldPopulateFieldAnnotationMap_withContentStyle_whenFieldAnnotated() { + // given - ExcelModelWithContentStyle has @ContentStyle(wrapped=TRUE, fillForegroundColor=10) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithContentStyle.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertTrue(annotationMap.hasAnnotation(ContentStyle.class)); + + AnnotationAttributes attrs = annotationMap.getAttributes(ContentStyle.class); + Assertions.assertEquals(BooleanEnum.TRUE, attrs.getRequiredAttribute("wrapped", BooleanEnum.class)); + Assertions.assertEquals((short) 10, attrs.getRequiredAttribute("fillForegroundColor", Short.class)); + } + + @Test + void shouldPopulateFieldAnnotationMap_withHeadFontStyle_whenFieldAnnotated() { + // given - ExcelModelWithHeadFontStyle has @HeadFontStyle(fontName="Calibri", color=10) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithHeadFontStyle.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotationMap annotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(annotationMap); + Assertions.assertTrue(annotationMap.hasAnnotation(HeadFontStyle.class)); + + AnnotationAttributes attrs = annotationMap.getAttributes(HeadFontStyle.class); + Assertions.assertEquals("Calibri", attrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 10, attrs.getRequiredAttribute("color", Short.class)); + } + } + + // ---- Model classes ---- + + static class ExcelModelWithoutAnnotations { + + @ExcelProperty("Name") + private String name; + } + + @ColumnWidth(20) + static class ExcelModelWithClassColumnWidth { + + @ExcelProperty("Name") + private String name; + } + + @ColumnWidth(15) + @HeadStyle(fillForegroundColor = 10) + static class ExcelModelWithMultipleClassAnnotations { + + @ExcelProperty("Name") + private String name; + } + + @ContentRowHeight(20) + static class ExcelModelWithContentRowHeight { + + @ExcelProperty("Name") + private String name; + } + + @HeadRowHeight(30) + static class ExcelModelWithHeadRowHeight { + + @ExcelProperty("Name") + private String name; + } + + @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 1, firstColumnIndex = 0, lastColumnIndex = 2) + static class ExcelModelWithOnceAbsoluteMerge { + + @ExcelProperty("Name") + private String name; + } + + @HeadRowHeight(30) + @ContentRowHeight(20) + @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 4) + @ColumnWidth(25) + @HeadStyle(fillForegroundColor = 15) + @HeadFontStyle(fontName = "Header", fontHeightInPoints = 14, bold = BooleanEnum.TRUE) + @ContentStyle(wrapped = BooleanEnum.TRUE) + @ContentFontStyle(fontName = "Content", fontHeightInPoints = 11) + static class ExcelModelMixedAllAnnotations { + + @ExcelProperty("Date") + @DateTimeFormat("yyyy-MM-dd") + @NumberFormat("#,##0.00") + @ContentLoopMerge(eachRow = 2, columnExtend = 3) + private String date; + } + + @ColumnWidth(20) + @HeadStyle(fillForegroundColor = 10) + static class ExcelModelMixedClassStyleAndFieldFormat { + + @ExcelProperty("Amount") + @NumberFormat("#,##0.00") + private String amount; + + @ExcelProperty("Date") + @DateTimeFormat("yyyy-MM-dd") + private String date; + } + + @Nested + class ClassLevelAnnotationTest { + + @Test + void shouldSetNullHeadClazzAnnotationMap_whenNoHeadClazzProvided() { + // given + List> head = new ArrayList<>(); + head.add(Arrays.asList("Name")); + + // when + ExcelHeadProperty property = new ExcelHeadProperty(configurationHolder, null, head); + + // then + Assertions.assertNull(property.getHeadClazzAnnotationMap()); + } + + @Test + void shouldSetNullHeadClazzAnnotationMap_whenHeadClazzHasNoAnnotations() { + // given - ExcelModelWithoutAnnotations has no class-level annotations + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithoutAnnotations.class, null); + + // then + Assertions.assertNull(property.getHeadClazzAnnotationMap()); + } + + @Test + void shouldPopulateHeadClazzAnnotationMap_withColumnWidth_whenClassAnnotated() { + // given - ExcelModelWithClassColumnWidth has @ColumnWidth(20) at class level + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithClassColumnWidth.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertFalse(classAnnotationMap.isEmpty()); + Assertions.assertEquals(1, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + + AnnotationAttributes widthAttrs = classAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(20, widthAttrs.getRequiredAttribute("value", Integer.class)); + } + + @Test + void shouldPopulateHeadClazzAnnotationMap_withMultipleAnnotations_whenClassAnnotated() { + // given - ExcelModelWithMultipleClassAnnotations has @ColumnWidth(15) and + // @HeadStyle(fillForegroundColor=10) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithMultipleClassAnnotations.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertFalse(classAnnotationMap.isEmpty()); + Assertions.assertEquals(2, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + + AnnotationAttributes widthAttrs = classAnnotationMap.getAttributes(ColumnWidth.class); + Assertions.assertEquals(15, widthAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes styleAttrs = classAnnotationMap.getAttributes(HeadStyle.class); + Assertions.assertEquals((short) 10, styleAttrs.getRequiredAttribute("fillForegroundColor", Short.class)); + } + + @Test + void shouldPopulateHeadClazzAnnotationMap_withContentRowHeight_whenClassAnnotated() { + // given - ExcelModelWithContentRowHeight has @ContentRowHeight(20) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithContentRowHeight.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentRowHeight.class)); + + AnnotationAttributes attrs = classAnnotationMap.getAttributes(ContentRowHeight.class); + Assertions.assertEquals((short) 20, attrs.getRequiredAttribute("value", Short.class)); + } + + @Test + void shouldPopulateHeadClazzAnnotationMap_withHeadRowHeight_whenClassAnnotated() { + // given - ExcelModelWithHeadRowHeight has @HeadRowHeight(30) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithHeadRowHeight.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadRowHeight.class)); + + AnnotationAttributes attrs = classAnnotationMap.getAttributes(HeadRowHeight.class); + Assertions.assertEquals((short) 30, attrs.getRequiredAttribute("value", Short.class)); + } + + @Test + void shouldPopulateHeadClazzAnnotationMap_withOnceAbsoluteMerge_whenClassAnnotated() { + // given - ExcelModelWithOnceAbsoluteMerge has @OnceAbsoluteMerge(firstRowIndex=0, lastRowIndex=1, + // firstColumnIndex=0, lastColumnIndex=2) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithOnceAbsoluteMerge.class, null); + + // then + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(OnceAbsoluteMerge.class)); + + AnnotationAttributes attrs = classAnnotationMap.getAttributes(OnceAbsoluteMerge.class); + Assertions.assertEquals(0, attrs.getRequiredAttribute("firstRowIndex", Integer.class)); + Assertions.assertEquals(1, attrs.getRequiredAttribute("lastRowIndex", Integer.class)); + Assertions.assertEquals(0, attrs.getRequiredAttribute("firstColumnIndex", Integer.class)); + Assertions.assertEquals(2, attrs.getRequiredAttribute("lastColumnIndex", Integer.class)); + } + } + + @Nested + class MixedLevelAnnotationTest { + + @Test + void shouldPopulateBothLevels_whenAllTwelveAnnotationsUsedAcrossClassAndField() { + // given - class-level: HeadRowHeight, ContentRowHeight, OnceAbsoluteMerge, ColumnWidth, + // HeadStyle, HeadFontStyle, ContentStyle, ContentFontStyle + // field-level: ExcelProperty, DateTimeFormat, NumberFormat, ContentLoopMerge + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedAllAnnotations.class, null); + + // then - class-level annotationMap covers 8 annotations + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(8, classAnnotationMap.size()); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadRowHeight.class)); + Assertions.assertEquals( + (short) 30, + classAnnotationMap.getAttributes(HeadRowHeight.class).getRequiredAttribute("value", Short.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentRowHeight.class)); + Assertions.assertEquals( + (short) 20, + classAnnotationMap + .getAttributes(ContentRowHeight.class) + .getRequiredAttribute("value", Short.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(OnceAbsoluteMerge.class)); + AnnotationAttributes mergeAttrs = classAnnotationMap.getAttributes(OnceAbsoluteMerge.class); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("lastRowIndex", Integer.class)); + Assertions.assertEquals(0, mergeAttrs.getRequiredAttribute("firstColumnIndex", Integer.class)); + Assertions.assertEquals(4, mergeAttrs.getRequiredAttribute("lastColumnIndex", Integer.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + Assertions.assertEquals( + 25, + classAnnotationMap.getAttributes(ColumnWidth.class).getRequiredAttribute("value", Integer.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + Assertions.assertEquals( + (short) 15, + classAnnotationMap + .getAttributes(HeadStyle.class) + .getRequiredAttribute("fillForegroundColor", Short.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadFontStyle.class)); + AnnotationAttributes hfAttrs = classAnnotationMap.getAttributes(HeadFontStyle.class); + Assertions.assertEquals("Header", hfAttrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 14, hfAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + Assertions.assertEquals(BooleanEnum.TRUE, hfAttrs.getRequiredAttribute("bold", BooleanEnum.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentStyle.class)); + Assertions.assertEquals( + BooleanEnum.TRUE, + classAnnotationMap + .getAttributes(ContentStyle.class) + .getRequiredAttribute("wrapped", BooleanEnum.class)); + + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentFontStyle.class)); + AnnotationAttributes cfAttrs = classAnnotationMap.getAttributes(ContentFontStyle.class); + Assertions.assertEquals("Content", cfAttrs.getRequiredAttribute("fontName", String.class)); + Assertions.assertEquals((short) 11, cfAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); + + // then - field-level annotationMap covers 4 annotations + Head head = property.getHeadMap().get(0); + AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + Assertions.assertNotNull(fieldAnnotationMap); + Assertions.assertEquals(4, fieldAnnotationMap.size()); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ExcelProperty.class)); + Assertions.assertArrayEquals( + new String[] {"Date"}, + fieldAnnotationMap + .getAttributes(ExcelProperty.class) + .getRequiredAttribute("value", String[].class)); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(DateTimeFormat.class)); + Assertions.assertEquals( + "yyyy-MM-dd", + fieldAnnotationMap.getAttributes(DateTimeFormat.class).getRequiredAttribute("value", String.class)); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(NumberFormat.class)); + Assertions.assertEquals( + "#,##0.00", + fieldAnnotationMap.getAttributes(NumberFormat.class).getRequiredAttribute("value", String.class)); + + Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ContentLoopMerge.class)); + AnnotationAttributes clmAttrs = fieldAnnotationMap.getAttributes(ContentLoopMerge.class); + Assertions.assertEquals(2, clmAttrs.getRequiredAttribute("eachRow", Integer.class)); + Assertions.assertEquals(3, clmAttrs.getRequiredAttribute("columnExtend", Integer.class)); + } + + @Test + void shouldPopulateEachFieldIndependently_whenMixedClassAndFieldAnnotations() { + // given - class has @ColumnWidth(20) and @HeadStyle(fillForegroundColor=10) + // field 0 has @ExcelProperty("Amount") + @NumberFormat("#,##0.00") + // field 1 has @ExcelProperty("Date") + @DateTimeFormat("yyyy-MM-dd") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedClassStyleAndFieldFormat.class, null); + + // then - class-level annotationMap + AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + Assertions.assertNotNull(classAnnotationMap); + Assertions.assertEquals(2, classAnnotationMap.size()); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); + Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadStyle.class)); + + // then - each field has its own independent annotationMap + Assertions.assertEquals(2, property.getHeadMap().size()); + + Head amountHead = property.getHeadMap().get(0); + AnnotationMap amountMap = amountHead.getAnnotationMap(); + Assertions.assertNotNull(amountMap); + Assertions.assertEquals(2, amountMap.size()); + Assertions.assertTrue(amountMap.hasAnnotation(ExcelProperty.class)); + Assertions.assertTrue(amountMap.hasAnnotation(NumberFormat.class)); + Assertions.assertEquals( + "#,##0.00", + amountMap.getAttributes(NumberFormat.class).getRequiredAttribute("value", String.class)); + + Head dateHead = property.getHeadMap().get(1); + AnnotationMap dateMap = dateHead.getAnnotationMap(); + Assertions.assertNotNull(dateMap); + Assertions.assertEquals(2, dateMap.size()); + Assertions.assertTrue(dateMap.hasAnnotation(ExcelProperty.class)); + Assertions.assertTrue(dateMap.hasAnnotation(DateTimeFormat.class)); + Assertions.assertEquals( + "yyyy-MM-dd", + dateMap.getAttributes(DateTimeFormat.class).getRequiredAttribute("value", String.class)); + } + } +} From 32c30b399fe0c6192dd15742b963295bd80c04a8 Mon Sep 17 00:00:00 2001 From: Bengbengbalabalabeng Date: Tue, 19 May 2026 16:38:51 +0800 Subject: [PATCH 2/3] feat: add composable annotation support (task-2). Task-2: - Introduced `AnnotatedTypeDescriptor` and `AnnotatedFieldDescriptor` abstract models to carry metadata parsing logic at the class and field dimensions, respectively. - Encapsulated the `AnnotatedClassUtils`, rewriting part of the `ClassUtils` logic based on `AnnotatedElementDescriptor`. - Introduced the `enableMetaMarked` configuration flag to ensure read/write stability when handling legacy ClassUtils logic (such as `declaredExcelContentProperty` and `declaredFields`). --- .../sheet/analysis/ExcelAnalyserImpl.java | 2 + .../AbstractAnnotatedElementDescriptor.java | 79 + .../fesod/sheet/annotation/AliasFor.java | 15 + .../AnnotatedElementDescriptor.java | 56 + .../annotation/AnnotatedFieldDescriptor.java | 41 + .../annotation/AnnotatedTypeDescriptor.java | 32 + .../annotation/AnnotationAttributes.java | 4 + .../annotation/AnnotationMetadataReader.java | 11 +- .../DefaultAnnotationMetadataResolver.java | 13 +- .../HierarchicalAnnotationScanner.java | 12 +- .../fesod/sheet/context/WriteContextImpl.java | 24 +- .../fesod/sheet/metadata/AbstractHolder.java | 9 + .../metadata/AbstractParameterBuilder.java | 9 + .../fesod/sheet/metadata/BasicParameter.java | 5 + .../fesod/sheet/metadata/CachedFields.java | 48 + .../fesod/sheet/metadata/FieldCache.java | 3 +- .../fesod/sheet/metadata/FieldWrapper.java | 4 +- .../sheet/metadata/GlobalConfiguration.java | 7 + .../org/apache/fesod/sheet/metadata/Head.java | 58 +- .../property/ColumnWidthProperty.java | 21 + .../property/DateTimeFormatProperty.java | 22 + .../metadata/property/ExcelHeadProperty.java | 102 +- .../sheet/metadata/property/FontProperty.java | 56 + .../metadata/property/LoopMergeProperty.java | 19 + .../property/NumberFormatProperty.java | 20 + .../property/OnceAbsoluteMergeProperty.java | 22 + .../metadata/property/RowHeightProperty.java | 28 + .../metadata/property/StyleProperty.java | 94 ++ .../listener/ModelBuildEventListener.java | 23 +- .../fesod/sheet/util/AnnotatedClassUtils.java | 606 ++++++++ .../write/executor/ExcelWriteAddExecutor.java | 87 +- .../property/ExcelWriteHeadProperty.java | 24 +- .../AnnotatedFieldDescriptorTest.java | 250 ++++ .../AnnotatedTypeDescriptorTest.java | 235 +++ .../AnnotationMetadataReaderTest.java | 216 +++ .../composite/CompositeAnnotationTest.java | 221 ++- .../composite/DirectAnnotationTest.java | 219 ++- .../composite/IntegrationAnnotations.java | 331 +++++ .../IntegrationCompositeAnnotationTest.java | 721 +++++++++ .../composite/IntegrationExcelDatas.java | 1298 +++++++++++++++++ .../annotation/composite/WorkbookAsserts.java | 217 +++ .../fesod/sheet/cache/CacheDataTest.java | 11 +- 42 files changed, 5120 insertions(+), 155 deletions(-) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AbstractAnnotatedElementDescriptor.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedElementDescriptor.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptor.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptor.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/CachedFields.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/util/AnnotatedClassUtils.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptorTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptorTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationAnnotations.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationExcelDatas.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java index 282f876b2..a6fe0affb 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java @@ -49,6 +49,7 @@ import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.xlsx.XlsxReadWorkbookHolder; import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.fesod.sheet.util.AnnotatedClassUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.DateUtils; import org.apache.fesod.sheet.util.FileUtils; @@ -298,6 +299,7 @@ private void removeThreadLocalCache() { NumberDataFormatterUtils.removeThreadLocalCache(); DateUtils.removeThreadLocalCache(); ClassUtils.removeThreadLocalCache(); + AnnotatedClassUtils.removeThreadLocalCache(); } /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AbstractAnnotatedElementDescriptor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AbstractAnnotatedElementDescriptor.java new file mode 100644 index 000000000..2022ea6df --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AbstractAnnotatedElementDescriptor.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Objects; + +/** + * Descriptor abstract base class, providing generic annotation extraction logic. + */ +public abstract class AbstractAnnotatedElementDescriptor + implements AnnotatedElementDescriptor { + + protected final E annotatedElement; + protected final AnnotationMap annotationMap; + + protected AbstractAnnotatedElementDescriptor(E annotatedElement, AnnotationMap annotationMap) { + this.annotatedElement = annotatedElement; + this.annotationMap = annotationMap; + } + + /** + * Get the original annotated element. + */ + @Override + public E getAnnotatedElement() { + return annotatedElement; + } + + /** + * Get a wrapper for all annotation (include composable annotation) attribute key-value pairs. + */ + @Override + public AnnotationMap getAnnotationMap() { + return annotationMap; + } + + /** + * Determine whether the specified annotation exists on this element. + */ + @Override + public boolean hasAnnotation(Class type) { + return Objects.nonNull(annotationMap) && annotationMap.hasAnnotation(type); + } + + /** + * Get the number of annotations. + */ + @Override + public int getAnnotationCount() { + return Objects.nonNull(annotationMap) ? annotationMap.size() : 0; + } + + /** + * Get the attributes of a specified annotation. + */ + @Override + public AnnotationAttributes getAnnotation(Class type) { + return Objects.nonNull(annotationMap) ? annotationMap.getAttributes(type) : null; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java index 6a93873e6..df82d5bcb 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java @@ -38,6 +38,11 @@ public class AliasFor { */ private final Class target; + /** + * The name of the attribute in the source annotation. + */ + private final String customAttribute; + /** * The name of the attribute in the target annotation to be overridden. */ @@ -50,8 +55,18 @@ public class AliasFor { public AliasFor( Class marked, Class target, String attribute, Object value) { + this(marked, target, attribute, attribute, value); + } + + public AliasFor( + Class marked, + Class target, + String customAttribute, + String attribute, + Object value) { this.marked = marked; this.target = target; + this.customAttribute = customAttribute; this.attribute = attribute; this.value = value; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedElementDescriptor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedElementDescriptor.java new file mode 100644 index 000000000..de7b2eb2a --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedElementDescriptor.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +/** + * A generic interface for describing annotation elements. + * + * @param The specific type of annotated element, such as {@code Class}, {@code Field}. + */ +public interface AnnotatedElementDescriptor { + + /** + * Get the original annotated element. + */ + E getAnnotatedElement(); + + /** + * Get a wrapper for all annotation (include composable annotation) attribute key-value pairs. + */ + AnnotationMap getAnnotationMap(); + + /** + * Determine whether the specified annotation exists on this element. + */ + boolean hasAnnotation(Class type); + + /** + * Get the number of annotations. + */ + int getAnnotationCount(); + + /** + * Get the attributes of a specified annotation. + */ + AnnotationAttributes getAnnotation(Class type); +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptor.java new file mode 100644 index 000000000..04bcd2e72 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptor.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.reflect.Field; +import lombok.Getter; +import org.apache.commons.lang3.Validate; + +/** + * A field-level annotation descriptor. + */ +public class AnnotatedFieldDescriptor extends AbstractAnnotatedElementDescriptor { + + /** + * The field name matching cglib + */ + @Getter + private final String fieldName; + + public AnnotatedFieldDescriptor(Field annotatedElement, String fieldName, AnnotationMap annotationMap) { + super(Validate.notNull(annotatedElement, "Field must not be null"), annotationMap); + this.fieldName = Validate.notBlank(fieldName, "Field name must not be blank"); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptor.java new file mode 100644 index 000000000..0178b1191 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptor.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +/** + * A class-level annotation descriptor. + */ +public class AnnotatedTypeDescriptor extends AbstractAnnotatedElementDescriptor> { + + public static final AnnotatedTypeDescriptor EMPTY = new AnnotatedTypeDescriptor(null, null); + + public AnnotatedTypeDescriptor(Class annotatedElement, AnnotationMap annotationMap) { + super(annotatedElement, annotationMap); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java index 7d93d39c9..a1e840181 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java @@ -55,6 +55,10 @@ public AnnotationAttributes(Class annotationType, Map annotationType) { + return this.annotationType.equals(annotationType); + } + @SuppressWarnings("unchecked") public T getRequiredAttribute(String attrName, Class type) { Validate.notBlank(attrName, "attributeName must not be null or blank"); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java index d4598a60b..a11ae4794 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java @@ -31,15 +31,22 @@ public class AnnotationMetadataReader extends HierarchicalAnnotationScanner { private final Map elementAnnotation; public AnnotationMetadataReader() { + this(Boolean.TRUE); + } + + public AnnotationMetadataReader(Boolean enableMetaMarked) { this( new DefaultAnnotationMetadataResolver(), + enableMetaMarked, ConcurrentReferenceHashMap.builder() .get()); } public AnnotationMetadataReader( - AnnotationMetadataResolver resolver, Map elementAnnotation) { - super(resolver); + AnnotationMetadataResolver resolver, + Boolean enableMetaMarked, + Map elementAnnotation) { + super(resolver, enableMetaMarked); this.elementAnnotation = elementAnnotation; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java index d009ba4a7..d28275a65 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.java @@ -162,8 +162,17 @@ public AnnotationMetadata resolve(Annotation ann) { ann.annotationType().getName())); } - aliases.add(new AliasFor( - ann.annotationType(), aliasFor.annotation(), aliasFor.attribute(), result)); + if (method.getName().equals(aliasFor.attribute())) { + aliases.add(new AliasFor( + ann.annotationType(), aliasFor.annotation(), aliasFor.attribute(), result)); + } else { + aliases.add(new AliasFor( + ann.annotationType(), + aliasFor.annotation(), + method.getName(), + aliasFor.attribute(), + result)); + } } return result; } catch (IllegalAccessException | InvocationTargetException ex) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java index 8626ad9ca..4677e5be1 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java @@ -37,9 +37,11 @@ public abstract class HierarchicalAnnotationScanner { protected final AnnotationMetadataResolver metadataResolver; + protected final Boolean enableMetaMarked; - protected HierarchicalAnnotationScanner(AnnotationMetadataResolver metadataResolver) { + protected HierarchicalAnnotationScanner(AnnotationMetadataResolver metadataResolver, Boolean enableMetaMarked) { this.metadataResolver = metadataResolver; + this.enableMetaMarked = enableMetaMarked; } protected AnnotationMap scan(AnnotatedElement element) { @@ -76,7 +78,7 @@ protected AnnotationMap scan(AnnotatedElement element) { } // Handle composable-annotations (low-level attribute value) - if (metadataResolver.isMetaMarked(ann)) { + if (isMetaMarkedEnabled() && metadataResolver.isMetaMarked(ann)) { AnnotationMetadata metadata = metadataResolver.resolve(ann); metadata.addTo(aliases); metadata.setDistance(distance); @@ -103,6 +105,10 @@ protected AnnotationMap scan(AnnotatedElement element) { return annotationMap; } + private boolean isMetaMarkedEnabled() { + return Boolean.TRUE.equals(enableMetaMarked); + } + /** * Handle the mapping and overriding logic of annotation attribute aliases (AliasFor). *

@@ -136,7 +142,7 @@ private void synthesize(AnnotationMap annotationMap, List aliases) { continue; } if ((marked.getDistance() + 1) <= target.getDistance()) { - target.put(alias.getAttribute(), marked.get(alias.getAttribute())); + target.put(alias.getAttribute(), marked.get(alias.getCustomAttribute())); } } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 5a1768606..d1e6c62a7 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -41,6 +41,7 @@ import org.apache.fesod.sheet.metadata.data.WriteCellData; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.fesod.sheet.util.AnnotatedClassUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.DateUtils; import org.apache.fesod.sheet.util.FileUtils; @@ -357,11 +358,23 @@ private void addOneRowOfHeadDataToExcel( for (Map.Entry entry : headMap.entrySet()) { Head head = entry.getValue(); int columnIndex = entry.getKey(); - ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - null, - currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), - head.getFieldName(), - currentWriteHolder); + + ExcelContentProperty excelContentProperty; + // Supports composable annotation processing (new-beta) and + // real-time class analysis (old-stable) to ensure compatibility + if (Boolean.TRUE.equals(currentWriteHolder.globalConfiguration().getEnableMetaMarked())) { + excelContentProperty = AnnotatedClassUtils.declaredExcelContentProperty( + null, + currentWriteHolder.excelWriteHeadProperty().getTypeDescriptor(), + head.getFieldDescriptor(), + currentWriteHolder); + } else { + excelContentProperty = ClassUtils.declaredExcelContentProperty( + null, + currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), + head.getFieldName(), + currentWriteHolder); + } CellWriteHandlerContext cellWriteHandlerContext = WriteHandlerUtils.createCellWriteHandlerContext( this, row, rowIndex, head, columnIndex, relativeRowIndex, Boolean.TRUE, excelContentProperty); @@ -563,6 +576,7 @@ private void removeThreadLocalCache() { NumberDataFormatterUtils.removeThreadLocalCache(); DateUtils.removeThreadLocalCache(); ClassUtils.removeThreadLocalCache(); + AnnotatedClassUtils.removeThreadLocalCache(); } /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java index 35b63aff4..57c038511 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java @@ -99,6 +99,15 @@ public AbstractHolder(BasicParameter basicParameter, AbstractHolder prentAbstrac globalConfiguration.setAutoStrip(basicParameter.getAutoStrip()); } + if (basicParameter.getEnableMetaMarked() == null) { + if (prentAbstractHolder != null) { + globalConfiguration.setEnableMetaMarked( + prentAbstractHolder.getGlobalConfiguration().getEnableMetaMarked()); + } + } else { + globalConfiguration.setEnableMetaMarked(basicParameter.getEnableMetaMarked()); + } + if (basicParameter.getUse1904windowing() == null) { if (prentAbstractHolder != null) { globalConfiguration.setUse1904windowing( diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java index e2966ea5d..c00f43ddf 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java @@ -156,6 +156,15 @@ public T autoStrip(Boolean autoStrip) { return self(); } + /** + * Whether to enable fully composable annotations support. Only effective when either {@link BasicParameter#head} or + * {@link BasicParameter#clazz} is set. Default is false. + */ + public T enableMetaMarked(Boolean enableMetaMarked) { + parameter().setEnableMetaMarked(enableMetaMarked); + return self(); + } + @SuppressWarnings("unchecked") protected T self() { return (T) this; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java index 19a2bece0..881e37ffb 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java @@ -89,4 +89,9 @@ public class BasicParameter { * Automatic strip includes sheet name and content */ private Boolean autoStrip; + /** + * Whether to enable fully composable annotations support. Only effective when either {@link BasicParameter#head} or + * {@link BasicParameter#clazz} is set. Default is false. + */ + private Boolean enableMetaMarked; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/CachedFields.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/CachedFields.java new file mode 100644 index 000000000..505d384fb --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/CachedFields.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.metadata; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; + +/** + * field cache + */ +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +public class CachedFields { + + /** + * A field cache that has been sorted by a class. + * It will exclude fields that are not needed. + */ + private Map sortedFieldMap; + + /** + * Fields using the index attribute + */ + private Map indexFieldMap; +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldCache.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldCache.java index 3bf87fc78..80b9d41b0 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldCache.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldCache.java @@ -34,8 +34,9 @@ /** * filed cache * - * + * @see CachedFields */ +@Deprecated @Getter @Setter @EqualsAndHashCode diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldWrapper.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldWrapper.java index cfbd267d5..663b668b9 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldWrapper.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/FieldWrapper.java @@ -31,13 +31,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; import org.apache.fesod.sheet.annotation.ExcelProperty; /** * filed wrapper * - * + * @see AnnotatedFieldDescriptor */ +@Deprecated @Getter @Setter @EqualsAndHashCode diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java index 6a894dc57..f57cca612 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java @@ -77,9 +77,16 @@ public class GlobalConfiguration { */ private Boolean autoStrip; + /** + * Whether to enable fully composable annotations support. Only effective when either {@link BasicParameter#head} or + * {@link BasicParameter#clazz} is set. Default is false. + */ + private Boolean enableMetaMarked; + public GlobalConfiguration() { this.autoTrim = Boolean.TRUE; this.autoStrip = Boolean.FALSE; + this.enableMetaMarked = Boolean.FALSE; this.use1904windowing = Boolean.FALSE; this.locale = Locale.getDefault(); this.useScientificFormat = Boolean.FALSE; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java index dc54c9307..0eecbddc2 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/Head.java @@ -25,16 +25,18 @@ package org.apache.fesod.sheet.metadata; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.AnnotationMap; import org.apache.fesod.sheet.exception.ExcelGenerateException; import org.apache.fesod.sheet.metadata.property.ColumnWidthProperty; -import org.apache.fesod.sheet.metadata.property.ExcelHeadProperty; import org.apache.fesod.sheet.metadata.property.FontProperty; import org.apache.fesod.sheet.metadata.property.LoopMergeProperty; import org.apache.fesod.sheet.metadata.property.StyleProperty; @@ -55,11 +57,7 @@ public class Head { /** * It only has values when passed in {@link Sheet#setClazz(Class)} and {@link Table#setClazz(Class)} */ - private Field field; - /** - * It only has values when passed in {@link Sheet#setClazz(Class)} and {@link Table#setClazz(Class)} - */ - private String fieldName; + private AnnotatedFieldDescriptor fieldDescriptor; /** * Head name */ @@ -91,11 +89,6 @@ public class Head { */ private FontProperty headFontProperty; - /** - * Custom class field-level annotation attribute key-value pairs. (Used in conjunction with {@link ExcelHeadProperty#headClazz}) - */ - private AnnotationMap annotationMap; - public Head( Integer columnIndex, Field field, @@ -104,9 +97,22 @@ public Head( AnnotationMap annotationMap, Boolean forceIndex, Boolean forceName) { + this( + columnIndex, + new AnnotatedFieldDescriptor(field, fieldName, annotationMap), + headNameList, + forceIndex, + forceName); + } + + public Head( + Integer columnIndex, + AnnotatedFieldDescriptor fieldDescriptor, + List headNameList, + Boolean forceIndex, + Boolean forceName) { this.columnIndex = columnIndex; - this.field = field; - this.fieldName = fieldName; + this.fieldDescriptor = fieldDescriptor; if (headNameList == null) { this.headNameList = new ArrayList<>(); } else { @@ -117,8 +123,32 @@ public Head( } } } - this.annotationMap = annotationMap; this.forceIndex = forceIndex; this.forceName = forceName; } + + public AnnotationAttributes findAnnotation(Class type) { + if (fieldDescriptor != null && fieldDescriptor.hasAnnotation(type)) { + return fieldDescriptor.getAnnotation(type); + } + return null; + } + + public boolean hasFieldDescriptor() { + return fieldDescriptor != null; + } + + public Field getField() { + if (fieldDescriptor != null) { + return fieldDescriptor.getAnnotatedElement(); + } + return null; + } + + public String getFieldName() { + if (fieldDescriptor != null) { + return fieldDescriptor.getFieldName(); + } + return null; + } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ColumnWidthProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ColumnWidthProperty.java index 58f4f845f..9187d5c11 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ColumnWidthProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ColumnWidthProperty.java @@ -25,6 +25,7 @@ package org.apache.fesod.sheet.metadata.property; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; /** @@ -39,6 +40,26 @@ public ColumnWidthProperty(Integer width) { this.width = width; } + public static ColumnWidthProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(ColumnWidth.class)) { + throw new IllegalArgumentException(String.format( + "ColumnWidthProperty only support ColumnWidth annotation" + ", but currently provides '%s'", + attributes.getAnnotationType())); + } + Integer columnWidth = attributes.getRequiredAttribute("value", Integer.class); + if (columnWidth < 0) { + return null; + } + return new ColumnWidthProperty(columnWidth); + } + + /** + * @see ColumnWidthProperty#build(AnnotationAttributes) + */ + @Deprecated public static ColumnWidthProperty build(ColumnWidth columnWidth) { if (columnWidth == null || columnWidth.value() < 0) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/DateTimeFormatProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/DateTimeFormatProperty.java index 4e2ad8ed7..1ed1256ef 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/DateTimeFormatProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/DateTimeFormatProperty.java @@ -29,7 +29,9 @@ import lombok.Getter; import lombok.Setter; import org.apache.fesod.common.util.BooleanUtils; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.enums.BooleanEnum; /** * Configuration from annotations @@ -48,6 +50,26 @@ public DateTimeFormatProperty(String format, Boolean use1904windowing) { this.use1904windowing = use1904windowing; } + public static DateTimeFormatProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(DateTimeFormat.class)) { + throw new IllegalArgumentException(String.format( + "DateTimeFormatProperty only support DateTimeFormat annotation" + ", but currently provides '%s'", + attributes.getAnnotationType())); + } + return new DateTimeFormatProperty( + attributes.getRequiredAttribute("value", String.class), + BooleanUtils.isTrue(attributes + .getRequiredAttribute("use1904windowing", BooleanEnum.class) + .getBooleanValue())); + } + + /** + * @see DateTimeFormatProperty#build(AnnotationAttributes) + */ + @Deprecated public static DateTimeFormatProperty build(DateTimeFormat dateTimeFormat) { if (dateTimeFormat == null) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java index 90e9b4928..b2795c827 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java @@ -25,6 +25,7 @@ package org.apache.fesod.sheet.metadata.property; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,14 +36,16 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.fesod.common.util.StringUtils; -import org.apache.fesod.sheet.annotation.AnnotationMap; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; +import org.apache.fesod.sheet.annotation.AnnotatedTypeDescriptor; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.AnnotationMetadataReader; +import org.apache.fesod.sheet.annotation.ExcelProperty; import org.apache.fesod.sheet.enums.HeadKindEnum; +import org.apache.fesod.sheet.metadata.CachedFields; import org.apache.fesod.sheet.metadata.ConfigurationHolder; -import org.apache.fesod.sheet.metadata.FieldCache; -import org.apache.fesod.sheet.metadata.FieldWrapper; import org.apache.fesod.sheet.metadata.Head; -import org.apache.fesod.sheet.util.ClassUtils; +import org.apache.fesod.sheet.util.AnnotatedClassUtils; import org.apache.fesod.sheet.write.metadata.holder.AbstractWriteHolder; /** @@ -55,19 +58,15 @@ @Slf4j public class ExcelHeadProperty { - /** - * Custom class - */ - private Class headClazz; /** * The types of head */ private HeadKindEnum headKind; /** - * Custom class-level annotation attribute key-value pairs. (Used in conjunction with {@link ExcelHeadProperty#headClazz}) + * Custom class descriptor */ - private AnnotationMap headClazzAnnotationMap; + private AnnotatedTypeDescriptor typeDescriptor; /** * The number of rows in the line with the most rows */ @@ -77,10 +76,11 @@ public class ExcelHeadProperty { */ private Map headMap; - private AnnotationMetadataReader metadataReader = new AnnotationMetadataReader(); + private AnnotationMetadataReader metadataReader; public ExcelHeadProperty(ConfigurationHolder configurationHolder, Class headClazz, List> head) { - this.headClazz = headClazz; + metadataReader = new AnnotationMetadataReader( + configurationHolder.globalConfiguration().getEnableMetaMarked()); headMap = new TreeMap<>(); headKind = HeadKindEnum.NONE; headRowNumber = 0; @@ -92,22 +92,13 @@ public ExcelHeadProperty(ConfigurationHolder configurationHolder, Class headC continue; } } - headMap.put( - headIndex, - new Head( - headIndex, - null, - null, - head.get(i), - AnnotationMap.builder().build(), - Boolean.FALSE, - Boolean.TRUE)); + headMap.put(headIndex, new Head(headIndex, null, head.get(i), Boolean.FALSE, Boolean.TRUE)); headIndex++; } headKind = HeadKindEnum.STRING; } // convert headClazz to head - initColumnProperties(configurationHolder); + initColumnProperties(headClazz, configurationHolder); initHeadRowNumber(); if (log.isDebugEnabled()) { @@ -135,19 +126,22 @@ private void initHeadRowNumber() { } } - private void initColumnProperties(ConfigurationHolder configurationHolder) { + private void initColumnProperties(Class headClazz, ConfigurationHolder configurationHolder) { if (headClazz == null) { + this.typeDescriptor = AnnotatedTypeDescriptor.EMPTY; return; } - headClazzAnnotationMap = metadataReader.read(headClazz); - FieldCache fieldCache = ClassUtils.declaredFields(headClazz, configurationHolder); - for (Map.Entry entry : - fieldCache.getSortedFieldMap().entrySet()) { + this.typeDescriptor = new AnnotatedTypeDescriptor(headClazz, metadataReader.read(headClazz)); + CachedFields cachedFields = + AnnotatedClassUtils.declaredFields(headClazz, metadataReader::read, configurationHolder); + + for (Map.Entry entry : + cachedFields.getSortedFieldMap().entrySet()) { initOneColumnProperty( entry.getKey(), entry.getValue(), - fieldCache.getIndexFieldMap().containsKey(entry.getKey())); + cachedFields.getIndexFieldMap().containsKey(entry.getKey())); } headKind = HeadKindEnum.CLASS; } @@ -156,37 +150,55 @@ private void initColumnProperties(ConfigurationHolder configurationHolder) { * Initialization column property * * @param index - * @param field + * @param fieldDescriptor * @param forceIndex * @return Ignore current field */ - private void initOneColumnProperty(int index, FieldWrapper field, Boolean forceIndex) { + private void initOneColumnProperty(int index, AnnotatedFieldDescriptor fieldDescriptor, Boolean forceIndex) { List tmpHeadList = new ArrayList<>(); - boolean notForceName = field.getHeads() == null - || field.getHeads().length == 0 - || (field.getHeads().length == 1 && StringUtils.isEmpty(field.getHeads()[0])); + String[] heads = getHeads(fieldDescriptor); + boolean notForceName = heads.length == 0 || (heads.length == 1 && StringUtils.isEmpty(heads[0])); + if (headMap.containsKey(index)) { tmpHeadList.addAll(headMap.get(index).getHeadNameList()); } else { if (notForceName) { - tmpHeadList.add(field.getFieldName()); + tmpHeadList.add(fieldDescriptor.getFieldName()); } else { - Collections.addAll(tmpHeadList, field.getHeads()); + Collections.addAll(tmpHeadList, heads); } } - AnnotationMap fieldAnnotationMap = metadataReader.read(field.getField()); - Head head = new Head( - index, - field.getField(), - field.getFieldName(), - tmpHeadList, - fieldAnnotationMap, - forceIndex, - !notForceName); + + Head head = new Head(index, fieldDescriptor, tmpHeadList, forceIndex, !notForceName); headMap.put(index, head); } + private static String[] getHeads(AnnotatedFieldDescriptor fieldDescriptor) { + if (fieldDescriptor.getAnnotationCount() == 0) { + return new String[0]; + } + if (fieldDescriptor.hasAnnotation(ExcelProperty.class)) { + AnnotationAttributes attrs = fieldDescriptor.getAnnotation(ExcelProperty.class); + return attrs.getRequiredAttribute("value", String[].class); + } + return new String[0]; + } + public boolean hasHead() { return headKind != HeadKindEnum.NONE; } + + public AnnotationAttributes findClazzAnnotation(Class clazz) { + if (HeadKindEnum.CLASS.equals(headKind)) { + return typeDescriptor.getAnnotation(clazz); + } + return null; + } + + public Class getHeadClazz() { + if (HeadKindEnum.CLASS.equals(headKind)) { + return typeDescriptor.getAnnotatedElement(); + } + return null; + } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/FontProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/FontProperty.java index caee2655b..70fd2c5e4 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/FontProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/FontProperty.java @@ -29,8 +29,10 @@ import lombok.Getter; import lombok.Setter; import org.apache.fesod.common.util.StringUtils; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.enums.BooleanEnum; import org.apache.poi.common.usermodel.fonts.FontCharset; import org.apache.poi.hssf.usermodel.HSSFPalette; import org.apache.poi.ss.usermodel.Font; @@ -102,6 +104,56 @@ public class FontProperty { */ private Boolean bold; + public static FontProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(HeadFontStyle.class) + && !attributes.isAnnotationTypeEqual(ContentFontStyle.class)) { + throw new IllegalArgumentException(String.format( + "FontProperty only supports HeadFontStyle, ContentFontStyle annotations" + + ", but currently provides '%s'", + attributes.getAnnotationType())); + } + + FontProperty fontProperty = new FontProperty(); + String fontName = attributes.getRequiredAttribute("fontName", String.class); + if (StringUtils.isNotBlank(fontName)) { + fontProperty.setFontName(fontName); + } + Short fontHeightInPoints = attributes.getRequiredAttribute("fontHeightInPoints", Short.class); + if (fontHeightInPoints >= 0) { + fontProperty.setFontHeightInPoints(fontHeightInPoints); + } + BooleanEnum italic = attributes.getRequiredAttribute("italic", BooleanEnum.class); + fontProperty.setItalic(italic.getBooleanValue()); + BooleanEnum strikeout = attributes.getRequiredAttribute("strikeout", BooleanEnum.class); + fontProperty.setStrikeout(strikeout.getBooleanValue()); + Short color = attributes.getRequiredAttribute("color", Short.class); + if (color >= 0) { + fontProperty.setColor(color); + } + Short typeOffset = attributes.getRequiredAttribute("typeOffset", Short.class); + if (typeOffset >= 0) { + fontProperty.setTypeOffset(typeOffset); + } + Byte underline = attributes.getRequiredAttribute("underline", Byte.class); + if (underline >= 0) { + fontProperty.setUnderline(underline); + } + Integer charset = attributes.getRequiredAttribute("charset", Integer.class); + if (charset >= 0) { + fontProperty.setCharset(charset); + } + BooleanEnum bold = attributes.getRequiredAttribute("bold", BooleanEnum.class); + fontProperty.setBold(bold.getBooleanValue()); + return fontProperty; + } + + /** + * @see FontProperty#build(AnnotationAttributes) + */ + @Deprecated public static FontProperty build(HeadFontStyle headFontStyle) { if (headFontStyle == null) { return null; @@ -131,6 +183,10 @@ public static FontProperty build(HeadFontStyle headFontStyle) { return styleProperty; } + /** + * @see FontProperty#build(AnnotationAttributes) + */ + @Deprecated public static FontProperty build(ContentFontStyle contentFontStyle) { if (contentFontStyle == null) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/LoopMergeProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/LoopMergeProperty.java index 1bcdc0b77..19ff9fee2 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/LoopMergeProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/LoopMergeProperty.java @@ -25,6 +25,7 @@ package org.apache.fesod.sheet.metadata.property; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; /** @@ -47,6 +48,24 @@ public LoopMergeProperty(int eachRow, int columnExtend) { this.columnExtend = columnExtend; } + public static LoopMergeProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(ContentLoopMerge.class)) { + throw new IllegalArgumentException(String.format( + "LoopMergeProperty only support ContentLoopMerge annotation" + ", but currently provides '%s'", + attributes.getAnnotationType())); + } + Integer eachRow = attributes.getRequiredAttribute("eachRow", Integer.class); + Integer columnExtend = attributes.getRequiredAttribute("columnExtend", Integer.class); + return new LoopMergeProperty(eachRow, columnExtend); + } + + /** + * @see LoopMergeProperty#build(AnnotationAttributes) + */ + @Deprecated public static LoopMergeProperty build(ContentLoopMerge contentLoopMerge) { if (contentLoopMerge == null) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/NumberFormatProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/NumberFormatProperty.java index ec7ae1823..b2caa0a7e 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/NumberFormatProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/NumberFormatProperty.java @@ -26,6 +26,7 @@ package org.apache.fesod.sheet.metadata.property; import java.math.RoundingMode; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.format.NumberFormat; /** @@ -42,6 +43,25 @@ public NumberFormatProperty(String format, RoundingMode roundingMode) { this.roundingMode = roundingMode; } + public static NumberFormatProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(NumberFormat.class)) { + throw new IllegalArgumentException(String.format( + "NumberFormatProperty only support NumberFormat annotation" + ", but currently provides '%s'", + attributes.getAnnotationType())); + } + + return new NumberFormatProperty( + attributes.getRequiredAttribute("value", String.class), + attributes.getRequiredAttribute("roundingMode", RoundingMode.class)); + } + + /** + * @see NumberFormatProperty#build(AnnotationAttributes) + */ + @Deprecated public static NumberFormatProperty build(NumberFormat numberFormat) { if (numberFormat == null) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/OnceAbsoluteMergeProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/OnceAbsoluteMergeProperty.java index ab4214d35..c6865c2d5 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/OnceAbsoluteMergeProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/OnceAbsoluteMergeProperty.java @@ -25,6 +25,7 @@ package org.apache.fesod.sheet.metadata.property; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; /** @@ -57,6 +58,27 @@ public OnceAbsoluteMergeProperty(int firstRowIndex, int lastRowIndex, int firstC this.lastColumnIndex = lastColumnIndex; } + public static OnceAbsoluteMergeProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(OnceAbsoluteMerge.class)) { + throw new IllegalArgumentException(String.format( + "OnceAbsoluteMergeProperty only support OnceAbsoluteMerge annotation" + + ", but currently provides '%s'", + attributes.getAnnotationType())); + } + return new OnceAbsoluteMergeProperty( + attributes.getRequiredAttribute("firstRowIndex", Integer.class), + attributes.getRequiredAttribute("lastRowIndex", Integer.class), + attributes.getRequiredAttribute("firstColumnIndex", Integer.class), + attributes.getRequiredAttribute("lastColumnIndex", Integer.class)); + } + + /** + * @see OnceAbsoluteMergeProperty#build(AnnotationAttributes) + */ + @Deprecated public static OnceAbsoluteMergeProperty build(OnceAbsoluteMerge onceAbsoluteMerge) { if (onceAbsoluteMerge == null) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/RowHeightProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/RowHeightProperty.java index 5dbafa5b4..658f380b9 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/RowHeightProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/RowHeightProperty.java @@ -25,6 +25,7 @@ package org.apache.fesod.sheet.metadata.property; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; @@ -40,6 +41,29 @@ public RowHeightProperty(Short height) { this.height = height; } + public static RowHeightProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(HeadRowHeight.class) + && !attributes.isAnnotationTypeEqual(ContentRowHeight.class)) { + throw new IllegalArgumentException(String.format( + "RowHeightProperty only supports HeadRowHeight, ContentRowHeight" + + " annotations, but currently provides '%s'", + attributes.getAnnotationType())); + } + + Short rowHeight = attributes.getRequiredAttribute("value", Short.class); + if (rowHeight < 0) { + return null; + } + return new RowHeightProperty(rowHeight); + } + + /** + * @see RowHeightProperty#build(AnnotationAttributes) + */ + @Deprecated public static RowHeightProperty build(HeadRowHeight headRowHeight) { if (headRowHeight == null || headRowHeight.value() < 0) { return null; @@ -47,6 +71,10 @@ public static RowHeightProperty build(HeadRowHeight headRowHeight) { return new RowHeightProperty(headRowHeight.value()); } + /** + * @see RowHeightProperty#build(AnnotationAttributes) + */ + @Deprecated public static RowHeightProperty build(ContentRowHeight contentRowHeight) { if (contentRowHeight == null || contentRowHeight.value() < 0) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/StyleProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/StyleProperty.java index 7c1e8d637..e12bf92f4 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/StyleProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/StyleProperty.java @@ -28,8 +28,14 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.ContentStyle; import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.enums.BooleanEnum; +import org.apache.fesod.sheet.enums.poi.BorderStyleEnum; +import org.apache.fesod.sheet.enums.poi.FillPatternTypeEnum; +import org.apache.fesod.sheet.enums.poi.HorizontalAlignmentEnum; +import org.apache.fesod.sheet.enums.poi.VerticalAlignmentEnum; import org.apache.fesod.sheet.metadata.data.DataFormatData; import org.apache.fesod.sheet.write.metadata.style.WriteFont; import org.apache.poi.ss.usermodel.BorderStyle; @@ -166,6 +172,90 @@ public class StyleProperty { */ private Boolean shrinkToFit; + public static StyleProperty build(AnnotationAttributes attributes) { + if (attributes == null) { + return null; + } + if (!attributes.isAnnotationTypeEqual(HeadStyle.class) + && !attributes.isAnnotationTypeEqual(ContentStyle.class)) { + throw new IllegalArgumentException(String.format( + "StyleProperty only supports HeadStyle, ContentStyle annotations, but currently provides '%s'", + attributes.getAnnotationType())); + } + + StyleProperty styleProperty = new StyleProperty(); + Short dataFormat = attributes.getRequiredAttribute("dataFormat", Short.class); + if (dataFormat >= 0) { + DataFormatData dataFormatData = new DataFormatData(); + dataFormatData.setIndex(dataFormat); + styleProperty.setDataFormatData(dataFormatData); + } + BooleanEnum hidden = attributes.getRequiredAttribute("hidden", BooleanEnum.class); + styleProperty.setHidden(hidden.getBooleanValue()); + BooleanEnum locked = attributes.getRequiredAttribute("locked", BooleanEnum.class); + styleProperty.setLocked(locked.getBooleanValue()); + BooleanEnum quotePrefix = attributes.getRequiredAttribute("quotePrefix", BooleanEnum.class); + styleProperty.setQuotePrefix(quotePrefix.getBooleanValue()); + HorizontalAlignmentEnum horizontalAlignment = + attributes.getRequiredAttribute("horizontalAlignment", HorizontalAlignmentEnum.class); + styleProperty.setHorizontalAlignment(horizontalAlignment.getPoiHorizontalAlignment()); + BooleanEnum wrapped = attributes.getRequiredAttribute("wrapped", BooleanEnum.class); + styleProperty.setWrapped(wrapped.getBooleanValue()); + VerticalAlignmentEnum verticalAlignment = + attributes.getRequiredAttribute("verticalAlignment", VerticalAlignmentEnum.class); + styleProperty.setVerticalAlignment(verticalAlignment.getPoiVerticalAlignmentEnum()); + Short rotation = attributes.getRequiredAttribute("rotation", Short.class); + if (rotation >= 0) { + styleProperty.setRotation(rotation); + } + Short indent = attributes.getRequiredAttribute("indent", Short.class); + if (indent >= 0) { + styleProperty.setIndent(indent); + } + BorderStyleEnum borderLeft = attributes.getRequiredAttribute("borderLeft", BorderStyleEnum.class); + styleProperty.setBorderLeft(borderLeft.getPoiBorderStyle()); + BorderStyleEnum borderRight = attributes.getRequiredAttribute("borderRight", BorderStyleEnum.class); + styleProperty.setBorderRight(borderRight.getPoiBorderStyle()); + BorderStyleEnum borderTop = attributes.getRequiredAttribute("borderTop", BorderStyleEnum.class); + styleProperty.setBorderTop(borderTop.getPoiBorderStyle()); + BorderStyleEnum borderBottom = attributes.getRequiredAttribute("borderBottom", BorderStyleEnum.class); + styleProperty.setBorderBottom(borderBottom.getPoiBorderStyle()); + Short leftBorderColor = attributes.getRequiredAttribute("leftBorderColor", Short.class); + if (leftBorderColor >= 0) { + styleProperty.setLeftBorderColor(leftBorderColor); + } + Short rightBorderColor = attributes.getRequiredAttribute("rightBorderColor", Short.class); + if (rightBorderColor >= 0) { + styleProperty.setRightBorderColor(rightBorderColor); + } + Short topBorderColor = attributes.getRequiredAttribute("topBorderColor", Short.class); + if (topBorderColor >= 0) { + styleProperty.setTopBorderColor(topBorderColor); + } + Short bottomBorderColor = attributes.getRequiredAttribute("bottomBorderColor", Short.class); + if (bottomBorderColor >= 0) { + styleProperty.setBottomBorderColor(bottomBorderColor); + } + FillPatternTypeEnum fillPatternType = + attributes.getRequiredAttribute("fillPatternType", FillPatternTypeEnum.class); + styleProperty.setFillPatternType(fillPatternType.getPoiFillPatternType()); + Short fillBackgroundColor = attributes.getRequiredAttribute("fillBackgroundColor", Short.class); + if (fillBackgroundColor >= 0) { + styleProperty.setFillBackgroundColor(fillBackgroundColor); + } + Short fillForegroundColor = attributes.getRequiredAttribute("fillForegroundColor", Short.class); + if (fillForegroundColor >= 0) { + styleProperty.setFillForegroundColor(fillForegroundColor); + } + BooleanEnum shrinkToFit = attributes.getRequiredAttribute("shrinkToFit", BooleanEnum.class); + styleProperty.setShrinkToFit(shrinkToFit.getBooleanValue()); + return styleProperty; + } + + /** + * @see StyleProperty#build(AnnotationAttributes) + */ + @Deprecated public static StyleProperty build(HeadStyle headStyle) { if (headStyle == null) { return null; @@ -215,6 +305,10 @@ public static StyleProperty build(HeadStyle headStyle) { return styleProperty; } + /** + * @see StyleProperty#build(AnnotationAttributes) + */ + @Deprecated public static StyleProperty build(ContentStyle contentStyle) { if (contentStyle == null) { return null; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java index 746f4c998..dee676c23 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java @@ -39,8 +39,10 @@ import org.apache.fesod.sheet.metadata.Head; import org.apache.fesod.sheet.metadata.data.DataFormatData; import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.read.metadata.holder.ReadSheetHolder; import org.apache.fesod.sheet.read.metadata.property.ExcelReadHeadProperty; +import org.apache.fesod.sheet.util.AnnotatedClassUtils; import org.apache.fesod.sheet.util.BeanMapUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.ConverterUtils; @@ -185,14 +187,25 @@ private Object buildUserModel( continue; } ReadCellData cellData = cellDataMap.get(index); + + ExcelContentProperty excelContentProperty; + // Supports composable annotation processing (new-beta) and + // real-time class analysis (old-stable) to ensure compatibility + if (Boolean.TRUE.equals(readSheetHolder.globalConfiguration().getEnableMetaMarked())) { + excelContentProperty = AnnotatedClassUtils.declaredExcelContentProperty( + dataMap, + readSheetHolder.excelReadHeadProperty().getTypeDescriptor(), + head.getFieldDescriptor(), + readSheetHolder); + } else { + excelContentProperty = ClassUtils.declaredExcelContentProperty( + dataMap, readSheetHolder.excelReadHeadProperty().getHeadClazz(), fieldName, readSheetHolder); + } + Object value = ConverterUtils.convertToJavaObject( cellData, head.getField(), - ClassUtils.declaredExcelContentProperty( - dataMap, - readSheetHolder.excelReadHeadProperty().getHeadClazz(), - fieldName, - readSheetHolder), + excelContentProperty, readSheetHolder.converterMap(), context, context.readRowHolder().getRowIndex(), diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/AnnotatedClassUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/AnnotatedClassUtils.java new file mode 100644 index 000000000..be0659623 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/AnnotatedClassUtils.java @@ -0,0 +1,606 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import lombok.Data; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.fesod.common.util.ListUtils; +import org.apache.fesod.common.util.MapUtils; +import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; +import org.apache.fesod.sheet.annotation.AnnotatedTypeDescriptor; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; +import org.apache.fesod.sheet.annotation.AnnotationMap; +import org.apache.fesod.sheet.annotation.ExcelIgnore; +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.converters.AutoConverter; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.exception.ExcelCommonException; +import org.apache.fesod.sheet.metadata.CachedFields; +import org.apache.fesod.sheet.metadata.ConfigurationHolder; +import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.metadata.property.FontProperty; +import org.apache.fesod.sheet.metadata.property.NumberFormatProperty; +import org.apache.fesod.sheet.metadata.property.StyleProperty; +import org.apache.fesod.sheet.write.metadata.holder.WriteHolder; + +/** + * Similar to {@link ClassUtils}, provides support for composable annotations. (beta yet) + */ +public final class AnnotatedClassUtils { + + /** + * memory cache + */ + public static final Map FIELD_CACHE = new ConcurrentHashMap<>(); + /** + * thread local cache + */ + private static final ThreadLocal> FIELD_THREAD_LOCAL = new ThreadLocal<>(); + + /** + * The cache configuration information for each of the class + */ + public static final Map, Map> CLASS_CONTENT_CACHE = + new ConcurrentHashMap<>(); + + /** + * The cache configuration information for each of the class + */ + private static final ThreadLocal, Map>> CLASS_CONTENT_THREAD_LOCAL = + new ThreadLocal<>(); + + /** + * The cache configuration information for each of the class + */ + public static final Map CONTENT_CACHE = + new ConcurrentHashMap<>(); + + /** + * The cache configuration information for each of the class + */ + private static final ThreadLocal> CONTENT_THREAD_LOCAL = + new ThreadLocal<>(); + + /** + * Calculate the configuration information for the class. (beta yet) + */ + public static ExcelContentProperty declaredExcelContentProperty( + Map dataMap, + AnnotatedTypeDescriptor typeDescriptor, + AnnotatedFieldDescriptor fieldDescriptor, + ConfigurationHolder configurationHolder) { + Class clazz = null; + if (dataMap instanceof BeanMap) { + Object bean = ((BeanMap) dataMap).getBean(); + if (bean != null) { + clazz = bean.getClass(); + } + } + return getExcelContentProperty(clazz, typeDescriptor, fieldDescriptor, configurationHolder); + } + + private static ExcelContentProperty getExcelContentProperty( + Class clazz, + AnnotatedTypeDescriptor typeDescriptor, + AnnotatedFieldDescriptor fieldDescriptor, + ConfigurationHolder configurationHolder) { + Class headClass = typeDescriptor.getAnnotatedElement(); + String fieldName = fieldDescriptor.getFieldName(); + + switch (configurationHolder.globalConfiguration().getFiledCacheLocation()) { + case THREAD_LOCAL: + Map contentCacheMap = CONTENT_THREAD_LOCAL.get(); + if (contentCacheMap == null) { + contentCacheMap = MapUtils.newHashMap(); + CONTENT_THREAD_LOCAL.set(contentCacheMap); + } + return contentCacheMap.computeIfAbsent(buildKey(clazz, headClass, fieldName), key -> { + return doGetExcelContentProperty(clazz, typeDescriptor, fieldDescriptor, configurationHolder); + }); + case MEMORY: + return CONTENT_CACHE.computeIfAbsent(buildKey(clazz, headClass, fieldName), key -> { + return doGetExcelContentProperty(clazz, typeDescriptor, fieldDescriptor, configurationHolder); + }); + case NONE: + return doGetExcelContentProperty(clazz, typeDescriptor, fieldDescriptor, configurationHolder); + default: + throw new UnsupportedOperationException("unsupported enum"); + } + } + + private static ClassUtils.ContentPropertyKey buildKey(Class clazz, Class headClass, String fieldName) { + return new ClassUtils.ContentPropertyKey(clazz, headClass, fieldName); + } + + private static Map declaredFieldContentMap( + Class clazz, ConfigurationHolder configurationHolder) { + if (clazz == null) { + return null; + } + switch (configurationHolder.globalConfiguration().getFiledCacheLocation()) { + case THREAD_LOCAL: + Map, Map> classContentCacheMap = + CLASS_CONTENT_THREAD_LOCAL.get(); + if (classContentCacheMap == null) { + classContentCacheMap = MapUtils.newHashMap(); + CLASS_CONTENT_THREAD_LOCAL.set(classContentCacheMap); + } + return classContentCacheMap.computeIfAbsent(clazz, key -> { + return doDeclaredFieldContentMap(clazz); + }); + case MEMORY: + return CLASS_CONTENT_CACHE.computeIfAbsent(clazz, key -> { + return doDeclaredFieldContentMap(clazz); + }); + case NONE: + return doDeclaredFieldContentMap(clazz); + default: + throw new UnsupportedOperationException("unsupported enum"); + } + } + + private static Map doDeclaredFieldContentMap(Class clazz) { + if (clazz == null) { + return null; + } + List tempFieldList = new ArrayList<>(); + Class tempClass = clazz; + while (tempClass != null) { + Collections.addAll(tempFieldList, tempClass.getDeclaredFields()); + // Get the parent class and give it to yourself + tempClass = tempClass.getSuperclass(); + } + + ContentStyle parentContentStyle = clazz.getAnnotation(ContentStyle.class); + ContentFontStyle parentContentFontStyle = clazz.getAnnotation(ContentFontStyle.class); + Map fieldContentMap = MapUtils.newHashMapWithExpectedSize(tempFieldList.size()); + for (Field field : tempFieldList) { + ExcelContentProperty excelContentProperty = new ExcelContentProperty(); + excelContentProperty.setField(field); + + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty != null) { + Class> convertClazz = excelProperty.converter(); + if (convertClazz != AutoConverter.class) { + try { + Converter converter = + convertClazz.getDeclaredConstructor().newInstance(); + excelContentProperty.setConverter(converter); + } catch (Exception e) { + throw new ExcelCommonException("Can not instance custom converter:" + convertClazz.getName()); + } + } + } + + ContentStyle contentStyle = field.getAnnotation(ContentStyle.class); + if (contentStyle == null) { + contentStyle = parentContentStyle; + } + excelContentProperty.setContentStyleProperty(StyleProperty.build(contentStyle)); + + ContentFontStyle contentFontStyle = field.getAnnotation(ContentFontStyle.class); + if (contentFontStyle == null) { + contentFontStyle = parentContentFontStyle; + } + excelContentProperty.setContentFontProperty(FontProperty.build(contentFontStyle)); + + excelContentProperty.setDateTimeFormatProperty( + DateTimeFormatProperty.build(field.getAnnotation(DateTimeFormat.class))); + excelContentProperty.setNumberFormatProperty( + NumberFormatProperty.build(field.getAnnotation(NumberFormat.class))); + + fieldContentMap.put(field.getName(), excelContentProperty); + } + return fieldContentMap; + } + + private static ExcelContentProperty doGetExcelContentProperty( + Class clazz, + AnnotatedTypeDescriptor typeDescriptor, + AnnotatedFieldDescriptor fieldDescriptor, + ConfigurationHolder configurationHolder) { + Class headClass = typeDescriptor.getAnnotatedElement(); + String fieldName = fieldDescriptor.getFieldName(); + + ExcelContentProperty headExcelContentProperty = Optional.ofNullable( + doDeclaredFieldContent(typeDescriptor, fieldDescriptor)) + .orElse(null); + ExcelContentProperty combineExcelContentProperty = new ExcelContentProperty(); + + combineExcelContentProperty(combineExcelContentProperty, headExcelContentProperty); + if (clazz != null && clazz != headClass) { + ExcelContentProperty excelContentProperty = Optional.ofNullable( + declaredFieldContentMap(clazz, configurationHolder)) + .map(map -> map.get(fieldName)) + .orElse(null); + + combineExcelContentProperty(combineExcelContentProperty, excelContentProperty); + } + return combineExcelContentProperty; + } + + public static void combineExcelContentProperty( + ExcelContentProperty combineExcelContentProperty, ExcelContentProperty excelContentProperty) { + if (excelContentProperty == null) { + return; + } + if (excelContentProperty.getField() != null) { + combineExcelContentProperty.setField(excelContentProperty.getField()); + } + if (excelContentProperty.getConverter() != null) { + combineExcelContentProperty.setConverter(excelContentProperty.getConverter()); + } + if (excelContentProperty.getDateTimeFormatProperty() != null) { + combineExcelContentProperty.setDateTimeFormatProperty(excelContentProperty.getDateTimeFormatProperty()); + } + if (excelContentProperty.getNumberFormatProperty() != null) { + combineExcelContentProperty.setNumberFormatProperty(excelContentProperty.getNumberFormatProperty()); + } + if (excelContentProperty.getContentStyleProperty() != null) { + combineExcelContentProperty.setContentStyleProperty(excelContentProperty.getContentStyleProperty()); + } + if (excelContentProperty.getContentFontProperty() != null) { + combineExcelContentProperty.setContentFontProperty(excelContentProperty.getContentFontProperty()); + } + } + + private static ExcelContentProperty doDeclaredFieldContent( + AnnotatedTypeDescriptor typeDescriptor, AnnotatedFieldDescriptor fieldDescriptor) { + Class clazz = typeDescriptor.getAnnotatedElement(); + if (clazz == null) { + return null; + } + + AnnotationAttributes parentContentStyle = typeDescriptor.getAnnotation(ContentStyle.class); + AnnotationAttributes parentContentFontStyle = typeDescriptor.getAnnotation(ContentFontStyle.class); + + ExcelContentProperty excelContentProperty = new ExcelContentProperty(); + excelContentProperty.setField(fieldDescriptor.getAnnotatedElement()); + + if (fieldDescriptor.hasAnnotation(ExcelProperty.class)) { + AnnotationAttributes attrs = fieldDescriptor.getAnnotation(ExcelProperty.class); + + @SuppressWarnings("unchecked") + Class> convertClazz = attrs.getRequiredAttribute("converter", Class.class); + if (convertClazz != AutoConverter.class) { + try { + Converter converter = + convertClazz.getDeclaredConstructor().newInstance(); + excelContentProperty.setConverter(converter); + } catch (Exception e) { + throw new ExcelCommonException("Can not instance custom converter:" + convertClazz.getName()); + } + } + } + + AnnotationAttributes contentStyle = fieldDescriptor.getAnnotation(ContentStyle.class); + if (contentStyle == null) { + contentStyle = parentContentStyle; + } + excelContentProperty.setContentStyleProperty(StyleProperty.build(contentStyle)); + + AnnotationAttributes contentFontStyle = fieldDescriptor.getAnnotation(ContentFontStyle.class); + if (contentFontStyle == null) { + contentFontStyle = parentContentFontStyle; + } + excelContentProperty.setContentFontProperty(FontProperty.build(contentFontStyle)); + + excelContentProperty.setDateTimeFormatProperty( + DateTimeFormatProperty.build(fieldDescriptor.getAnnotation(DateTimeFormat.class))); + excelContentProperty.setNumberFormatProperty( + NumberFormatProperty.build(fieldDescriptor.getAnnotation(NumberFormat.class))); + return excelContentProperty; + } + + /** + * Parsing field in the class + * + * @param clazz Need to parse the class + * @param configurationHolder configuration + */ + public static CachedFields declaredFields( + Class clazz, Function resolver, ConfigurationHolder configurationHolder) { + switch (configurationHolder.globalConfiguration().getFiledCacheLocation()) { + case THREAD_LOCAL: + Map fieldsCacheMap = FIELD_THREAD_LOCAL.get(); + if (fieldsCacheMap == null) { + fieldsCacheMap = MapUtils.newHashMap(); + FIELD_THREAD_LOCAL.set(fieldsCacheMap); + } + return fieldsCacheMap.computeIfAbsent(new FieldCacheKey(clazz, configurationHolder), key -> { + return doDeclaredFields(clazz, resolver, configurationHolder); + }); + case MEMORY: + return FIELD_CACHE.computeIfAbsent(new FieldCacheKey(clazz, configurationHolder), key -> { + return doDeclaredFields(clazz, resolver, configurationHolder); + }); + case NONE: + return doDeclaredFields(clazz, resolver, configurationHolder); + default: + throw new UnsupportedOperationException("unsupported enum"); + } + } + + private static CachedFields doDeclaredFields( + Class clazz, Function resolver, ConfigurationHolder configurationHolder) { + List tempFieldList = new ArrayList<>(); + Map fieldNameToField = new HashMap<>(); + Class tempClass = clazz; + // Prefer subclass fields, only process the bottom-most (subclass) definition for fields with the same name + while (tempClass != null) { + for (Field field : tempClass.getDeclaredFields()) { + String fieldName = FieldUtils.resolveCglibFieldName(field); + if (!fieldNameToField.containsKey(fieldName)) { + fieldNameToField.put(fieldName, field); + tempFieldList.add(field); + } + } + tempClass = tempClass.getSuperclass(); + } + ExcelIgnoreUnannotated excelIgnoreUnannotated = clazz.getAnnotation(ExcelIgnoreUnannotated.class); + Set ignoreSet = new HashSet<>(); + // First collect all field names annotated with ExcelIgnore (including subclass overrides) + for (Field field : tempFieldList) { + if (field.getAnnotation(ExcelIgnore.class) != null) { + ignoreSet.add(FieldUtils.resolveCglibFieldName(field)); + } + } + Map> orderFieldMap = new TreeMap<>(); + Map indexFieldMap = new TreeMap<>(); + for (Field field : tempFieldList) { + String fieldName = FieldUtils.resolveCglibFieldName(field); + // Skip if ignored + if (ignoreSet.contains(fieldName)) { + continue; + } + declaredOneField(field, orderFieldMap, indexFieldMap, ignoreSet, resolver, excelIgnoreUnannotated); + } + Map sortedFieldMap = buildSortedAllFieldMap(orderFieldMap, indexFieldMap); + CachedFields cachedFields = new CachedFields(sortedFieldMap, indexFieldMap); + + if (!(configurationHolder instanceof WriteHolder)) { + return cachedFields; + } + + WriteHolder writeHolder = (WriteHolder) configurationHolder; + + boolean needIgnore = !CollectionUtils.isEmpty(writeHolder.excludeColumnFieldNames()) + || !CollectionUtils.isEmpty(writeHolder.excludeColumnIndexes()) + || !CollectionUtils.isEmpty(writeHolder.includeColumnFieldNames()) + || !CollectionUtils.isEmpty(writeHolder.includeColumnIndexes()); + + if (!needIgnore) { + return cachedFields; + } + // ignore filed + Map tempSortedFieldMap = MapUtils.newHashMap(); + int index = 0; + for (Map.Entry entry : sortedFieldMap.entrySet()) { + Integer key = entry.getKey(); + AnnotatedFieldDescriptor field = entry.getValue(); + + // The current field needs to be ignored + if (writeHolder.ignore(field.getFieldName(), entry.getKey())) { + ignoreSet.add(field.getFieldName()); + indexFieldMap.remove(index); + } else { + // Mandatory sorted fields + if (indexFieldMap.containsKey(key)) { + tempSortedFieldMap.put(key, field); + } else { + // Need to reorder automatically + // Check whether the current key is already in use + while (tempSortedFieldMap.containsKey(index)) { + index++; + } + tempSortedFieldMap.put(index++, field); + } + } + } + cachedFields.setSortedFieldMap(tempSortedFieldMap); + + // resort field + resortField(writeHolder, cachedFields); + return cachedFields; + } + + /** + * it only works when {@link WriteHolder#includeColumnFieldNames()} or + * {@link WriteHolder#includeColumnIndexes()} has value + * and {@link WriteHolder#orderByIncludeColumn()} is true + **/ + private static void resortField(WriteHolder writeHolder, CachedFields cachedFields) { + if (!writeHolder.orderByIncludeColumn()) { + return; + } + Map indexFieldMap = cachedFields.getIndexFieldMap(); + + Collection includeColumnFieldNames = writeHolder.includeColumnFieldNames(); + if (!CollectionUtils.isEmpty(includeColumnFieldNames)) { + // Field sorted map + Map filedIndexMap = MapUtils.newHashMap(); + int fieldIndex = 0; + for (String includeColumnFieldName : includeColumnFieldNames) { + filedIndexMap.put(includeColumnFieldName, fieldIndex++); + } + + // rebuild sortedFieldMap + Map tempSortedFieldMap = MapUtils.newHashMap(); + cachedFields.getSortedFieldMap().forEach((index, field) -> { + Integer tempFieldIndex = filedIndexMap.get(field.getFieldName()); + if (tempFieldIndex != null) { + tempSortedFieldMap.put(tempFieldIndex, field); + + // The user has redefined the ordering and the ordering of annotations needs to be invalidated + if (!tempFieldIndex.equals(index)) { + indexFieldMap.remove(index); + } + } + }); + cachedFields.setSortedFieldMap(tempSortedFieldMap); + return; + } + + Collection includeColumnIndexes = writeHolder.includeColumnIndexes(); + if (!CollectionUtils.isEmpty(includeColumnIndexes)) { + // Index sorted map + Map filedIndexMap = MapUtils.newHashMap(); + int fieldIndex = 0; + for (Integer includeColumnIndex : includeColumnIndexes) { + filedIndexMap.put(includeColumnIndex, fieldIndex++); + } + + // rebuild sortedFieldMap + Map tempSortedFieldMap = MapUtils.newHashMap(); + cachedFields.getSortedFieldMap().forEach((index, field) -> { + Integer tempFieldIndex = filedIndexMap.get(index); + + // The user has redefined the ordering and the ordering of annotations needs to be invalidated + if (tempFieldIndex != null) { + tempSortedFieldMap.put(tempFieldIndex, field); + } + }); + cachedFields.setSortedFieldMap(tempSortedFieldMap); + } + } + + private static Map buildSortedAllFieldMap( + Map> orderFieldMap, + Map indexFieldMap) { + + Map sortedAllFieldMap = + new HashMap<>((orderFieldMap.size() + indexFieldMap.size()) * 4 / 3 + 1); + + Map tempIndexFieldMap = new HashMap<>(indexFieldMap); + int index = 0; + for (List fieldList : orderFieldMap.values()) { + for (AnnotatedFieldDescriptor field : fieldList) { + while (tempIndexFieldMap.containsKey(index)) { + sortedAllFieldMap.put(index, tempIndexFieldMap.get(index)); + tempIndexFieldMap.remove(index); + index++; + } + sortedAllFieldMap.put(index, field); + index++; + } + } + sortedAllFieldMap.putAll(tempIndexFieldMap); + return sortedAllFieldMap; + } + + private static void declaredOneField( + Field field, + Map> orderFieldMap, + Map indexFieldMap, + Set ignoreSet, + Function resolver, + ExcelIgnoreUnannotated excelIgnoreUnannotated) { + String fieldName = FieldUtils.resolveCglibFieldName(field); + // skip if the field is in ignoreSet + if (ignoreSet.contains(fieldName)) { + return; + } + + AnnotatedFieldDescriptor fieldDescriptor = + new AnnotatedFieldDescriptor(field, fieldName, resolver.apply(field)); + AnnotationAttributes excelProperty = fieldDescriptor.getAnnotation(ExcelProperty.class); + + if (excelProperty == null) { + if (excelIgnoreUnannotated != null || isStaticFinalOrTransient(field)) { + ignoreSet.add(fieldName); + return; + } + } + + if (excelProperty != null) { + Integer index = excelProperty.getRequiredAttribute("index", Integer.class); + if (index >= 0) { + if (indexFieldMap.containsKey(index)) { + throw new ExcelCommonException("The index of '" + + indexFieldMap.get(index).getFieldName() + "' and '" + field.getName() + + "' must be inconsistent"); + } + indexFieldMap.put(index, fieldDescriptor); + return; + } + } + + int order = Integer.MAX_VALUE; + if (excelProperty != null) { + order = excelProperty.getRequiredAttribute("order", Integer.class); + } + + List orderFieldList = + orderFieldMap.computeIfAbsent(order, key -> ListUtils.newArrayList()); + orderFieldList.add(fieldDescriptor); + } + + private static boolean isStaticFinalOrTransient(Field field) { + return (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) + || Modifier.isTransient(field.getModifiers()); + } + + @Data + public static class FieldCacheKey { + private Class clazz; + private Collection excludeColumnFieldNames; + private Collection excludeColumnIndexes; + private Collection includeColumnFieldNames; + private Collection includeColumnIndexes; + + FieldCacheKey(Class clazz, ConfigurationHolder configurationHolder) { + this.clazz = clazz; + if (configurationHolder instanceof WriteHolder) { + WriteHolder writeHolder = (WriteHolder) configurationHolder; + this.excludeColumnFieldNames = writeHolder.excludeColumnFieldNames(); + this.excludeColumnIndexes = writeHolder.excludeColumnIndexes(); + this.includeColumnFieldNames = writeHolder.includeColumnFieldNames(); + this.includeColumnIndexes = writeHolder.includeColumnIndexes(); + } + } + } + + public static void removeThreadLocalCache() { + FIELD_THREAD_LOCAL.remove(); + CLASS_CONTENT_THREAD_LOCAL.remove(); + CONTENT_THREAD_LOCAL.remove(); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java index 36cace7f5..a028e3d7d 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java @@ -32,12 +32,13 @@ import java.util.Set; import org.apache.commons.collections4.CollectionUtils; import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; import org.apache.fesod.sheet.context.WriteContext; import org.apache.fesod.sheet.enums.HeadKindEnum; -import org.apache.fesod.sheet.metadata.FieldCache; -import org.apache.fesod.sheet.metadata.FieldWrapper; +import org.apache.fesod.sheet.metadata.CachedFields; import org.apache.fesod.sheet.metadata.Head; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.util.AnnotatedClassUtils; import org.apache.fesod.sheet.util.BeanMapUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.FieldUtils; @@ -144,11 +145,31 @@ private void doAddBasicTypeToExcel( int relativeRowIndex, int dataIndex, int columnIndex) { - ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - null, - writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadClazz(), - head == null ? null : head.getFieldName(), - writeContext.currentWriteHolder()); + + ExcelContentProperty excelContentProperty; + // Supports composable annotation processing (new-beta) and + // real-time class analysis (old-stable) to ensure compatibility + if (Boolean.TRUE.equals( + writeContext.currentWriteHolder().globalConfiguration().getEnableMetaMarked())) { + if (head != null && head.hasFieldDescriptor()) { + excelContentProperty = AnnotatedClassUtils.declaredExcelContentProperty( + null, + writeContext + .currentWriteHolder() + .excelWriteHeadProperty() + .getTypeDescriptor(), + head.getFieldDescriptor(), + writeContext.currentWriteHolder()); + } else { + excelContentProperty = null; + } + } else { + excelContentProperty = ClassUtils.declaredExcelContentProperty( + null, + writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadClazz(), + head == null ? null : head.getFieldName(), + writeContext.currentWriteHolder()); + } CellWriteHandlerContext cellWriteHandlerContext = WriteHandlerUtils.createCellWriteHandlerContext( writeContext, row, rowIndex, head, columnIndex, relativeRowIndex, Boolean.FALSE, excelContentProperty); @@ -187,8 +208,23 @@ private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int continue; } - ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - beanMap, currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), name, currentWriteHolder); + ExcelContentProperty excelContentProperty; + // Supports composable annotation processing (new-beta) and + // real-time class analysis (old-stable) to ensure compatibility + if (Boolean.TRUE.equals(currentWriteHolder.globalConfiguration().getEnableMetaMarked())) { + excelContentProperty = AnnotatedClassUtils.declaredExcelContentProperty( + beanMap, + currentWriteHolder.excelWriteHeadProperty().getTypeDescriptor(), + head.getFieldDescriptor(), + currentWriteHolder); + } else { + excelContentProperty = ClassUtils.declaredExcelContentProperty( + beanMap, + currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), + name, + currentWriteHolder); + } + CellWriteHandlerContext cellWriteHandlerContext = WriteHandlerUtils.createCellWriteHandlerContext( writeContext, row, @@ -221,18 +257,37 @@ private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int } maxCellIndex++; - FieldCache fieldCache = ClassUtils.declaredFields(oneRowData.getClass(), writeContext.currentWriteHolder()); - for (Map.Entry entry : - fieldCache.getSortedFieldMap().entrySet()) { - FieldWrapper field = entry.getValue(); - String fieldName = field.getFieldName(); + CachedFields cachedFields = AnnotatedClassUtils.declaredFields( + oneRowData.getClass(), + currentWriteHolder.excelWriteHeadProperty().getMetadataReader()::read, + writeContext.currentWriteHolder()); + for (Map.Entry entry : + cachedFields.getSortedFieldMap().entrySet()) { + AnnotatedFieldDescriptor fieldDescriptor = entry.getValue(); + String fieldName = fieldDescriptor.getFieldName(); boolean uselessData = !beanKeySet.contains(fieldName) || beanMapHandledSet.contains(fieldName); if (uselessData) { continue; } Object value = beanMap.get(fieldName); - ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - beanMap, currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), fieldName, currentWriteHolder); + + ExcelContentProperty excelContentProperty; + // Supports composable annotation processing (new-beta) and + // real-time class analysis (old-stable) to ensure compatibility + if (Boolean.TRUE.equals(currentWriteHolder.globalConfiguration().getEnableMetaMarked())) { + excelContentProperty = AnnotatedClassUtils.declaredExcelContentProperty( + beanMap, + currentWriteHolder.excelWriteHeadProperty().getTypeDescriptor(), + fieldDescriptor, + currentWriteHolder); + } else { + excelContentProperty = ClassUtils.declaredExcelContentProperty( + beanMap, + currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), + fieldName, + currentWriteHolder); + } + CellWriteHandlerContext cellWriteHandlerContext = WriteHandlerUtils.createCellWriteHandlerContext( writeContext, row, diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java index 92cbbea21..b1dc166f8 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java @@ -25,7 +25,6 @@ package org.apache.fesod.sheet.write.property; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -35,6 +34,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; @@ -74,14 +74,13 @@ public ExcelWriteHeadProperty( if (getHeadKind() != HeadKindEnum.CLASS) { return; } - this.headRowHeightProperty = RowHeightProperty.build(headClazz.getAnnotation(HeadRowHeight.class)); - this.contentRowHeightProperty = RowHeightProperty.build(headClazz.getAnnotation(ContentRowHeight.class)); - this.onceAbsoluteMergeProperty = - OnceAbsoluteMergeProperty.build(headClazz.getAnnotation(OnceAbsoluteMerge.class)); + this.headRowHeightProperty = RowHeightProperty.build(findClazzAnnotation(HeadRowHeight.class)); + this.contentRowHeightProperty = RowHeightProperty.build(findClazzAnnotation(ContentRowHeight.class)); + this.onceAbsoluteMergeProperty = OnceAbsoluteMergeProperty.build(findClazzAnnotation(OnceAbsoluteMerge.class)); - ColumnWidth parentColumnWidth = headClazz.getAnnotation(ColumnWidth.class); - HeadStyle parentHeadStyle = headClazz.getAnnotation(HeadStyle.class); - HeadFontStyle parentHeadFontStyle = headClazz.getAnnotation(HeadFontStyle.class); + AnnotationAttributes parentColumnWidth = findClazzAnnotation(ColumnWidth.class); + AnnotationAttributes parentHeadStyle = findClazzAnnotation(HeadStyle.class); + AnnotationAttributes parentHeadFontStyle = findClazzAnnotation(HeadFontStyle.class); for (Map.Entry entry : getHeadMap().entrySet()) { Head headData = entry.getValue(); @@ -89,27 +88,26 @@ public ExcelWriteHeadProperty( throw new IllegalArgumentException( "Passing in the class and list the head, the two must be the same size."); } - Field field = headData.getField(); - ColumnWidth columnWidth = field.getAnnotation(ColumnWidth.class); + AnnotationAttributes columnWidth = headData.findAnnotation(ColumnWidth.class); if (columnWidth == null) { columnWidth = parentColumnWidth; } headData.setColumnWidthProperty(ColumnWidthProperty.build(columnWidth)); - HeadStyle headStyle = field.getAnnotation(HeadStyle.class); + AnnotationAttributes headStyle = headData.findAnnotation(HeadStyle.class); if (headStyle == null) { headStyle = parentHeadStyle; } headData.setHeadStyleProperty(StyleProperty.build(headStyle)); - HeadFontStyle headFontStyle = field.getAnnotation(HeadFontStyle.class); + AnnotationAttributes headFontStyle = headData.findAnnotation(HeadFontStyle.class); if (headFontStyle == null) { headFontStyle = parentHeadFontStyle; } headData.setHeadFontProperty(FontProperty.build(headFontStyle)); - headData.setLoopMergeProperty(LoopMergeProperty.build(field.getAnnotation(ContentLoopMerge.class))); + headData.setLoopMergeProperty(LoopMergeProperty.build(headData.findAnnotation(ContentLoopMerge.class))); } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptorTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptorTest.java new file mode 100644 index 000000000..2cf632fcf --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedFieldDescriptorTest.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotatedFieldDescriptor} + */ +class AnnotatedFieldDescriptorTest { + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation {} + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface AnotherAnnotation {} + + static class SampleClass { + @TestAnnotation + String sampleField; + + String plainField; + } + + // ---- Helper methods ---- + + private Field getField(String name) throws NoSuchFieldException { + return SampleClass.class.getDeclaredField(name); + } + + private AnnotationMap createAnnotationMap( + Class type, Map attrs) { + return AnnotationMap.builder() + .put(type, new AnnotationAttributes(type, attrs)) + .build(); + } + + private AnnotationMap createEmptyAnnotationMap() { + return new AnnotationMap(new LinkedHashMap<>()); + } + + // ---- Constructor tests ---- + + @Test + void shouldCreateDescriptor_whenValidArguments() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + AnnotationMap map = createEmptyAnnotationMap(); + + // when + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "sampleField", map); + + // then + Assertions.assertSame(field, descriptor.getAnnotatedElement()); + Assertions.assertEquals("sampleField", descriptor.getFieldName()); + Assertions.assertSame(map, descriptor.getAnnotationMap()); + } + + @Test + void shouldThrow_whenFieldIsNull() { + // given + AnnotationMap map = createEmptyAnnotationMap(); + + // when / then + Assertions.assertThrows(NullPointerException.class, () -> new AnnotatedFieldDescriptor(null, "field", map)); + } + + @Test + void shouldThrow_whenFieldNameIsNull() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + AnnotationMap map = createEmptyAnnotationMap(); + + // when / then + Assertions.assertThrows(NullPointerException.class, () -> new AnnotatedFieldDescriptor(field, null, map)); + } + + @Test + void shouldThrow_whenFieldNameIsBlank() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + AnnotationMap map = createEmptyAnnotationMap(); + + // when / then + Assertions.assertThrows(IllegalArgumentException.class, () -> new AnnotatedFieldDescriptor(field, " ", map)); + } + + @Test + void shouldAcceptNullAnnotationMap() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + + // when + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "sampleField", null); + + // then + Assertions.assertNull(descriptor.getAnnotationMap()); + } + + // ---- Inherited: getAnnotatedElement tests ---- + + @Test + void shouldReturnField_whenGetAnnotatedElement() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", null); + + // when / then + Assertions.assertSame(field, descriptor.getAnnotatedElement()); + } + + // ---- Inherited: hasAnnotation tests ---- + + @Test + void shouldReturnTrue_whenAnnotationPresent() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + AnnotationMap map = createAnnotationMap(TestAnnotation.class, new LinkedHashMap<>()); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "sampleField", map); + + // when / then + Assertions.assertTrue(descriptor.hasAnnotation(TestAnnotation.class)); + } + + @Test + void shouldReturnFalse_whenAnnotationNotPresent() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotationMap map = createEmptyAnnotationMap(); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", map); + + // when / then + Assertions.assertFalse(descriptor.hasAnnotation(TestAnnotation.class)); + } + + @Test + void shouldReturnFalse_whenAnnotationMapIsNull() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", null); + + // when / then + Assertions.assertFalse(descriptor.hasAnnotation(TestAnnotation.class)); + } + + // ---- Inherited: getAnnotationCount tests ---- + + @Test + void shouldReturnCorrectCount_whenAnnotationsPresent() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + Map attrs = new LinkedHashMap<>(); + AnnotationMap map = AnnotationMap.builder() + .put(TestAnnotation.class, new AnnotationAttributes(TestAnnotation.class, attrs)) + .put(AnotherAnnotation.class, new AnnotationAttributes(AnotherAnnotation.class, attrs)) + .build(); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "sampleField", map); + + // when / then + Assertions.assertEquals(2, descriptor.getAnnotationCount()); + } + + @Test + void shouldReturnZero_whenNoAnnotations() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotationMap map = createEmptyAnnotationMap(); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", map); + + // when / then + Assertions.assertEquals(0, descriptor.getAnnotationCount()); + } + + @Test + void shouldReturnZero_whenAnnotationMapIsNull() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", null); + + // when / then + Assertions.assertEquals(0, descriptor.getAnnotationCount()); + } + + // ---- Inherited: getAnnotation tests ---- + + @Test + void shouldReturnAttributes_whenAnnotationPresent() throws NoSuchFieldException { + // given + Field field = getField("sampleField"); + Map attrsMap = new LinkedHashMap<>(); + attrsMap.put("value", "test"); + AnnotationMap map = createAnnotationMap(TestAnnotation.class, attrsMap); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "sampleField", map); + + // when + AnnotationAttributes result = descriptor.getAnnotation(TestAnnotation.class); + + // then + Assertions.assertNotNull(result); + Assertions.assertEquals("test", result.get("value")); + } + + @Test + void shouldReturnNull_whenAnnotationNotPresent() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotationMap map = createEmptyAnnotationMap(); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", map); + + // when / then + Assertions.assertNull(descriptor.getAnnotation(TestAnnotation.class)); + } + + @Test + void shouldReturnNull_whenAnnotationMapIsNull() throws NoSuchFieldException { + // given + Field field = getField("plainField"); + AnnotatedFieldDescriptor descriptor = new AnnotatedFieldDescriptor(field, "plainField", null); + + // when / then + Assertions.assertNull(descriptor.getAnnotation(TestAnnotation.class)); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptorTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptorTest.java new file mode 100644 index 000000000..6efb8b660 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotatedTypeDescriptorTest.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AnnotatedTypeDescriptor} + */ +class AnnotatedTypeDescriptorTest { + + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation {} + + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface AnotherAnnotation {} + + @TestAnnotation + static class AnnotatedClass {} + + static class PlainClass {} + + // ---- Helper methods ---- + + private AnnotationMap createAnnotationMap( + Class type, Map attrs) { + return AnnotationMap.builder() + .put(type, new AnnotationAttributes(type, attrs)) + .build(); + } + + private AnnotationMap createEmptyAnnotationMap() { + return new AnnotationMap(new LinkedHashMap<>()); + } + + // ---- EMPTY constant tests ---- + + @Test + void shouldHaveNullElementAndMap_whenEmpty() { + // when / then + Assertions.assertNull(AnnotatedTypeDescriptor.EMPTY.getAnnotatedElement()); + Assertions.assertNull(AnnotatedTypeDescriptor.EMPTY.getAnnotationMap()); + Assertions.assertEquals(0, AnnotatedTypeDescriptor.EMPTY.getAnnotationCount()); + Assertions.assertFalse(AnnotatedTypeDescriptor.EMPTY.hasAnnotation(TestAnnotation.class)); + Assertions.assertNull(AnnotatedTypeDescriptor.EMPTY.getAnnotation(TestAnnotation.class)); + } + + // ---- Constructor tests ---- + + @Test + void shouldCreateDescriptor_whenValidArguments() { + // given + AnnotationMap map = createEmptyAnnotationMap(); + + // when + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(AnnotatedClass.class, map); + + // then + Assertions.assertSame(AnnotatedClass.class, descriptor.getAnnotatedElement()); + Assertions.assertSame(map, descriptor.getAnnotationMap()); + } + + @Test + void shouldAcceptNullElement() { + // given + AnnotationMap map = createEmptyAnnotationMap(); + + // when + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(null, map); + + // then + Assertions.assertNull(descriptor.getAnnotatedElement()); + Assertions.assertSame(map, descriptor.getAnnotationMap()); + } + + @Test + void shouldAcceptNullAnnotationMap() { + // when + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, null); + + // then + Assertions.assertSame(PlainClass.class, descriptor.getAnnotatedElement()); + Assertions.assertNull(descriptor.getAnnotationMap()); + } + + @Test + void shouldAcceptBothNull() { + // when + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(null, null); + + // then + Assertions.assertNull(descriptor.getAnnotatedElement()); + Assertions.assertNull(descriptor.getAnnotationMap()); + } + + // ---- Inherited: getAnnotatedElement tests ---- + + @Test + void shouldReturnClass_whenGetAnnotatedElement() { + // given + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, null); + + // when / then + Assertions.assertSame(PlainClass.class, descriptor.getAnnotatedElement()); + } + + // ---- Inherited: hasAnnotation tests ---- + + @Test + void shouldReturnTrue_whenAnnotationPresent() { + // given + AnnotationMap map = createAnnotationMap(TestAnnotation.class, new LinkedHashMap<>()); + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(AnnotatedClass.class, map); + + // when / then + Assertions.assertTrue(descriptor.hasAnnotation(TestAnnotation.class)); + } + + @Test + void shouldReturnFalse_whenAnnotationNotPresent() { + // given + AnnotationMap map = createEmptyAnnotationMap(); + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, map); + + // when / then + Assertions.assertFalse(descriptor.hasAnnotation(TestAnnotation.class)); + } + + @Test + void shouldReturnFalse_whenAnnotationMapIsNull() { + // given + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, null); + + // when / then + Assertions.assertFalse(descriptor.hasAnnotation(TestAnnotation.class)); + } + + // ---- Inherited: getAnnotationCount tests ---- + + @Test + void shouldReturnCorrectCount_whenAnnotationsPresent() { + // given + Map attrs = new LinkedHashMap<>(); + AnnotationMap map = AnnotationMap.builder() + .put(TestAnnotation.class, new AnnotationAttributes(TestAnnotation.class, attrs)) + .put(AnotherAnnotation.class, new AnnotationAttributes(AnotherAnnotation.class, attrs)) + .build(); + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(AnnotatedClass.class, map); + + // when / then + Assertions.assertEquals(2, descriptor.getAnnotationCount()); + } + + @Test + void shouldReturnZero_whenNoAnnotations() { + // given + AnnotationMap map = createEmptyAnnotationMap(); + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, map); + + // when / then + Assertions.assertEquals(0, descriptor.getAnnotationCount()); + } + + @Test + void shouldReturnZero_whenAnnotationMapIsNull() { + // given + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, null); + + // when / then + Assertions.assertEquals(0, descriptor.getAnnotationCount()); + } + + // ---- Inherited: getAnnotation tests ---- + + @Test + void shouldReturnAttributes_whenAnnotationPresent() { + // given + Map attrsMap = new LinkedHashMap<>(); + attrsMap.put("value", "test"); + AnnotationMap map = createAnnotationMap(TestAnnotation.class, attrsMap); + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(AnnotatedClass.class, map); + + // when + AnnotationAttributes result = descriptor.getAnnotation(TestAnnotation.class); + + // then + Assertions.assertNotNull(result); + Assertions.assertEquals("test", result.get("value")); + } + + @Test + void shouldReturnNull_whenAnnotationNotPresent() { + // given + AnnotationMap map = createEmptyAnnotationMap(); + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, map); + + // when / then + Assertions.assertNull(descriptor.getAnnotation(TestAnnotation.class)); + } + + @Test + void shouldReturnNull_whenAnnotationMapIsNull() { + // given + AnnotatedTypeDescriptor descriptor = new AnnotatedTypeDescriptor(PlainClass.class, null); + + // when / then + Assertions.assertNull(descriptor.getAnnotation(TestAnnotation.class)); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java index f67d1051b..679cf39e2 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; +import java.util.List; import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -45,6 +46,19 @@ class AnnotationMetadataReaderTest { int value() default 25; } + /** + * Method name ({@code width}) differs from the aliased target attribute ({@code value}), + * exercising the {@code customAttribute} path in {@link AliasFor}. + */ + @FesodMarked + @ColumnWidth(35) + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposableColumnWidthRenamed { + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int width() default 25; + } + // ---- Annotated elements ---- @ColumnWidth(20) @@ -53,6 +67,9 @@ class AnnotationMetadataReaderTest { @ComposableColumnWidth(15) static String composableField; + @ComposableColumnWidthRenamed(width = 18) + static String renamedAliasField; + static String plainField; @ColumnWidth(30) @@ -156,6 +173,205 @@ void shouldReturnNull_fromUnannotatedField() { Assertions.assertNull(result); } + // ---- enableMetaMarked = false tests ---- + + @Test + void shouldReadInnerAnnotation_whenMetaMarkedDisabled() { + // given + AnnotationMetadataReader disabledReader = new AnnotationMetadataReader(Boolean.FALSE); + Field field = getField("columnWidthField"); + + // when + AnnotationMap result = disabledReader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + AnnotationAttributes attrs = result.getAttributes(ColumnWidth.class); + Integer actualValue = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(20, actualValue); + } + + @Test + void shouldNotResolveComposableAnnotation_whenMetaMarkedDisabled() { + // given + AnnotationMetadataReader disabledReader = new AnnotationMetadataReader(Boolean.FALSE); + Field field = getField("composableField"); + + // when + AnnotationMap result = disabledReader.read(field); + + // then + // ComposableColumnWidth is not an inner annotation and meta-marked scanning is disabled, + // so neither ComposableColumnWidth nor its meta-annotation ColumnWidth should appear. + Assertions.assertNotNull(result); + Assertions.assertFalse(result.hasAnnotation(ComposableColumnWidth.class)); + Assertions.assertFalse(result.hasAnnotation(ColumnWidth.class)); + } + + @Test + void shouldReturnNull_fromUnannotatedField_whenMetaMarkedDisabled() { + // given + AnnotationMetadataReader disabledReader = new AnnotationMetadataReader(Boolean.FALSE); + Field field = getField("plainField"); + + // when + AnnotationMap result = disabledReader.read(field); + + // then + Assertions.assertNull(result); + } + + @Test + void shouldReturnSameInstance_whenReadTwiceWithMetaMarkedDisabled() { + // given + AnnotationMetadataReader disabledReader = new AnnotationMetadataReader(Boolean.FALSE); + Field field = getField("columnWidthField"); + + // when + AnnotationMap first = disabledReader.read(field); + AnnotationMap second = disabledReader.read(field); + + // then + Assertions.assertSame(first, second); + } + + @Test + void shouldResolveComposableAnnotation_whenMetaMarkedEnabled() { + // given + AnnotationMetadataReader enabledReader = new AnnotationMetadataReader(Boolean.TRUE); + Field field = getField("composableField"); + + // when + AnnotationMap result = enabledReader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ComposableColumnWidth.class)); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + // AliasFor should synthesize: ComposableColumnWidth.value(15) -> ColumnWidth.value + AnnotationAttributes columnWidthAttrs = result.getAttributes(ColumnWidth.class); + Integer value = + Assertions.assertDoesNotThrow(() -> columnWidthAttrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(15, value); + } + + // ---- Renamed @AliasFor (customAttribute) tests ---- + + @Test + void shouldSynthesizeAlias_whenMethodNameDiffersFromTargetAttribute() { + // given: @ComposableColumnWidthRenamed uses width() → @AliasFor(attribute="value"), + // so the method name "width" differs from the target attribute "value" + Field field = getField("renamedAliasField"); + + // when + AnnotationMap result = reader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + // AliasFor should synthesize: ComposableColumnWidthRenamed.width(18) → ColumnWidth.value(18) + AnnotationAttributes attrs = result.getAttributes(ColumnWidth.class); + Integer actualValue = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(18, actualValue); + } + + @Test + void shouldContainComposableAnnotation_withRenamedAliasInAnnotationMap() { + // given + Field field = getField("renamedAliasField"); + + // when + AnnotationMap result = reader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ComposableColumnWidthRenamed.class)); + AnnotationAttributes attrs = result.getAttributes(ComposableColumnWidthRenamed.class); + Integer actualWidth = Assertions.assertDoesNotThrow(() -> attrs.getRequiredAttribute("width", Integer.class)); + Assertions.assertEquals(18, actualWidth); + } + + @Test + void shouldResolveRenamedAlias_whenMetaMarkedEnabled() { + // given + AnnotationMetadataReader enabledReader = new AnnotationMetadataReader(Boolean.TRUE); + Field field = getField("renamedAliasField"); + + // when + AnnotationMap result = enabledReader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertTrue(result.hasAnnotation(ComposableColumnWidthRenamed.class)); + Assertions.assertTrue(result.hasAnnotation(ColumnWidth.class)); + AnnotationAttributes columnWidthAttrs = result.getAttributes(ColumnWidth.class); + Integer value = + Assertions.assertDoesNotThrow(() -> columnWidthAttrs.getRequiredAttribute("value", Integer.class)); + Assertions.assertEquals(18, value); + } + + @Test + void shouldNotResolveRenamedAlias_whenMetaMarkedDisabled() { + // given + AnnotationMetadataReader disabledReader = new AnnotationMetadataReader(Boolean.FALSE); + Field field = getField("renamedAliasField"); + + // when + AnnotationMap result = disabledReader.read(field); + + // then + Assertions.assertNotNull(result); + Assertions.assertFalse(result.hasAnnotation(ComposableColumnWidthRenamed.class)); + Assertions.assertFalse(result.hasAnnotation(ColumnWidth.class)); + } + + @Test + void shouldPopulateCustomAttribute_inAliasFor_whenMethodNameDiffersFromTarget() { + // given: resolve the annotation directly via DefaultAnnotationMetadataResolver + // to inspect the AliasFor value object + DefaultAnnotationMetadataResolver resolver = new DefaultAnnotationMetadataResolver(); + ComposableColumnWidthRenamed ann = + getField("renamedAliasField").getAnnotation(ComposableColumnWidthRenamed.class); + + // when + AnnotationMetadata metadata = resolver.resolve(ann); + + // then + List aliases = metadata.getAliases(); + Assertions.assertEquals(1, aliases.size()); + + AliasFor alias = aliases.get(0); + Assertions.assertEquals(ComposableColumnWidthRenamed.class, alias.getMarked()); + Assertions.assertEquals(ColumnWidth.class, alias.getTarget()); + // customAttribute is the source method name "width" (different from target "value") + Assertions.assertEquals("width", alias.getCustomAttribute()); + // attribute is the target attribute name "value" + Assertions.assertEquals("value", alias.getAttribute()); + Assertions.assertEquals(18, alias.getValue()); + } + + @Test + void shouldSetCustomAttributeEqualToAttribute_whenMethodNameMatchesTarget() { + // given: the existing ComposableColumnWidth uses value() → @AliasFor(attribute="value"), + // same-name case, so customAttribute should equal attribute + DefaultAnnotationMetadataResolver resolver = new DefaultAnnotationMetadataResolver(); + ComposableColumnWidth ann = getField("composableField").getAnnotation(ComposableColumnWidth.class); + + // when + AnnotationMetadata metadata = resolver.resolve(ann); + + // then + List aliases = metadata.getAliases(); + Assertions.assertEquals(1, aliases.size()); + + AliasFor alias = aliases.get(0); + // Same-name case: customAttribute == attribute == "value" + Assertions.assertEquals("value", alias.getCustomAttribute()); + Assertions.assertEquals("value", alias.getAttribute()); + Assertions.assertEquals(15, alias.getValue()); + } + // ---- Caching tests ---- @Test diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java index c16caf730..d23459db6 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java @@ -26,6 +26,8 @@ import java.lang.annotation.Target; import java.math.RoundingMode; import lombok.Data; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; +import org.apache.fesod.sheet.annotation.AnnotatedTypeDescriptor; import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.AnnotationMap; import org.apache.fesod.sheet.annotation.ExcelProperty; @@ -99,6 +101,7 @@ class CompositeAnnotationTest { @BeforeEach void setup() { Mockito.lenient().when(configurationHolder.globalConfiguration()).thenReturn(globalConfiguration); + Mockito.lenient().when(globalConfiguration.getEnableMetaMarked()).thenReturn(true); Mockito.lenient().when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); } @@ -440,7 +443,7 @@ void shouldIncludeComposableAndInnerAnnotation_whenPartialAliasFor() { Head head = property.getHeadMap().get(0); Assertions.assertNotNull(head); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertFalse(fieldAnnotationMap.isEmpty()); Assertions.assertEquals(2, fieldAnnotationMap.size()); @@ -469,9 +472,9 @@ void shouldIncludeAllAliasedValuesInComposable_whenAllParamsAliasFor() { // then Assertions.assertEquals(1, property.getHeadMap().size()); - Head head = property.getHeadMap().get(0); + Head head = property.getHeadMap().get(2); Assertions.assertNotNull(head); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertFalse(fieldAnnotationMap.isEmpty()); Assertions.assertEquals(2, fieldAnnotationMap.size()); @@ -509,7 +512,7 @@ void shouldPreserveDirectAnnotationValue_whenOriginalAndComposableAtSameLevel() Head head = property.getHeadMap().get(0); Assertions.assertNotNull(head); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertFalse(fieldAnnotationMap.isEmpty()); Assertions.assertEquals(2, fieldAnnotationMap.size()); @@ -537,7 +540,7 @@ void shouldIncludeComposableAndInnerAnnotation_whenNumberFormatWithAliasFor() { // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertEquals(2, annotationMap.size()); Assertions.assertTrue(annotationMap.hasAnnotation(ComposableNumberFormat.class)); @@ -563,7 +566,7 @@ void shouldExpandAllInnerAnnotations_whenContentStylePresetNoMethods() { // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertEquals(4, annotationMap.size()); Assertions.assertTrue(annotationMap.hasAnnotation(ComposableContentStylePreset.class)); @@ -598,7 +601,7 @@ void shouldIncludeComposableAndInnerAnnotation_whenDateTimeFormatWithAliasFor() Assertions.assertEquals(1, property.getHeadMap().size()); Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertEquals(2, annotationMap.size()); Assertions.assertTrue(annotationMap.hasAnnotation(ComposableDateTimeFormat.class)); @@ -626,7 +629,7 @@ void shouldIncludeComposableAndInnerAnnotation_whenContentLoopMergeWithAliasFor( Assertions.assertEquals(1, property.getHeadMap().size()); Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertEquals(2, annotationMap.size()); Assertions.assertTrue(annotationMap.hasAnnotation(ComposableContentLoopMerge.class)); @@ -640,6 +643,68 @@ void shouldIncludeComposableAndInnerAnnotation_whenContentLoopMergeWithAliasFor( Assertions.assertEquals(3, targetAttrs.getRequiredAttribute("eachRow", Integer.class)); Assertions.assertEquals(2, targetAttrs.getRequiredAttribute("columnExtend", Integer.class)); } + + @Test + void shouldPopulateFieldDescriptor_withCorrectFieldNameAndElement() { + // given - ExcelModelWithComposableField has @ComposableExcelProperty({"Custom Name"}) + // on the "name" field + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableField.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + Assertions.assertEquals("name", fieldDescriptor.getFieldName()); + Assertions.assertNotNull(fieldDescriptor.getAnnotatedElement()); + Assertions.assertEquals( + "name", fieldDescriptor.getAnnotatedElement().getName()); + } + + @Test + void shouldDelegateHasAnnotationAndCount_throughFieldDescriptor() { + // given - ExcelModelWithComposableContentStyle has @ComposableContentStylePreset + @ExcelProperty("Data") + // on the "data" field + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableContentStyle.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + Assertions.assertEquals(4, fieldDescriptor.getAnnotationCount()); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ComposableContentStylePreset.class)); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ContentStyle.class)); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ContentFontStyle.class)); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ExcelProperty.class)); + Assertions.assertFalse(fieldDescriptor.hasAnnotation(ColumnWidth.class)); + } + + @Test + void shouldDelegateGetAnnotation_throughFieldDescriptor() { + // given - ExcelModelWithComposableNumberFormat has @ComposableNumberFormat("#,##0.00") + // on the "amount" field + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableNumberFormat.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + + AnnotationAttributes numberAttrs = fieldDescriptor.getAnnotation(NumberFormat.class); + Assertions.assertNotNull(numberAttrs); + Assertions.assertEquals("#,##0.00", numberAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes missingAttrs = fieldDescriptor.getAnnotation(ColumnWidth.class); + Assertions.assertNull(missingAttrs); + } } @Nested @@ -654,7 +719,7 @@ void shouldIncludeComposableAndInnerAnnotation_whenAliasForPresent() { new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableClassAnnotation.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertFalse(classAnnotationMap.isEmpty()); Assertions.assertEquals(2, classAnnotationMap.size()); @@ -678,7 +743,7 @@ void shouldExpandAllInnerAnnotations_whenNoMethodsInComposable() { new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableGroupAnnotation.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertFalse(classAnnotationMap.isEmpty()); Assertions.assertEquals(3, classAnnotationMap.size()); @@ -708,7 +773,7 @@ void shouldExpandAllInnerAnnotations_whenTableStylePresetNoMethods() { new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableTableStyle.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(4, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableTableStylePreset.class)); @@ -743,7 +808,7 @@ void shouldExpandAllInnerAnnotations_whenHeaderStylePresetNoMethods() { new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableHeaderStyle.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(3, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableHeaderStylePreset.class)); @@ -761,6 +826,59 @@ void shouldExpandAllInnerAnnotations_whenHeaderStylePresetNoMethods() { Assertions.assertEquals((short) 14, fontAttrs.getRequiredAttribute("fontHeightInPoints", Short.class)); Assertions.assertEquals(BooleanEnum.TRUE, fontAttrs.getRequiredAttribute("bold", BooleanEnum.class)); } + + @Test + void shouldPopulateTypeDescriptor_withCorrectAnnotatedElement() { + // given - ExcelModelWithComposableClassAnnotation has @ComposableColumnWidth(25) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableClassAnnotation.class, null); + + // then + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + Assertions.assertSame(ExcelModelWithComposableClassAnnotation.class, typeDescriptor.getAnnotatedElement()); + } + + @Test + void shouldDelegateHasAnnotationAndCount_throughTypeDescriptor() { + // given - ExcelModelWithComposableGroupAnnotation has @ComposableAnnotationWithCommonStyle + // which groups @ColumnWidth(10) and @HeadStyle(fillForegroundColor=10) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableGroupAnnotation.class, null); + + // then + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + Assertions.assertEquals(3, typeDescriptor.getAnnotationCount()); + Assertions.assertTrue(typeDescriptor.hasAnnotation(ComposableAnnotationWithCommonStyle.class)); + Assertions.assertTrue(typeDescriptor.hasAnnotation(ColumnWidth.class)); + Assertions.assertTrue(typeDescriptor.hasAnnotation(HeadStyle.class)); + Assertions.assertFalse(typeDescriptor.hasAnnotation(ContentRowHeight.class)); + } + + @Test + void shouldDelegateGetAnnotation_throughTypeDescriptor() { + // given - ExcelModelWithComposableClassAnnotation has @ComposableColumnWidth(25) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableClassAnnotation.class, null); + + // then + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + + AnnotationAttributes widthAttrs = typeDescriptor.getAnnotation(ColumnWidth.class); + Assertions.assertNotNull(widthAttrs); + Assertions.assertEquals(25, widthAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes missingAttrs = typeDescriptor.getAnnotation(HeadStyle.class); + Assertions.assertNull(missingAttrs); + } } @Nested @@ -776,7 +894,7 @@ void shouldPopulateBothLevels_whenClassComposableGroupAndFieldComposableAliasFor new ExcelHeadProperty(configurationHolder, ExcelModelMixedClassAndFieldComposable.class, null); // then - class-level annotationMap - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(4, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableTableStylePreset.class)); @@ -804,7 +922,7 @@ void shouldPopulateBothLevels_whenClassComposableGroupAndFieldComposableAliasFor Assertions.assertEquals(1, property.getHeadMap().size()); Head head = property.getHeadMap().get(0); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertEquals(2, fieldAnnotationMap.size()); Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableExcelProperty.class)); @@ -829,7 +947,7 @@ void shouldPopulateBothLevels_whenClassAndFieldBothUseNoMethodsComposable() { new ExcelHeadProperty(configurationHolder, ExcelModelMixedBothNoMethods.class, null); // then - class-level annotationMap - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(3, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableAnnotationWithCommonStyle.class)); @@ -852,7 +970,7 @@ void shouldPopulateBothLevels_whenClassAndFieldBothUseNoMethodsComposable() { Assertions.assertEquals(1, property.getHeadMap().size()); Head head = property.getHeadMap().get(0); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertEquals(4, fieldAnnotationMap.size()); @@ -884,7 +1002,7 @@ void shouldPopulateBothLevels_whenClassAndFieldBothUseAliasForComposable() { new ExcelHeadProperty(configurationHolder, ExcelModelMixedAliasForBothLevels.class, null); // then - class-level annotationMap - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(2, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableColumnWidth.class)); @@ -898,7 +1016,7 @@ void shouldPopulateBothLevels_whenClassAndFieldBothUseAliasForComposable() { // then - field-level annotationMap Head head = property.getHeadMap().get(0); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertEquals(2, fieldAnnotationMap.size()); Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableNumberFormat.class)); @@ -924,7 +1042,7 @@ void shouldPopulateEachFieldIndependently_whenClassComposableAndMultipleFieldsWi new ExcelHeadProperty(configurationHolder, ExcelModelMixedMultipleFields.class, null); // then - class-level annotationMap is shared - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(4, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableTableStylePreset.class)); @@ -952,7 +1070,7 @@ void shouldPopulateEachFieldIndependently_whenClassComposableAndMultipleFieldsWi Assertions.assertEquals(2, property.getHeadMap().size()); Head nameHead = property.getHeadMap().get(0); - AnnotationMap nameAnnotationMap = nameHead.getAnnotationMap(); + AnnotationMap nameAnnotationMap = nameHead.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(nameAnnotationMap); Assertions.assertTrue(nameAnnotationMap.hasAnnotation(ComposableExcelProperty.class)); Assertions.assertTrue(nameAnnotationMap.hasAnnotation(ExcelProperty.class)); @@ -968,7 +1086,7 @@ void shouldPopulateEachFieldIndependently_whenClassComposableAndMultipleFieldsWi expectedValue, targetField1Attrs.getRequiredAttribute("value", String[].class)); Head amountHead = property.getHeadMap().get(1); - AnnotationMap amountAnnotationMap = amountHead.getAnnotationMap(); + AnnotationMap amountAnnotationMap = amountHead.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(amountAnnotationMap); Assertions.assertTrue(amountAnnotationMap.hasAnnotation(ComposableNumberFormat.class)); Assertions.assertTrue(amountAnnotationMap.hasAnnotation(NumberFormat.class)); @@ -992,7 +1110,7 @@ void shouldPopulateBothLevels_whenClassHeaderStylePresetAndFieldDateTimeFormat() new ExcelHeadProperty(configurationHolder, ExcelModelMixedHeaderStyleAndDateFormat.class, null); // then - class-level annotationMap - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(3, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ComposableHeaderStylePreset.class)); @@ -1012,7 +1130,7 @@ void shouldPopulateBothLevels_whenClassHeaderStylePresetAndFieldDateTimeFormat() // then - field-level annotationMap Head head = property.getHeadMap().get(0); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertEquals(2, fieldAnnotationMap.size()); Assertions.assertTrue(fieldAnnotationMap.hasAnnotation(ComposableDateTimeFormat.class)); @@ -1024,6 +1142,63 @@ void shouldPopulateBothLevels_whenClassHeaderStylePresetAndFieldDateTimeFormat() AnnotationAttributes targetAttrs = fieldAnnotationMap.getAttributes(DateTimeFormat.class); Assertions.assertEquals("yyyy-MM-dd", targetAttrs.getRequiredAttribute("value", String.class)); } + + @Test + void shouldPopulateDescriptorProperties_atBothLevels() { + // given - class has @ComposableTableStylePreset + // field "name" has @ComposableExcelProperty({"Mixed Name"}) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedClassAndFieldComposable.class, null); + + // then - type descriptor + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + Assertions.assertSame(ExcelModelMixedClassAndFieldComposable.class, typeDescriptor.getAnnotatedElement()); + Assertions.assertEquals(4, typeDescriptor.getAnnotationCount()); + + // then - field descriptor + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + Assertions.assertEquals("name", fieldDescriptor.getFieldName()); + Assertions.assertNotNull(fieldDescriptor.getAnnotatedElement()); + Assertions.assertEquals( + "name", fieldDescriptor.getAnnotatedElement().getName()); + Assertions.assertEquals(2, fieldDescriptor.getAnnotationCount()); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ComposableExcelProperty.class)); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ExcelProperty.class)); + } + + @Test + void shouldPopulateIndependentFieldDescriptors_forMultipleFields() { + // given - class has @ComposableTableStylePreset + // field 0 "name" has @ComposableExcelProperty({"Name"}) + // field 1 "amount" has @ComposableNumberFormat("#,##0.00") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedMultipleFields.class, null); + + // then + Assertions.assertEquals(2, property.getHeadMap().size()); + + Head nameHead = property.getHeadMap().get(0); + AnnotatedFieldDescriptor nameDescriptor = nameHead.getFieldDescriptor(); + Assertions.assertEquals("name", nameDescriptor.getFieldName()); + Assertions.assertEquals("name", nameDescriptor.getAnnotatedElement().getName()); + Assertions.assertTrue(nameDescriptor.hasAnnotation(ComposableExcelProperty.class)); + Assertions.assertFalse(nameDescriptor.hasAnnotation(ComposableNumberFormat.class)); + + Head amountHead = property.getHeadMap().get(1); + AnnotatedFieldDescriptor amountDescriptor = amountHead.getFieldDescriptor(); + Assertions.assertEquals("amount", amountDescriptor.getFieldName()); + Assertions.assertEquals( + "amount", amountDescriptor.getAnnotatedElement().getName()); + Assertions.assertTrue(amountDescriptor.hasAnnotation(ComposableNumberFormat.class)); + Assertions.assertFalse(amountDescriptor.hasAnnotation(ComposableExcelProperty.class)); + } } @Nested diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java index 62ca06749..7d767df89 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java @@ -23,6 +23,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; +import org.apache.fesod.sheet.annotation.AnnotatedTypeDescriptor; import org.apache.fesod.sheet.annotation.AnnotationAttributes; import org.apache.fesod.sheet.annotation.AnnotationMap; import org.apache.fesod.sheet.annotation.ExcelProperty; @@ -168,7 +170,7 @@ void shouldSetNullFieldAnnotationMap_whenFieldHasNoRelevantAnnotations() { Head head = property.getHeadMap().get(0); Assertions.assertNotNull(head); - Assertions.assertNull(head.getAnnotationMap()); + Assertions.assertNull(head.getFieldDescriptor().getAnnotationMap()); } @Test @@ -184,7 +186,7 @@ void shouldPopulateFieldAnnotationMap_withExcelProperty_whenFieldAnnotated() { Head head = property.getHeadMap().get(0); Assertions.assertNotNull(head); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertFalse(fieldAnnotationMap.isEmpty()); Assertions.assertEquals(1, fieldAnnotationMap.size()); @@ -208,7 +210,7 @@ void shouldPopulateFieldAnnotationMap_withMultipleAnnotations_whenFieldAnnotated Head head = property.getHeadMap().get(0); Assertions.assertNotNull(head); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertFalse(fieldAnnotationMap.isEmpty()); Assertions.assertEquals(3, fieldAnnotationMap.size()); @@ -234,7 +236,7 @@ void shouldPopulateFieldAnnotationMap_withNumberFormat_whenFieldAnnotated() { // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertTrue(annotationMap.hasAnnotation(NumberFormat.class)); @@ -255,7 +257,7 @@ void shouldPopulateFieldAnnotationMap_withContentFontStyle_whenFieldAnnotated() // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertTrue(annotationMap.hasAnnotation(ContentFontStyle.class)); @@ -275,7 +277,7 @@ void shouldPopulateFieldAnnotationMap_withContentLoopMerge_whenFieldAnnotated() // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertTrue(annotationMap.hasAnnotation(ContentLoopMerge.class)); @@ -294,7 +296,7 @@ void shouldPopulateFieldAnnotationMap_withContentStyle_whenFieldAnnotated() { // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertTrue(annotationMap.hasAnnotation(ContentStyle.class)); @@ -313,7 +315,7 @@ void shouldPopulateFieldAnnotationMap_withHeadFontStyle_whenFieldAnnotated() { // then Head head = property.getHeadMap().get(0); - AnnotationMap annotationMap = head.getAnnotationMap(); + AnnotationMap annotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(annotationMap); Assertions.assertTrue(annotationMap.hasAnnotation(HeadFontStyle.class)); @@ -321,6 +323,83 @@ void shouldPopulateFieldAnnotationMap_withHeadFontStyle_whenFieldAnnotated() { Assertions.assertEquals("Calibri", attrs.getRequiredAttribute("fontName", String.class)); Assertions.assertEquals((short) 10, attrs.getRequiredAttribute("color", Short.class)); } + + @Test + void shouldPopulateFieldDescriptor_withCorrectFieldNameAndElement_whenFieldAnnotated() { + // given - ExcelModelWithFieldProperty has @ExcelProperty("Name") on "name" field + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithFieldProperty.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + Assertions.assertEquals("name", fieldDescriptor.getFieldName()); + Assertions.assertNotNull(fieldDescriptor.getAnnotatedElement()); + Assertions.assertEquals( + "name", fieldDescriptor.getAnnotatedElement().getName()); + } + + @Test + void shouldPopulateFieldDescriptor_withCorrectFieldNameAndElement_whenPlainField() { + // given - ExcelModelWithPlainField has a plain "name" field without annotations + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithPlainField.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + Assertions.assertEquals("name", fieldDescriptor.getFieldName()); + Assertions.assertNotNull(fieldDescriptor.getAnnotatedElement()); + Assertions.assertEquals( + "name", fieldDescriptor.getAnnotatedElement().getName()); + Assertions.assertEquals(0, fieldDescriptor.getAnnotationCount()); + } + + @Test + void shouldDelegateHasAnnotationAndCount_throughFieldDescriptor() { + // given - ExcelModelWithMultipleFieldAnnotations has @ExcelProperty, @DateTimeFormat, @ColumnWidth + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithMultipleFieldAnnotations.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + Assertions.assertEquals(3, fieldDescriptor.getAnnotationCount()); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ExcelProperty.class)); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(DateTimeFormat.class)); + Assertions.assertTrue(fieldDescriptor.hasAnnotation(ColumnWidth.class)); + Assertions.assertFalse(fieldDescriptor.hasAnnotation(NumberFormat.class)); + } + + @Test + void shouldDelegateGetAnnotation_throughFieldDescriptor() { + // given - ExcelModelWithNumberFormat has @ExcelProperty("Amount") + @NumberFormat("#,##0.00") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithNumberFormat.class, null); + + // then + Head head = property.getHeadMap().get(0); + AnnotatedFieldDescriptor fieldDescriptor = head.getFieldDescriptor(); + Assertions.assertNotNull(fieldDescriptor); + + AnnotationAttributes numberAttrs = fieldDescriptor.getAnnotation(NumberFormat.class); + Assertions.assertNotNull(numberAttrs); + Assertions.assertEquals("#,##0.00", numberAttrs.getRequiredAttribute("value", String.class)); + + AnnotationAttributes missingAttrs = fieldDescriptor.getAnnotation(DateTimeFormat.class); + Assertions.assertNull(missingAttrs); + } } // ---- Model classes ---- @@ -410,7 +489,7 @@ void shouldSetNullHeadClazzAnnotationMap_whenNoHeadClazzProvided() { ExcelHeadProperty property = new ExcelHeadProperty(configurationHolder, null, head); // then - Assertions.assertNull(property.getHeadClazzAnnotationMap()); + Assertions.assertNull(property.getTypeDescriptor().getAnnotationMap()); } @Test @@ -422,7 +501,7 @@ void shouldSetNullHeadClazzAnnotationMap_whenHeadClazzHasNoAnnotations() { new ExcelHeadProperty(configurationHolder, ExcelModelWithoutAnnotations.class, null); // then - Assertions.assertNull(property.getHeadClazzAnnotationMap()); + Assertions.assertNull(property.getTypeDescriptor().getAnnotationMap()); } @Test @@ -434,7 +513,7 @@ void shouldPopulateHeadClazzAnnotationMap_withColumnWidth_whenClassAnnotated() { new ExcelHeadProperty(configurationHolder, ExcelModelWithClassColumnWidth.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertFalse(classAnnotationMap.isEmpty()); Assertions.assertEquals(1, classAnnotationMap.size()); @@ -454,7 +533,7 @@ void shouldPopulateHeadClazzAnnotationMap_withMultipleAnnotations_whenClassAnnot new ExcelHeadProperty(configurationHolder, ExcelModelWithMultipleClassAnnotations.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertFalse(classAnnotationMap.isEmpty()); Assertions.assertEquals(2, classAnnotationMap.size()); @@ -477,7 +556,7 @@ void shouldPopulateHeadClazzAnnotationMap_withContentRowHeight_whenClassAnnotate new ExcelHeadProperty(configurationHolder, ExcelModelWithContentRowHeight.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ContentRowHeight.class)); @@ -494,7 +573,7 @@ void shouldPopulateHeadClazzAnnotationMap_withHeadRowHeight_whenClassAnnotated() new ExcelHeadProperty(configurationHolder, ExcelModelWithHeadRowHeight.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertTrue(classAnnotationMap.hasAnnotation(HeadRowHeight.class)); @@ -512,7 +591,7 @@ void shouldPopulateHeadClazzAnnotationMap_withOnceAbsoluteMerge_whenClassAnnotat new ExcelHeadProperty(configurationHolder, ExcelModelWithOnceAbsoluteMerge.class, null); // then - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertTrue(classAnnotationMap.hasAnnotation(OnceAbsoluteMerge.class)); @@ -522,6 +601,75 @@ void shouldPopulateHeadClazzAnnotationMap_withOnceAbsoluteMerge_whenClassAnnotat Assertions.assertEquals(0, attrs.getRequiredAttribute("firstColumnIndex", Integer.class)); Assertions.assertEquals(2, attrs.getRequiredAttribute("lastColumnIndex", Integer.class)); } + + @Test + void shouldSetEmptyTypeDescriptor_whenNoHeadClazzProvided() { + // given + List> head = new ArrayList<>(); + head.add(Arrays.asList("Name")); + + // when + ExcelHeadProperty property = new ExcelHeadProperty(configurationHolder, null, head); + + // then + Assertions.assertSame(AnnotatedTypeDescriptor.EMPTY, property.getTypeDescriptor()); + Assertions.assertNull(property.getTypeDescriptor().getAnnotatedElement()); + Assertions.assertNull(property.getTypeDescriptor().getAnnotationMap()); + Assertions.assertEquals(0, property.getTypeDescriptor().getAnnotationCount()); + Assertions.assertFalse(property.getTypeDescriptor().hasAnnotation(ColumnWidth.class)); + } + + @Test + void shouldPopulateTypeDescriptor_withCorrectAnnotatedElement() { + // given - ExcelModelWithClassColumnWidth has @ColumnWidth(20) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithClassColumnWidth.class, null); + + // then + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + Assertions.assertSame(ExcelModelWithClassColumnWidth.class, typeDescriptor.getAnnotatedElement()); + } + + @Test + void shouldDelegateHasAnnotationAndCount_throughTypeDescriptor() { + // given - ExcelModelWithMultipleClassAnnotations has @ColumnWidth(15) and + // @HeadStyle(fillForegroundColor=10) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithMultipleClassAnnotations.class, null); + + // then + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + Assertions.assertEquals(2, typeDescriptor.getAnnotationCount()); + Assertions.assertTrue(typeDescriptor.hasAnnotation(ColumnWidth.class)); + Assertions.assertTrue(typeDescriptor.hasAnnotation(HeadStyle.class)); + Assertions.assertFalse(typeDescriptor.hasAnnotation(ContentRowHeight.class)); + } + + @Test + void shouldDelegateGetAnnotation_throughTypeDescriptor() { + // given - ExcelModelWithClassColumnWidth has @ColumnWidth(20) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithClassColumnWidth.class, null); + + // then + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + + AnnotationAttributes widthAttrs = typeDescriptor.getAnnotation(ColumnWidth.class); + Assertions.assertNotNull(widthAttrs); + Assertions.assertEquals(20, widthAttrs.getRequiredAttribute("value", Integer.class)); + + AnnotationAttributes missingAttrs = typeDescriptor.getAnnotation(HeadStyle.class); + Assertions.assertNull(missingAttrs); + } } @Nested @@ -538,7 +686,7 @@ void shouldPopulateBothLevels_whenAllTwelveAnnotationsUsedAcrossClassAndField() new ExcelHeadProperty(configurationHolder, ExcelModelMixedAllAnnotations.class, null); // then - class-level annotationMap covers 8 annotations - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(8, classAnnotationMap.size()); @@ -593,7 +741,7 @@ void shouldPopulateBothLevels_whenAllTwelveAnnotationsUsedAcrossClassAndField() // then - field-level annotationMap covers 4 annotations Head head = property.getHeadMap().get(0); - AnnotationMap fieldAnnotationMap = head.getAnnotationMap(); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(fieldAnnotationMap); Assertions.assertEquals(4, fieldAnnotationMap.size()); @@ -631,7 +779,7 @@ void shouldPopulateEachFieldIndependently_whenMixedClassAndFieldAnnotations() { new ExcelHeadProperty(configurationHolder, ExcelModelMixedClassStyleAndFieldFormat.class, null); // then - class-level annotationMap - AnnotationMap classAnnotationMap = property.getHeadClazzAnnotationMap(); + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); Assertions.assertNotNull(classAnnotationMap); Assertions.assertEquals(2, classAnnotationMap.size()); Assertions.assertTrue(classAnnotationMap.hasAnnotation(ColumnWidth.class)); @@ -641,7 +789,7 @@ void shouldPopulateEachFieldIndependently_whenMixedClassAndFieldAnnotations() { Assertions.assertEquals(2, property.getHeadMap().size()); Head amountHead = property.getHeadMap().get(0); - AnnotationMap amountMap = amountHead.getAnnotationMap(); + AnnotationMap amountMap = amountHead.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(amountMap); Assertions.assertEquals(2, amountMap.size()); Assertions.assertTrue(amountMap.hasAnnotation(ExcelProperty.class)); @@ -651,7 +799,7 @@ void shouldPopulateEachFieldIndependently_whenMixedClassAndFieldAnnotations() { amountMap.getAttributes(NumberFormat.class).getRequiredAttribute("value", String.class)); Head dateHead = property.getHeadMap().get(1); - AnnotationMap dateMap = dateHead.getAnnotationMap(); + AnnotationMap dateMap = dateHead.getFieldDescriptor().getAnnotationMap(); Assertions.assertNotNull(dateMap); Assertions.assertEquals(2, dateMap.size()); Assertions.assertTrue(dateMap.hasAnnotation(ExcelProperty.class)); @@ -660,5 +808,36 @@ void shouldPopulateEachFieldIndependently_whenMixedClassAndFieldAnnotations() { "yyyy-MM-dd", dateMap.getAttributes(DateTimeFormat.class).getRequiredAttribute("value", String.class)); } + + @Test + void shouldPopulateDescriptorProperties_atBothLevels() { + // given - class has @ColumnWidth(20) and @HeadStyle(fillForegroundColor=10) + // field 0 "amount" has @ExcelProperty("Amount") + @NumberFormat("#,##0.00") + // field 1 "date" has @ExcelProperty("Date") + @DateTimeFormat("yyyy-MM-dd") + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelMixedClassStyleAndFieldFormat.class, null); + + // then - type descriptor + AnnotatedTypeDescriptor typeDescriptor = property.getTypeDescriptor(); + Assertions.assertNotNull(typeDescriptor); + Assertions.assertSame(ExcelModelMixedClassStyleAndFieldFormat.class, typeDescriptor.getAnnotatedElement()); + Assertions.assertEquals(2, typeDescriptor.getAnnotationCount()); + + // then - field descriptors have correct names and elements + Head amountHead = property.getHeadMap().get(0); + AnnotatedFieldDescriptor amountDescriptor = amountHead.getFieldDescriptor(); + Assertions.assertEquals("amount", amountDescriptor.getFieldName()); + Assertions.assertEquals( + "amount", amountDescriptor.getAnnotatedElement().getName()); + Assertions.assertEquals(2, amountDescriptor.getAnnotationCount()); + + Head dateHead = property.getHeadMap().get(1); + AnnotatedFieldDescriptor dateDescriptor = dateHead.getFieldDescriptor(); + Assertions.assertEquals("date", dateDescriptor.getFieldName()); + Assertions.assertEquals("date", dateDescriptor.getAnnotatedElement().getName()); + Assertions.assertEquals(2, dateDescriptor.getAnnotationCount()); + } } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationAnnotations.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationAnnotations.java new file mode 100644 index 000000000..9e9d8186f --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationAnnotations.java @@ -0,0 +1,331 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation.composite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.FesodMarked; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.apache.fesod.sheet.enums.BooleanEnum; +import org.apache.fesod.sheet.enums.poi.FillPatternTypeEnum; +import org.apache.fesod.sheet.enums.poi.HorizontalAlignmentEnum; +import org.apache.fesod.sheet.enums.poi.VerticalAlignmentEnum; + +/** + * All composable (composite) annotation definitions used in integration tests. + * Each composable annotation bundles one or more inner annotations via {@link FesodMarked}. + */ +public class IntegrationAnnotations { + + private IntegrationAnnotations() {} + + // ---- Field-Level (or dual-target) Composable Annotations ---- + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ExcelProperty(value = "Name") + @Inherited + public @interface CompositeExcelProperty {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ExcelProperty + @Inherited + public @interface CompositeExcelPropertyAliasFor { + @FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value") + String[] value() default {""}; + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @DateTimeFormat(value = "yyyy-MM-dd") + @Inherited + public @interface CompositeDateTimeFormat {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @NumberFormat(value = "#,##0.00") + @Inherited + public @interface CompositeNumberFormat {} + + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ColumnWidth(value = 25) + @Inherited + public @interface CompositeColumnWidth {} + + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadStyle( + horizontalAlignment = HorizontalAlignmentEnum.CENTER, + fillForegroundColor = 42, + fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND) + @Inherited + public @interface CompositeHeadStyle {} + + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadFontStyle(bold = BooleanEnum.TRUE, fontHeightInPoints = 14) + @Inherited + public @interface CompositeHeadFontStyle {} + + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentStyle(wrapped = BooleanEnum.TRUE, verticalAlignment = VerticalAlignmentEnum.CENTER) + @Inherited + public @interface CompositeContentStyle {} + + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentFontStyle(italic = BooleanEnum.TRUE, fontName = "Arial") + @Inherited + public @interface CompositeContentFontStyle {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentLoopMerge(eachRow = 2, columnExtend = 1) + @Inherited + public @interface CompositeContentLoopMerge {} + + // ---- @AliasFor Composable Annotations ---- + + /** + * Aliases {@code width} to {@link ColumnWidth#value()} (different attribute name). + */ + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ColumnWidth + @Inherited + public @interface CompositeColumnWidthAliasFor { + @FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value") + int width() default -1; + } + + /** + * Aliases {@code pattern} to {@link DateTimeFormat#value()} (different attribute name). + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @DateTimeFormat + @Inherited + public @interface CompositeDateTimeFormatAliasFor { + @FesodMarked.AliasFor(annotation = DateTimeFormat.class, attribute = "value") + String pattern() default ""; + } + + /** + * Aliases {@code pattern} to {@link NumberFormat#value()} (different attribute name). + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @NumberFormat + @Inherited + public @interface CompositeNumberFormatAliasFor { + @FesodMarked.AliasFor(annotation = NumberFormat.class, attribute = "value") + String pattern() default ""; + } + + /** + * Multiple {@code @AliasFor} attributes mapping to the same inner annotation. + * Aliases {@code fontSize} → {@link HeadFontStyle#fontHeightInPoints()}, + * {@code fontColor} → {@link HeadFontStyle#color()}. + */ + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadFontStyle + @Inherited + public @interface CompositeHeadFontStyleAliasFor { + @FesodMarked.AliasFor(annotation = HeadFontStyle.class, attribute = "fontHeightInPoints") + short fontSize() default -1; + + @FesodMarked.AliasFor(annotation = HeadFontStyle.class, attribute = "color") + short fontColor() default -1; + } + + /** + * Aliases {@code alignment} to {@link HeadStyle#horizontalAlignment()}, + * {@code bgColor} to {@link HeadStyle#fillForegroundColor()}. + */ + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadStyle + @Inherited + public @interface CompositeHeadStyleAliasFor { + @FesodMarked.AliasFor(annotation = HeadStyle.class, attribute = "horizontalAlignment") + HorizontalAlignmentEnum alignment() default HorizontalAlignmentEnum.DEFAULT; + + @FesodMarked.AliasFor(annotation = HeadStyle.class, attribute = "fillForegroundColor") + short bgColor() default -1; + } + + /** + * Aliases {@code wrap} to {@link ContentStyle#wrapped()}, + * {@code vAlign} to {@link ContentStyle#verticalAlignment()}. + */ + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentStyle + @Inherited + public @interface CompositeContentStyleAliasFor { + @FesodMarked.AliasFor(annotation = ContentStyle.class, attribute = "wrapped") + BooleanEnum wrap() default BooleanEnum.DEFAULT; + + @FesodMarked.AliasFor(annotation = ContentStyle.class, attribute = "verticalAlignment") + VerticalAlignmentEnum vAlign() default VerticalAlignmentEnum.DEFAULT; + } + + /** + * Aliases {@code font} to {@link ContentFontStyle#fontName()}, + * {@code size} to {@link ContentFontStyle#fontHeightInPoints()}. + */ + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentFontStyle + @Inherited + public @interface CompositeContentFontStyleAliasFor { + @FesodMarked.AliasFor(annotation = ContentFontStyle.class, attribute = "fontName") + String font() default ""; + + @FesodMarked.AliasFor(annotation = ContentFontStyle.class, attribute = "fontHeightInPoints") + short size() default -1; + } + + /** + * Aliases {@code rows} to {@link ContentLoopMerge#eachRow()}, + * {@code cols} to {@link ContentLoopMerge#columnExtend()}. + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentLoopMerge + @Inherited + public @interface CompositeContentLoopMergeAliasFor { + @FesodMarked.AliasFor(annotation = ContentLoopMerge.class, attribute = "eachRow") + int rows() default 1; + + @FesodMarked.AliasFor(annotation = ContentLoopMerge.class, attribute = "columnExtend") + int cols() default 1; + } + + /** + * Aliases {@code height} to {@link HeadRowHeight#value()}. + */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadRowHeight + @Inherited + public @interface CompositeHeadRowHeightAliasFor { + @FesodMarked.AliasFor(annotation = HeadRowHeight.class, attribute = "value") + short height() default -1; + } + + /** + * Aliases {@code height} to {@link ContentRowHeight#value()}. + */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentRowHeight + @Inherited + public @interface CompositeContentRowHeightAliasFor { + @FesodMarked.AliasFor(annotation = ContentRowHeight.class, attribute = "value") + short height() default -1; + } + + /** + * Aliases {@code startRow} → {@link OnceAbsoluteMerge#firstRowIndex()}, + * {@code endRow} → {@link OnceAbsoluteMerge#lastRowIndex()}, + * {@code startCol} → {@link OnceAbsoluteMerge#firstColumnIndex()}, + * {@code endCol} → {@link OnceAbsoluteMerge#lastColumnIndex()}. + */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @OnceAbsoluteMerge + @Inherited + public @interface CompositeOnceAbsoluteMergeAliasFor { + @FesodMarked.AliasFor(annotation = OnceAbsoluteMerge.class, attribute = "firstRowIndex") + int startRow() default -1; + + @FesodMarked.AliasFor(annotation = OnceAbsoluteMerge.class, attribute = "lastRowIndex") + int endRow() default -1; + + @FesodMarked.AliasFor(annotation = OnceAbsoluteMerge.class, attribute = "firstColumnIndex") + int startCol() default -1; + + @FesodMarked.AliasFor(annotation = OnceAbsoluteMerge.class, attribute = "lastColumnIndex") + int endCol() default -1; + } + + // ---- Type-Level Composable Annotations ---- + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @HeadRowHeight(value = 40) + @Inherited + public @interface CompositeHeadRowHeight {} + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @ContentRowHeight(value = 30) + @Inherited + public @interface CompositeContentRowHeight {} + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @FesodMarked + @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 1) + @Inherited + public @interface CompositeOnceAbsoluteMerge {} +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java new file mode 100644 index 000000000..5f7cb4c88 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java @@ -0,0 +1,721 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation.composite; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.apache.fesod.sheet.FesodSheet; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Integration tests: composable annotations produce + * identical output to their equivalent direct annotations. + *

+ * Strategy: write both composite and direct models, then compare cell-by-cell + * via {@link WorkbookAsserts}. + */ +class IntegrationCompositeAnnotationTest { + + private static final String FILE_GROUPS_METHOD = + "org.apache.fesod.sheet.annotation.composite.IntegrationCompositeAnnotationTest#fileGroups"; + + @TempDir + static Path dir; + + private static File composite07; + private static File direct07; + private static File composite03; + private static File direct03; + + @BeforeAll + static void setup() { + composite07 = createTmpFile("composite07.xlsx"); + direct07 = createTmpFile("direct07.xlsx"); + composite03 = createTmpFile("composite03.xls"); + direct03 = createTmpFile("direct03.xls"); + } + + private static File createTmpFile(String filename) { + return new File(dir.resolve(filename).toString()); + } + + static Stream fileGroups() { + return Stream.of(Arguments.of(composite07, direct07), Arguments.of(composite03, direct03)); + } + + // ==================================================================== + // Helper + // ==================================================================== + + private void writeAndAssert( + File compositeFile, + File directFile, + Class compositeClass, + Class directClass, + List compositeData, + List directData) + throws Exception { + + FesodSheet.write(compositeFile, compositeClass) + .enableMetaMarked(true) + .sheet(0) + .doWrite(compositeData); + + FesodSheet.write(directFile, directClass) + .enableMetaMarked(false) + .sheet(0) + .doWrite(directData); + + WorkbookAsserts.assertWorkbooksMatch(compositeFile, directFile); + } + + // ==================================================================== + // Field-Level Tests + // ==================================================================== + + @Nested + @DisplayName("Field-level composable annotations") + class FieldLevelTests { + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingExcelProperty(File composite, File direct) throws Exception { + // Given: composable @ExcelProperty("Name") vs direct @ExcelProperty("Name") with identical data + List compositeData = + IntegrationExcelDatas.FieldExcelProperty.compositeData(); + List directData = + IntegrationExcelDatas.FieldExcelProperty.directData(); + + // When + Then: both outputs must be identical + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldExcelProperty.Composite.class, + IntegrationExcelDatas.FieldExcelProperty.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingDateTimeFormat(File composite, File direct) throws Exception { + // Given: composable @DateTimeFormat("yyyy-MM-dd") vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldDateTimeFormat.compositeData(); + List directData = + IntegrationExcelDatas.FieldDateTimeFormat.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldDateTimeFormat.Composite.class, + IntegrationExcelDatas.FieldDateTimeFormat.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingNumberFormat(File composite, File direct) throws Exception { + // Given: composable @NumberFormat("#,##0.00") vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldNumberFormat.compositeData(); + List directData = + IntegrationExcelDatas.FieldNumberFormat.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldNumberFormat.Composite.class, + IntegrationExcelDatas.FieldNumberFormat.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingColumnWidth(File composite, File direct) throws Exception { + // Given: composable @ColumnWidth(25) on field vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldColumnWidth.compositeData(); + List directData = + IntegrationExcelDatas.FieldColumnWidth.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldColumnWidth.Composite.class, + IntegrationExcelDatas.FieldColumnWidth.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadStyle(File composite, File direct) throws Exception { + // Given: composable @HeadStyle(...) on field vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldHeadStyle.compositeData(); + List directData = + IntegrationExcelDatas.FieldHeadStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldHeadStyle.Composite.class, + IntegrationExcelDatas.FieldHeadStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadFontStyle(File composite, File direct) throws Exception { + // Given: composable @HeadFontStyle(bold=true, fontHeight=14) on field vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldHeadFontStyle.compositeData(); + List directData = + IntegrationExcelDatas.FieldHeadFontStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldHeadFontStyle.Composite.class, + IntegrationExcelDatas.FieldHeadFontStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentStyle(File composite, File direct) throws Exception { + // Given: composable @ContentStyle(wrapped=true, verticalAlignment=CENTER) on field vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldContentStyle.compositeData(); + List directData = + IntegrationExcelDatas.FieldContentStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldContentStyle.Composite.class, + IntegrationExcelDatas.FieldContentStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentFontStyle(File composite, File direct) throws Exception { + // Given: composable @ContentFontStyle(italic=true, fontName="Arial") on field vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldContentFontStyle.compositeData(); + List directData = + IntegrationExcelDatas.FieldContentFontStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldContentFontStyle.Composite.class, + IntegrationExcelDatas.FieldContentFontStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentLoopMerge(File composite, File direct) throws Exception { + // Given: composable @ContentLoopMerge(eachRow=2, columnExtend=1) on field vs direct equivalent + List compositeData = + IntegrationExcelDatas.FieldContentLoopMerge.compositeData(); + List directData = + IntegrationExcelDatas.FieldContentLoopMerge.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.FieldContentLoopMerge.Composite.class, + IntegrationExcelDatas.FieldContentLoopMerge.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingExcelPropertyAliasFor(File composite, File direct) throws Exception { + // Given: @AliasFor(value) → @ExcelProperty("Custom Name") — same attribute name + List compositeData = + IntegrationExcelDatas.AliasForExcelProperty.compositeData(); + List directData = + IntegrationExcelDatas.AliasForExcelProperty.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForExcelProperty.Composite.class, + IntegrationExcelDatas.AliasForExcelProperty.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingColumnWidthAliasFor(File composite, File direct) throws Exception { + // Given: @AliasFor(width=20) → @ColumnWidth(20) — renamed attribute + List compositeData = + IntegrationExcelDatas.AliasForColumnWidth.compositeData(); + List directData = + IntegrationExcelDatas.AliasForColumnWidth.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForColumnWidth.Composite.class, + IntegrationExcelDatas.AliasForColumnWidth.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingDateTimeFormatAliasFor(File composite, File direct) + throws Exception { + // Given: @AliasFor(pattern="yyyy/MM/dd") → @DateTimeFormat("yyyy/MM/dd") — renamed attribute + List compositeData = + IntegrationExcelDatas.AliasForDateTimeFormat.compositeData(); + List directData = + IntegrationExcelDatas.AliasForDateTimeFormat.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForDateTimeFormat.Composite.class, + IntegrationExcelDatas.AliasForDateTimeFormat.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingNumberFormatAliasFor(File composite, File direct) throws Exception { + // Given: @AliasFor(pattern="0.00%") → @NumberFormat("0.00%") — renamed attribute + List compositeData = + IntegrationExcelDatas.AliasForNumberFormat.compositeData(); + List directData = + IntegrationExcelDatas.AliasForNumberFormat.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForNumberFormat.Composite.class, + IntegrationExcelDatas.AliasForNumberFormat.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadStyleAliasFor(File composite, File direct) throws Exception { + // Given: multiple @AliasFor: alignment=RIGHT → horizontalAlignment, bgColor=13 → fillForegroundColor + List compositeData = + IntegrationExcelDatas.AliasForHeadStyle.compositeData(); + List directData = + IntegrationExcelDatas.AliasForHeadStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForHeadStyle.Composite.class, + IntegrationExcelDatas.AliasForHeadStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadFontStyleAliasFor(File composite, File direct) throws Exception { + // Given: multiple @AliasFor: fontSize=16 → fontHeightInPoints, fontColor=10 → color + List compositeData = + IntegrationExcelDatas.AliasForHeadFontStyle.compositeData(); + List directData = + IntegrationExcelDatas.AliasForHeadFontStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForHeadFontStyle.Composite.class, + IntegrationExcelDatas.AliasForHeadFontStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentStyleAliasFor(File composite, File direct) throws Exception { + // Given: multiple @AliasFor: wrap=TRUE → wrapped, vAlign=CENTER → verticalAlignment + List compositeData = + IntegrationExcelDatas.AliasForContentStyle.compositeData(); + List directData = + IntegrationExcelDatas.AliasForContentStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForContentStyle.Composite.class, + IntegrationExcelDatas.AliasForContentStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentFontStyleAliasFor(File composite, File direct) + throws Exception { + // Given: multiple @AliasFor: font="Courier New" → fontName, size=18 → fontHeightInPoints + List compositeData = + IntegrationExcelDatas.AliasForContentFontStyle.compositeData(); + List directData = + IntegrationExcelDatas.AliasForContentFontStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForContentFontStyle.Composite.class, + IntegrationExcelDatas.AliasForContentFontStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentLoopMergeAliasFor(File composite, File direct) + throws Exception { + // Given: multiple @AliasFor: rows=3 → eachRow, cols=1 → columnExtend + List compositeData = + IntegrationExcelDatas.AliasForContentLoopMerge.compositeData(); + List directData = + IntegrationExcelDatas.AliasForContentLoopMerge.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForContentLoopMerge.Composite.class, + IntegrationExcelDatas.AliasForContentLoopMerge.Direct.class, + compositeData, + directData); + } + } + + // ==================================================================== + // Class-Level Tests + // ==================================================================== + + @Nested + @DisplayName("Class-level composable annotations") + class ClassLevelTests { + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingColumnWidth(File composite, File direct) throws Exception { + // Given: composable @ColumnWidth(25) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassColumnWidth.compositeData(); + List directData = + IntegrationExcelDatas.ClassColumnWidth.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassColumnWidth.Composite.class, + IntegrationExcelDatas.ClassColumnWidth.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadStyle(File composite, File direct) throws Exception { + // Given: composable @HeadStyle(...) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassHeadStyle.compositeData(); + List directData = + IntegrationExcelDatas.ClassHeadStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassHeadStyle.Composite.class, + IntegrationExcelDatas.ClassHeadStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadFontStyle(File composite, File direct) throws Exception { + // Given: composable @HeadFontStyle(...) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassHeadFontStyle.compositeData(); + List directData = + IntegrationExcelDatas.ClassHeadFontStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassHeadFontStyle.Composite.class, + IntegrationExcelDatas.ClassHeadFontStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentStyle(File composite, File direct) throws Exception { + // Given: composable @ContentStyle(...) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassContentStyle.compositeData(); + List directData = + IntegrationExcelDatas.ClassContentStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassContentStyle.Composite.class, + IntegrationExcelDatas.ClassContentStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentFontStyle(File composite, File direct) throws Exception { + // Given: composable @ContentFontStyle(...) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassContentFontStyle.compositeData(); + List directData = + IntegrationExcelDatas.ClassContentFontStyle.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassContentFontStyle.Composite.class, + IntegrationExcelDatas.ClassContentFontStyle.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadRowHeight(File composite, File direct) throws Exception { + // Given: composable @HeadRowHeight(40) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassHeadRowHeight.compositeData(); + List directData = + IntegrationExcelDatas.ClassHeadRowHeight.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassHeadRowHeight.Composite.class, + IntegrationExcelDatas.ClassHeadRowHeight.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentRowHeight(File composite, File direct) throws Exception { + // Given: composable @ContentRowHeight(30) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassContentRowHeight.compositeData(); + List directData = + IntegrationExcelDatas.ClassContentRowHeight.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassContentRowHeight.Composite.class, + IntegrationExcelDatas.ClassContentRowHeight.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingOnceAbsoluteMerge(File composite, File direct) throws Exception { + // Given: composable @OnceAbsoluteMerge(...) on class vs direct equivalent + List compositeData = + IntegrationExcelDatas.ClassOnceAbsoluteMerge.compositeData(); + List directData = + IntegrationExcelDatas.ClassOnceAbsoluteMerge.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.ClassOnceAbsoluteMerge.Composite.class, + IntegrationExcelDatas.ClassOnceAbsoluteMerge.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingHeadRowHeightAliasFor(File composite, File direct) throws Exception { + // Given: @AliasFor(height=50) → @HeadRowHeight(50) — renamed attribute + List compositeData = + IntegrationExcelDatas.AliasForHeadRowHeight.compositeData(); + List directData = + IntegrationExcelDatas.AliasForHeadRowHeight.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForHeadRowHeight.Composite.class, + IntegrationExcelDatas.AliasForHeadRowHeight.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingContentRowHeightAliasFor(File composite, File direct) + throws Exception { + // Given: @AliasFor(height=35) → @ContentRowHeight(35) — renamed attribute + List compositeData = + IntegrationExcelDatas.AliasForContentRowHeight.compositeData(); + List directData = + IntegrationExcelDatas.AliasForContentRowHeight.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForContentRowHeight.Composite.class, + IntegrationExcelDatas.AliasForContentRowHeight.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingOnceAbsoluteMergeAliasFor(File composite, File direct) + throws Exception { + // Given: 4 @AliasFor: startRow/endRow/startCol/endCol → + // firstRowIndex/lastRowIndex/firstColumnIndex/lastColumnIndex + List compositeData = + IntegrationExcelDatas.AliasForOnceAbsoluteMerge.compositeData(); + List directData = + IntegrationExcelDatas.AliasForOnceAbsoluteMerge.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.AliasForOnceAbsoluteMerge.Composite.class, + IntegrationExcelDatas.AliasForOnceAbsoluteMerge.Direct.class, + compositeData, + directData); + } + } + + // ==================================================================== + // Mixed-Level Tests + // ==================================================================== + + @Nested + @DisplayName("Mixed-level composable annotations (class + field)") + class MixedLevelTests { + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenUsingMixedAnnotations(File composite, File direct) throws Exception { + // Given: composable @HeadRowHeight + @ContentRowHeight on class, + // @ExcelProperty + @ColumnWidth + @HeadStyle on field "name", + // @ExcelProperty + @ContentFontStyle on field "value" + List compositeData = + IntegrationExcelDatas.MixedAll.compositeData(); + List directData = IntegrationExcelDatas.MixedAll.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.MixedAll.Composite.class, + IntegrationExcelDatas.MixedAll.Direct.class, + compositeData, + directData); + } + + @ParameterizedTest + @MethodSource(FILE_GROUPS_METHOD) + void shouldProduceIdenticalOutput_whenDirectAnnotationOverridesComposite(File composite, File direct) + throws Exception { + // Given: field has both @CompositeExcelPropertyAliasFor(value="Value") and @ExcelProperty("Final Value") + // Direct annotation has higher priority (smaller distance), so final header is "Final Value" + List compositeData = + IntegrationExcelDatas.PriorityDirectOverComposite.compositeData(); + List directData = + IntegrationExcelDatas.PriorityDirectOverComposite.directData(); + + // When + Then + writeAndAssert( + composite, + direct, + IntegrationExcelDatas.PriorityDirectOverComposite.Composite.class, + IntegrationExcelDatas.PriorityDirectOverComposite.Direct.class, + compositeData, + directData); + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationExcelDatas.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationExcelDatas.java new file mode 100644 index 000000000..ce7f38d69 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationExcelDatas.java @@ -0,0 +1,1298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation.composite; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.Data; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.apache.fesod.sheet.enums.BooleanEnum; +import org.apache.fesod.sheet.enums.poi.FillPatternTypeEnum; +import org.apache.fesod.sheet.enums.poi.HorizontalAlignmentEnum; +import org.apache.fesod.sheet.enums.poi.VerticalAlignmentEnum; + +/** + * All model objects (test data) for integration tests. + * Each group provides a Composite (composable annotations) and Direct (direct annotations) + * model class with equivalent annotation semantics, plus matching data generators. + */ +public class IntegrationExcelDatas { + + private IntegrationExcelDatas() {} + + private static final int ROW_COUNT = 5; + + private static Date dateOf(int year, int month, int day) { + return Date.from(LocalDate.of(year, month, day) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + } + + // ==================================================================== + // Field-Level Models + // ==================================================================== + + // ---- ExcelProperty ---- + + public static class FieldExcelProperty { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- DateTimeFormat ---- + + public static class FieldDateTimeFormat { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeDateTimeFormat + private Date date; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @DateTimeFormat("yyyy-MM-dd") + private Date date; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setDate(dateOf(2026, 1, i + 1)); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setDate(dateOf(2026, 1, i + 1)); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- NumberFormat ---- + + public static class FieldNumberFormat { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeNumberFormat + private BigDecimal amount; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @NumberFormat("#,##0.00") + private BigDecimal amount; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setAmount(BigDecimal.valueOf(100.0 + i * 10.5)); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setAmount(BigDecimal.valueOf(100.0 + i * 10.5)); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ColumnWidth (field-level) ---- + + public static class FieldColumnWidth { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeColumnWidth + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ColumnWidth(25) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadStyle (field-level) ---- + + public static class FieldHeadStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeHeadStyle + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @HeadStyle( + horizontalAlignment = HorizontalAlignmentEnum.CENTER, + fillForegroundColor = 42, + fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadFontStyle (field-level) ---- + + public static class FieldHeadFontStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeHeadFontStyle + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @HeadFontStyle(bold = BooleanEnum.TRUE, fontHeightInPoints = 14) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentStyle (field-level) ---- + + public static class FieldContentStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeContentStyle + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ContentStyle(wrapped = BooleanEnum.TRUE, verticalAlignment = VerticalAlignmentEnum.CENTER) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentFontStyle (field-level) ---- + + public static class FieldContentFontStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeContentFontStyle + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ContentFontStyle(italic = BooleanEnum.TRUE, fontName = "Arial") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentLoopMerge (field-level) ---- + + public static class FieldContentLoopMerge { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeContentLoopMerge + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ContentLoopMerge(eachRow = 2, columnExtend = 1) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ==================================================================== + // Class-Level Models + // ==================================================================== + + // ---- ColumnWidth (class-level) ---- + + public static class ClassColumnWidth { + @IntegrationAnnotations.CompositeColumnWidth + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @ColumnWidth(25) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadStyle (class-level) ---- + + public static class ClassHeadStyle { + @IntegrationAnnotations.CompositeHeadStyle + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @HeadStyle( + horizontalAlignment = HorizontalAlignmentEnum.CENTER, + fillForegroundColor = 42, + fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadFontStyle (class-level) ---- + + public static class ClassHeadFontStyle { + @IntegrationAnnotations.CompositeHeadFontStyle + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @HeadFontStyle(bold = BooleanEnum.TRUE, fontHeightInPoints = 14) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentStyle (class-level) ---- + + public static class ClassContentStyle { + @IntegrationAnnotations.CompositeContentStyle + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @ContentStyle(wrapped = BooleanEnum.TRUE, verticalAlignment = VerticalAlignmentEnum.CENTER) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentFontStyle (class-level) ---- + + public static class ClassContentFontStyle { + @IntegrationAnnotations.CompositeContentFontStyle + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @ContentFontStyle(italic = BooleanEnum.TRUE, fontName = "Arial") + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadRowHeight (class-level) ---- + + public static class ClassHeadRowHeight { + @IntegrationAnnotations.CompositeHeadRowHeight + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @HeadRowHeight(40) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentRowHeight (class-level) ---- + + public static class ClassContentRowHeight { + @IntegrationAnnotations.CompositeContentRowHeight + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @ContentRowHeight(30) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- OnceAbsoluteMerge (class-level) ---- + + public static class ClassOnceAbsoluteMerge { + @IntegrationAnnotations.CompositeOnceAbsoluteMerge + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + + @IntegrationAnnotations.CompositeExcelPropertyAliasFor(value = "Value") + private String value; + } + + @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 1) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + + @ExcelProperty("Value") + private String value; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + c.setValue("Value" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + d.setValue("Value" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ==================================================================== + // @AliasFor Models + // ==================================================================== + + // ---- ExcelProperty via AliasFor (same attribute name: value → value) ---- + + public static class AliasForExcelProperty { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelPropertyAliasFor(value = "Custom Name") + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Custom Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ColumnWidth via AliasFor (different name: width → value) ---- + + public static class AliasForColumnWidth { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeColumnWidthAliasFor(width = 20) + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ColumnWidth(20) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- DateTimeFormat via AliasFor (different name: pattern → value) ---- + + public static class AliasForDateTimeFormat { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeDateTimeFormatAliasFor(pattern = "yyyy/MM/dd") + private Date date; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @DateTimeFormat("yyyy/MM/dd") + private Date date; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setDate(dateOf(2026, 1, i + 1)); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setDate(dateOf(2026, 1, i + 1)); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- NumberFormat via AliasFor (different name: pattern → value) ---- + + public static class AliasForNumberFormat { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeNumberFormatAliasFor(pattern = "0.00%") + private BigDecimal amount; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @NumberFormat("0.00%") + private BigDecimal amount; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setAmount(BigDecimal.valueOf(0.5 + i * 0.1)); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setAmount(BigDecimal.valueOf(0.5 + i * 0.1)); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadFontStyle via multiple AliasFor (fontSize → fontHeightInPoints, fontColor → color) ---- + + public static class AliasForHeadFontStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeHeadFontStyleAliasFor(fontSize = 16, fontColor = 10) + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @HeadFontStyle(fontHeightInPoints = 16, color = 10) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadStyle via AliasFor (alignment → horizontalAlignment, bgColor → fillForegroundColor) ---- + + public static class AliasForHeadStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeHeadStyleAliasFor(alignment = HorizontalAlignmentEnum.RIGHT, bgColor = 13) + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.RIGHT, fillForegroundColor = 13) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentStyle via AliasFor (wrap → wrapped, vAlign → verticalAlignment) ---- + + public static class AliasForContentStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeContentStyleAliasFor( + wrap = BooleanEnum.TRUE, + vAlign = VerticalAlignmentEnum.CENTER) + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ContentStyle(wrapped = BooleanEnum.TRUE, verticalAlignment = VerticalAlignmentEnum.CENTER) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentFontStyle via AliasFor (font → fontName, size → fontHeightInPoints) ---- + + public static class AliasForContentFontStyle { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeContentFontStyleAliasFor(font = "Courier New", size = 18) + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ContentFontStyle(fontName = "Courier New", fontHeightInPoints = 18) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentLoopMerge via AliasFor (rows → eachRow, cols → columnExtend) ---- + + public static class AliasForContentLoopMerge { + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeContentLoopMergeAliasFor(rows = 3, cols = 1) + private String name; + } + + @Data + public static class Direct { + @ExcelProperty("Name") + @ContentLoopMerge(eachRow = 3, columnExtend = 1) + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- HeadRowHeight via AliasFor (height → value) ---- + + public static class AliasForHeadRowHeight { + @IntegrationAnnotations.CompositeHeadRowHeightAliasFor(height = 50) + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @HeadRowHeight(50) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- ContentRowHeight via AliasFor (height → value) ---- + + public static class AliasForContentRowHeight { + @IntegrationAnnotations.CompositeContentRowHeightAliasFor(height = 35) + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + } + + @ContentRowHeight(35) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ---- OnceAbsoluteMerge via AliasFor (startRow/endRow/startCol/endCol → 4 inner attrs) ---- + + public static class AliasForOnceAbsoluteMerge { + @IntegrationAnnotations.CompositeOnceAbsoluteMergeAliasFor(startRow = 0, endRow = 1, startCol = 0, endCol = 1) + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + private String name; + + @IntegrationAnnotations.CompositeExcelPropertyAliasFor(value = "Value") + private String value; + } + + @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 1, firstColumnIndex = 0, lastColumnIndex = 1) + @Data + public static class Direct { + @ExcelProperty("Name") + private String name; + + @ExcelProperty("Value") + private String value; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + c.setValue("Value" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + d.setValue("Value" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + // ==================================================================== + // Mixed-Level Model + // ==================================================================== + + // ---- Direct annotation overrides composite at same level ---- + + public static class PriorityDirectOverComposite { + /** + * Field has BOTH composite and direct annotations. + * Direct {@code @ExcelProperty("Final Value")} should win over + * composite's aliased value "Value". + */ + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelPropertyAliasFor(value = "Value") + @ExcelProperty(value = "Final Value") + private String name; + } + + @Data + public static class Direct { + @ExcelProperty(value = "Final Value") + private String name; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + return d; + }) + .collect(Collectors.toList()); + } + } + + public static class MixedAll { + @IntegrationAnnotations.CompositeHeadRowHeight + @IntegrationAnnotations.CompositeContentRowHeight + @Data + public static class Composite { + @IntegrationAnnotations.CompositeExcelProperty + @IntegrationAnnotations.CompositeColumnWidth + @IntegrationAnnotations.CompositeHeadStyle + private String name; + + @IntegrationAnnotations.CompositeExcelPropertyAliasFor(value = "Value") + @IntegrationAnnotations.CompositeContentFontStyle + private String value; + } + + @HeadRowHeight(40) + @ContentRowHeight(30) + @Data + public static class Direct { + @ExcelProperty("Name") + @ColumnWidth(25) + @HeadStyle( + horizontalAlignment = HorizontalAlignmentEnum.CENTER, + fillForegroundColor = 42, + fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND) + private String name; + + @ExcelProperty("Value") + @ContentFontStyle(italic = BooleanEnum.TRUE, fontName = "Arial") + private String value; + } + + public static List compositeData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Composite c = new Composite(); + c.setName("Name" + i); + c.setValue("Value" + i); + return c; + }) + .collect(Collectors.toList()); + } + + public static List directData() { + return IntStream.range(0, ROW_COUNT) + .mapToObj(i -> { + Direct d = new Direct(); + d.setName("Name" + i); + d.setValue("Value" + i); + return d; + }) + .collect(Collectors.toList()); + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java new file mode 100644 index 000000000..b1640641f --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.annotation.composite; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import java.io.File; +import java.util.List; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; + +/** + * Compares two Excel workbooks cell-by-cell for integration testing of composable annotations. + *

+ * Verifies: cell values, cell styles (alignment, fill, border, data format), + * fonts (name, height, bold, italic, color, etc.), column widths, row heights, + * and merged regions. + */ +class WorkbookAsserts { + + static void assertWorkbooksMatch(File expected, File actual) { + try (Workbook wb1 = WorkbookFactory.create(expected); + Workbook wb2 = WorkbookFactory.create(actual)) { + assertWorkbooksMatch(wb1, wb2); + } catch (AssertionError e) { + throw e; + } catch (Exception e) { + fail("Failed to read workbooks: " + e.getMessage(), e); + } + } + + static void assertWorkbooksMatch(Workbook expected, Workbook actual) { + assertEquals(expected.getNumberOfSheets(), actual.getNumberOfSheets(), "Sheet count mismatch"); + for (int i = 0; i < expected.getNumberOfSheets(); i++) { + assertSheetMatches(expected.getSheetAt(i), actual.getSheetAt(i), i); + } + } + + // ---- Sheet ---- + + private static void assertSheetMatches(Sheet expected, Sheet actual, int sheetIdx) { + String ctx = "Sheet[" + sheetIdx + "]"; + + // Merged regions + List expMerged = expected.getMergedRegions(); + List actMerged = actual.getMergedRegions(); + assertEquals(expMerged.size(), actMerged.size(), ctx + " merged region count"); + for (int i = 0; i < expMerged.size(); i++) { + assertEquals( + expMerged.get(i).formatAsString(), + actMerged.get(i).formatAsString(), + ctx + " merged region[" + i + "]"); + } + + // Rows + int lastRow = Math.max(expected.getLastRowNum(), actual.getLastRowNum()); + for (int r = 0; r <= lastRow; r++) { + assertRowMatches(expected.getRow(r), actual.getRow(r), sheetIdx, r); + } + + // Column widths + int maxCol = Math.max(maxColumnIndex(expected), maxColumnIndex(actual)); + for (int c = 0; c <= maxCol; c++) { + assertEquals(expected.getColumnWidth(c), actual.getColumnWidth(c), ctx + " column[" + c + "] width"); + } + } + + // ---- Row ---- + + private static void assertRowMatches(Row expected, Row actual, int sheetIdx, int rowIdx) { + String ctx = "Sheet[" + sheetIdx + "].Row[" + rowIdx + "]"; + if (expected == null && actual == null) { + return; + } + assertNotNull(expected, ctx + " missing in expected"); + assertNotNull(actual, ctx + " missing in actual"); + + assertEquals(expected.getHeightInPoints(), actual.getHeightInPoints(), ctx + " heightInPoints"); + + short maxCell = (short) Math.max( + expected.getLastCellNum() < 0 ? 0 : expected.getLastCellNum(), + actual.getLastCellNum() < 0 ? 0 : actual.getLastCellNum()); + for (int c = 0; c < maxCell; c++) { + assertCellMatches( + expected.getCell(c), + actual.getCell(c), + sheetIdx, + rowIdx, + c, + expected.getSheet().getWorkbook(), + actual.getSheet().getWorkbook()); + } + } + + // ---- Cell ---- + + private static void assertCellMatches( + Cell expected, Cell actual, int sheetIdx, int rowIdx, int colIdx, Workbook expWb, Workbook actWb) { + String ctx = "Cell[" + rowIdx + "," + colIdx + "]"; + if (expected == null && actual == null) { + return; + } + assertNotNull(expected, ctx + " missing in expected"); + assertNotNull(actual, ctx + " missing in actual"); + + // Type + assertEquals(expected.getCellType(), actual.getCellType(), ctx + " type"); + + // Value + switch (expected.getCellType()) { + case STRING: + assertEquals(expected.getStringCellValue(), actual.getStringCellValue(), ctx + " value"); + break; + case NUMERIC: + assertEquals(expected.getNumericCellValue(), actual.getNumericCellValue(), ctx + " value"); + break; + case BOOLEAN: + assertEquals(expected.getBooleanCellValue(), actual.getBooleanCellValue(), ctx + " value"); + break; + case FORMULA: + assertEquals(expected.getCellFormula(), actual.getCellFormula(), ctx + " formula"); + break; + default: + break; + } + + // Style + assertStyleMatches(expected.getCellStyle(), actual.getCellStyle(), expWb, actWb, ctx); + } + + // ---- CellStyle ---- + + private static void assertStyleMatches( + CellStyle expected, CellStyle actual, Workbook expWb, Workbook actWb, String ctx) { + // Alignment + assertEquals(expected.getAlignment(), actual.getAlignment(), ctx + " alignment"); + assertEquals(expected.getVerticalAlignment(), actual.getVerticalAlignment(), ctx + " verticalAlignment"); + assertEquals(expected.getWrapText(), actual.getWrapText(), ctx + " wrapText"); + assertEquals(expected.getRotation(), actual.getRotation(), ctx + " rotation"); + assertEquals(expected.getIndention(), actual.getIndention(), ctx + " indent"); + + // Visibility / protection + assertEquals(expected.getHidden(), actual.getHidden(), ctx + " hidden"); + assertEquals(expected.getLocked(), actual.getLocked(), ctx + " locked"); + assertEquals(expected.getShrinkToFit(), actual.getShrinkToFit(), ctx + " shrinkToFit"); + + // Borders + assertEquals(expected.getBorderLeft(), actual.getBorderLeft(), ctx + " borderLeft"); + assertEquals(expected.getBorderRight(), actual.getBorderRight(), ctx + " borderRight"); + assertEquals(expected.getBorderTop(), actual.getBorderTop(), ctx + " borderTop"); + assertEquals(expected.getBorderBottom(), actual.getBorderBottom(), ctx + " borderBottom"); + assertEquals(expected.getLeftBorderColor(), actual.getLeftBorderColor(), ctx + " leftBorderColor"); + assertEquals(expected.getRightBorderColor(), actual.getRightBorderColor(), ctx + " rightBorderColor"); + assertEquals(expected.getTopBorderColor(), actual.getTopBorderColor(), ctx + " topBorderColor"); + assertEquals(expected.getBottomBorderColor(), actual.getBottomBorderColor(), ctx + " bottomBorderColor"); + + // Fill + assertEquals(expected.getFillPattern(), actual.getFillPattern(), ctx + " fillPattern"); + assertEquals(expected.getFillForegroundColor(), actual.getFillForegroundColor(), ctx + " fillForegroundColor"); + assertEquals(expected.getFillBackgroundColor(), actual.getFillBackgroundColor(), ctx + " fillBackgroundColor"); + + // Data format + assertEquals(expected.getDataFormat(), actual.getDataFormat(), ctx + " dataFormat"); + + // Font + assertFontMatches(expWb.getFontAt(expected.getFontIndex()), actWb.getFontAt(actual.getFontIndex()), ctx); + } + + // ---- Font ---- + + private static void assertFontMatches(Font expected, Font actual, String ctx) { + assertEquals(expected.getFontName(), actual.getFontName(), ctx + " fontName"); + assertEquals(expected.getFontHeightInPoints(), actual.getFontHeightInPoints(), ctx + " fontHeightInPoints"); + assertEquals(expected.getBold(), actual.getBold(), ctx + " bold"); + assertEquals(expected.getItalic(), actual.getItalic(), ctx + " italic"); + assertEquals(expected.getColor(), actual.getColor(), ctx + " color"); + assertEquals(expected.getUnderline(), actual.getUnderline(), ctx + " underline"); + assertEquals(expected.getStrikeout(), actual.getStrikeout(), ctx + " strikeout"); + assertEquals(expected.getTypeOffset(), actual.getTypeOffset(), ctx + " typeOffset"); + } + + // ---- Helpers ---- + + private static int maxColumnIndex(Sheet sheet) { + int max = 0; + for (Row row : sheet) { + if (row.getLastCellNum() > max) { + max = row.getLastCellNum(); + } + } + return max; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/cache/CacheDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/cache/CacheDataTest.java index 3899ba8c4..b9c9fa764 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/cache/CacheDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/cache/CacheDataTest.java @@ -34,14 +34,14 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.annotation.AnnotatedFieldDescriptor; import org.apache.fesod.sheet.annotation.ExcelProperty; import org.apache.fesod.sheet.context.AnalysisContext; import org.apache.fesod.sheet.data.DemoData; import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.event.AnalysisEventListener; -import org.apache.fesod.sheet.metadata.FieldCache; import org.apache.fesod.sheet.read.listener.PageReadListener; -import org.apache.fesod.sheet.util.ClassUtils; +import org.apache.fesod.sheet.util.AnnotatedClassUtils; import org.apache.fesod.sheet.util.FieldUtils; import org.apache.fesod.sheet.util.TestFileUtil; import org.junit.jupiter.api.Assertions; @@ -74,9 +74,10 @@ public static void init() { @Test public void t01ReadAndWrite() throws Exception { - Field field = FieldUtils.getField(ClassUtils.class, "FIELD_THREAD_LOCAL", true); - ThreadLocal, FieldCache>> fieldThreadLocal = - (ThreadLocal, FieldCache>>) field.get(ClassUtils.class.newInstance()); + Field field = FieldUtils.getField(AnnotatedClassUtils.class, "FIELD_THREAD_LOCAL", true); + ThreadLocal, AnnotatedFieldDescriptor>> fieldThreadLocal = + (ThreadLocal, AnnotatedFieldDescriptor>>) + field.get(AnnotatedClassUtils.class.newInstance()); Assertions.assertNull(fieldThreadLocal.get()); FesodSheet.write(file07, CacheData.class).sheet().doWrite(data()); FesodSheet.read(file07, CacheData.class, new PageReadListener(dataList -> { From 19466894ecda61b7a4026bd21096e568c89892a3 Mon Sep 17 00:00:00 2001 From: Bengbengbalabalabeng Date: Wed, 20 May 2026 16:51:24 +0800 Subject: [PATCH 3/3] test: refactor the test assertion method --- .../IntegrationCompositeAnnotationTest.java | 1248 ++++++++++++++++- .../annotation/composite/WorkbookAsserts.java | 198 +-- 2 files changed, 1199 insertions(+), 247 deletions(-) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java index 5f7cb4c88..6f65d80cf 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java @@ -21,9 +21,35 @@ import java.io.File; import java.nio.file.Path; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.stream.Stream; import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ColumnWidth; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentLoopMerge; +import org.apache.fesod.sheet.annotation.write.style.ContentRowHeight; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadFontStyle; +import org.apache.fesod.sheet.annotation.write.style.HeadRowHeight; +import org.apache.fesod.sheet.annotation.write.style.HeadStyle; +import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -35,9 +61,28 @@ /** * Integration tests: composable annotations produce * identical output to their equivalent direct annotations. - *

- * Strategy: write both composite and direct models, then compare cell-by-cell - * via {@link WorkbookAsserts}. + *

+ * Covered inner annotations: + *

    + *
  • {@link ExcelProperty}
  • + *
  • {@link DateTimeFormat}
  • + *
  • {@link NumberFormat}
  • + *
  • {@link ColumnWidth}
  • + *
  • {@link ContentFontStyle}
  • + *
  • {@link ContentLoopMerge}
  • + *
  • {@link ContentRowHeight}
  • + *
  • {@link ContentStyle}
  • + *
  • {@link HeadFontStyle}
  • + *
  • {@link HeadRowHeight}
  • + *
  • {@link HeadStyle}
  • + *
  • {@link OnceAbsoluteMerge}
  • + *
+ * Covered test scenarios: + *
    + *
  • Field-level composable/direct
  • + *
  • Class-level composable/direct
  • + *
  • Mixed-level composable/direct
  • + *
*/ class IntegrationCompositeAnnotationTest { @@ -72,7 +117,7 @@ static Stream fileGroups() { // Helper // ==================================================================== - private void writeAndAssert( + private void doWrite( File compositeFile, File directFile, Class compositeClass, @@ -81,17 +126,35 @@ private void writeAndAssert( List directData) throws Exception { - FesodSheet.write(compositeFile, compositeClass) - .enableMetaMarked(true) - .sheet(0) - .doWrite(compositeData); + try { + FesodSheet.write(compositeFile, compositeClass) + .enableMetaMarked(true) + .sheet(0) + .doWrite(compositeData); + + FesodSheet.write(directFile, directClass) + .enableMetaMarked(false) + .sheet(0) + .doWrite(directData); + } catch (Exception ex) { + Assertions.fail("Data write failed.", ex); + } + } - FesodSheet.write(directFile, directClass) - .enableMetaMarked(false) - .sheet(0) - .doWrite(directData); + private void assertHeadNames(Row head, List headNames, String label) { + Assertions.assertNotNull(headNames); + for (int i = 0; i < headNames.size(); i++) { + String actual = head.getCell(i).getStringCellValue(); + String expected = headNames.get(i); + Assertions.assertEquals( + expected, actual, "[" + label + "] The header of column [" + i + "] is unexpected."); + } + } - WorkbookAsserts.assertWorkbooksMatch(compositeFile, directFile); + private Date dateOf(int year, int month, int day) { + return Date.from(LocalDate.of(year, month, day) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); } // ==================================================================== @@ -111,14 +174,39 @@ void shouldProduceIdenticalOutput_whenUsingExcelProperty(File composite, File di List directData = IntegrationExcelDatas.FieldExcelProperty.directData(); - // When + Then: both outputs must be identical - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldExcelProperty.Composite.class, IntegrationExcelDatas.FieldExcelProperty.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -130,14 +218,43 @@ void shouldProduceIdenticalOutput_whenUsingDateTimeFormat(File composite, File d List directData = IntegrationExcelDatas.FieldDateTimeFormat.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldDateTimeFormat.Composite.class, IntegrationExcelDatas.FieldDateTimeFormat.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.NUMERIC, data.getCell(0).getCellType()); + Assertions.assertEquals( + dateOf(2026, 1, i + 1), + data.getCell(0).getDateCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + "yyyy-MM-dd", + data.getCell(0).getCellStyle().getDataFormatString(), + "[" + label + "] The data format of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -149,14 +266,43 @@ void shouldProduceIdenticalOutput_whenUsingNumberFormat(File composite, File dir List directData = IntegrationExcelDatas.FieldNumberFormat.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldNumberFormat.Composite.class, IntegrationExcelDatas.FieldNumberFormat.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.NUMERIC, data.getCell(0).getCellType()); + Assertions.assertEquals( + 100.0 + i * 10.5, + data.getCell(0).getNumericCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + "#,##0.00", + data.getCell(0).getCellStyle().getDataFormatString(), + "[" + label + "] The data format of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -168,14 +314,43 @@ void shouldProduceIdenticalOutput_whenUsingColumnWidth(File composite, File dire List directData = IntegrationExcelDatas.FieldColumnWidth.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldColumnWidth.Composite.class, IntegrationExcelDatas.FieldColumnWidth.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + 25 * 256, + sheet.getColumnWidth(0), + "[" + label + "] The column width of column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -187,14 +362,53 @@ void shouldProduceIdenticalOutput_whenUsingHeadStyle(File composite, File direct List directData = IntegrationExcelDatas.FieldHeadStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldHeadStyle.Composite.class, IntegrationExcelDatas.FieldHeadStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + CellStyle headStyle = headRow.getCell(0).getCellStyle(); + Assertions.assertEquals( + HorizontalAlignment.CENTER, + headStyle.getAlignment(), + "[" + label + "] The horizontal alignment of head row is unexpected."); + Assertions.assertEquals( + 42, + headStyle.getFillForegroundColor(), + "[" + label + "] The fill foreground color of head row is unexpected."); + Assertions.assertEquals( + FillPatternType.SOLID_FOREGROUND, + headStyle.getFillPattern(), + "[" + label + "] The fill pattern of head row is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -206,14 +420,47 @@ void shouldProduceIdenticalOutput_whenUsingHeadFontStyle(File composite, File di List directData = IntegrationExcelDatas.FieldHeadFontStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldHeadFontStyle.Composite.class, IntegrationExcelDatas.FieldHeadFontStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + Font headFont = + workbook.getFontAt(headRow.getCell(0).getCellStyle().getFontIndex()); + Assertions.assertTrue(headFont.getBold(), "[" + label + "] The bold of head row font is unexpected."); + Assertions.assertEquals( + 14, + headFont.getFontHeightInPoints(), + "[" + label + "] The font height of head row is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -225,14 +472,48 @@ void shouldProduceIdenticalOutput_whenUsingContentStyle(File composite, File dir List directData = IntegrationExcelDatas.FieldContentStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldContentStyle.Composite.class, IntegrationExcelDatas.FieldContentStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + CellStyle contentStyle = data.getCell(0).getCellStyle(); + Assertions.assertTrue( + contentStyle.getWrapText(), + "[" + label + "] The wrap text of row[" + i + "] is unexpected."); + Assertions.assertEquals( + VerticalAlignment.CENTER, + contentStyle.getVerticalAlignment(), + "[" + label + "] The vertical alignment of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -244,14 +525,49 @@ void shouldProduceIdenticalOutput_whenUsingContentFontStyle(File composite, File List directData = IntegrationExcelDatas.FieldContentFontStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldContentFontStyle.Composite.class, IntegrationExcelDatas.FieldContentFontStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + Font contentFont = + workbook.getFontAt(data.getCell(0).getCellStyle().getFontIndex()); + Assertions.assertTrue( + contentFont.getItalic(), + "[" + label + "] The italic of row[" + i + "] font is unexpected."); + Assertions.assertEquals( + "Arial", + contentFont.getFontName(), + "[" + label + "] The font name of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -263,14 +579,56 @@ void shouldProduceIdenticalOutput_whenUsingContentLoopMerge(File composite, File List directData = IntegrationExcelDatas.FieldContentLoopMerge.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.FieldContentLoopMerge.Composite.class, IntegrationExcelDatas.FieldContentLoopMerge.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + + // merged regions: every 2 rows merged + List merges = sheet.getMergedRegions(); + Assertions.assertEquals( + 3, merges.size(), "[" + label + "] The number of merged regions is unexpected."); + Assertions.assertEquals( + "A2:A3", + merges.get(0).formatAsString(), + "[" + label + "] The first merged region is unexpected."); + Assertions.assertEquals( + "A4:A5", + merges.get(1).formatAsString(), + "[" + label + "] The second merged region is unexpected."); + Assertions.assertEquals( + "A6:A7", + merges.get(2).formatAsString(), + "[" + label + "] The last merged region is unexpected."); + }); } @ParameterizedTest @@ -282,14 +640,39 @@ void shouldProduceIdenticalOutput_whenUsingExcelPropertyAliasFor(File composite, List directData = IntegrationExcelDatas.AliasForExcelProperty.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForExcelProperty.Composite.class, IntegrationExcelDatas.AliasForExcelProperty.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Custom Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -301,14 +684,43 @@ void shouldProduceIdenticalOutput_whenUsingColumnWidthAliasFor(File composite, F List directData = IntegrationExcelDatas.AliasForColumnWidth.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForColumnWidth.Composite.class, IntegrationExcelDatas.AliasForColumnWidth.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + 20 * 256, + sheet.getColumnWidth(0), + "[" + label + "] The column width of column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -321,14 +733,43 @@ void shouldProduceIdenticalOutput_whenUsingDateTimeFormatAliasFor(File composite List directData = IntegrationExcelDatas.AliasForDateTimeFormat.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForDateTimeFormat.Composite.class, IntegrationExcelDatas.AliasForDateTimeFormat.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.NUMERIC, data.getCell(0).getCellType()); + Assertions.assertEquals( + dateOf(2026, 1, i + 1), + data.getCell(0).getDateCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + "yyyy/MM/dd", + data.getCell(0).getCellStyle().getDataFormatString(), + "[" + label + "] The data format of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -340,14 +781,43 @@ void shouldProduceIdenticalOutput_whenUsingNumberFormatAliasFor(File composite, List directData = IntegrationExcelDatas.AliasForNumberFormat.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForNumberFormat.Composite.class, IntegrationExcelDatas.AliasForNumberFormat.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.NUMERIC, data.getCell(0).getCellType()); + Assertions.assertEquals( + 0.5 + i * 0.1, + data.getCell(0).getNumericCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + "0.00%", + data.getCell(0).getCellStyle().getDataFormatString(), + "[" + label + "] The data format of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -359,14 +829,49 @@ void shouldProduceIdenticalOutput_whenUsingHeadStyleAliasFor(File composite, Fil List directData = IntegrationExcelDatas.AliasForHeadStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForHeadStyle.Composite.class, IntegrationExcelDatas.AliasForHeadStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + CellStyle headStyle = headRow.getCell(0).getCellStyle(); + Assertions.assertEquals( + HorizontalAlignment.RIGHT, + headStyle.getAlignment(), + "[" + label + "] The horizontal alignment of head row is unexpected."); + Assertions.assertEquals( + 13, + headStyle.getFillForegroundColor(), + "[" + label + "] The fill foreground color of head row is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -378,14 +883,48 @@ void shouldProduceIdenticalOutput_whenUsingHeadFontStyleAliasFor(File composite, List directData = IntegrationExcelDatas.AliasForHeadFontStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForHeadFontStyle.Composite.class, IntegrationExcelDatas.AliasForHeadFontStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + Font headFont = + workbook.getFontAt(headRow.getCell(0).getCellStyle().getFontIndex()); + Assertions.assertEquals( + 16, + headFont.getFontHeightInPoints(), + "[" + label + "] The font height of head row is unexpected."); + Assertions.assertEquals( + 10, headFont.getColor(), "[" + label + "] The color of head row font is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -397,14 +936,48 @@ void shouldProduceIdenticalOutput_whenUsingContentStyleAliasFor(File composite, List directData = IntegrationExcelDatas.AliasForContentStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForContentStyle.Composite.class, IntegrationExcelDatas.AliasForContentStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + CellStyle contentStyle = data.getCell(0).getCellStyle(); + Assertions.assertTrue( + contentStyle.getWrapText(), + "[" + label + "] The wrap text of row[" + i + "] is unexpected."); + Assertions.assertEquals( + VerticalAlignment.CENTER, + contentStyle.getVerticalAlignment(), + "[" + label + "] The vertical alignment of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -417,14 +990,50 @@ void shouldProduceIdenticalOutput_whenUsingContentFontStyleAliasFor(File composi List directData = IntegrationExcelDatas.AliasForContentFontStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForContentFontStyle.Composite.class, IntegrationExcelDatas.AliasForContentFontStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + Font contentFont = + workbook.getFontAt(data.getCell(0).getCellStyle().getFontIndex()); + Assertions.assertEquals( + "Courier New", + contentFont.getFontName(), + "[" + label + "] The font name of row[" + i + "] is unexpected."); + Assertions.assertEquals( + 18, + contentFont.getFontHeightInPoints(), + "[" + label + "] The font height of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -437,14 +1046,52 @@ void shouldProduceIdenticalOutput_whenUsingContentLoopMergeAliasFor(File composi List directData = IntegrationExcelDatas.AliasForContentLoopMerge.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForContentLoopMerge.Composite.class, IntegrationExcelDatas.AliasForContentLoopMerge.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + + // merged regions: every 3 rows merged + List merges = sheet.getMergedRegions(); + Assertions.assertEquals( + 2, merges.size(), "[" + label + "] The number of merged regions is unexpected."); + Assertions.assertEquals( + "A2:A4", + merges.get(0).formatAsString(), + "[" + label + "] The first merged region is unexpected."); + Assertions.assertEquals( + "A5:A7", + merges.get(1).formatAsString(), + "[" + label + "] The last merged region is unexpected."); + }); } } @@ -465,14 +1112,44 @@ void shouldProduceIdenticalOutput_whenUsingColumnWidth(File composite, File dire List directData = IntegrationExcelDatas.ClassColumnWidth.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassColumnWidth.Composite.class, IntegrationExcelDatas.ClassColumnWidth.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + + Assertions.assertEquals( + 25 * 256, + sheet.getColumnWidth(0), + "[" + label + "] The column width of column[0] is unexpected."); + }); } @ParameterizedTest @@ -484,14 +1161,53 @@ void shouldProduceIdenticalOutput_whenUsingHeadStyle(File composite, File direct List directData = IntegrationExcelDatas.ClassHeadStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassHeadStyle.Composite.class, IntegrationExcelDatas.ClassHeadStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + CellStyle headStyle = headRow.getCell(0).getCellStyle(); + Assertions.assertEquals( + HorizontalAlignment.CENTER, + headStyle.getAlignment(), + "[" + label + "] The horizontal alignment of head row is unexpected."); + Assertions.assertEquals( + 42, + headStyle.getFillForegroundColor(), + "[" + label + "] The fill foreground color of head row is unexpected."); + Assertions.assertEquals( + FillPatternType.SOLID_FOREGROUND, + headStyle.getFillPattern(), + "[" + label + "] The fill pattern of head row is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -503,14 +1219,47 @@ void shouldProduceIdenticalOutput_whenUsingHeadFontStyle(File composite, File di List directData = IntegrationExcelDatas.ClassHeadFontStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassHeadFontStyle.Composite.class, IntegrationExcelDatas.ClassHeadFontStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + Font headFont = + workbook.getFontAt(headRow.getCell(0).getCellStyle().getFontIndex()); + Assertions.assertTrue(headFont.getBold(), "[" + label + "] The bold of head row font is unexpected."); + Assertions.assertEquals( + 14, + headFont.getFontHeightInPoints(), + "[" + label + "] The font height of head row is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -522,14 +1271,48 @@ void shouldProduceIdenticalOutput_whenUsingContentStyle(File composite, File dir List directData = IntegrationExcelDatas.ClassContentStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassContentStyle.Composite.class, IntegrationExcelDatas.ClassContentStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + CellStyle contentStyle = data.getCell(0).getCellStyle(); + Assertions.assertTrue( + contentStyle.getWrapText(), + "[" + label + "] The wrap text of row[" + i + "] is unexpected."); + Assertions.assertEquals( + VerticalAlignment.CENTER, + contentStyle.getVerticalAlignment(), + "[" + label + "] The vertical alignment of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -541,14 +1324,49 @@ void shouldProduceIdenticalOutput_whenUsingContentFontStyle(File composite, File List directData = IntegrationExcelDatas.ClassContentFontStyle.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassContentFontStyle.Composite.class, IntegrationExcelDatas.ClassContentFontStyle.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + Font contentFont = + workbook.getFontAt(data.getCell(0).getCellStyle().getFontIndex()); + Assertions.assertTrue( + contentFont.getItalic(), + "[" + label + "] The italic of row[" + i + "] font is unexpected."); + Assertions.assertEquals( + "Arial", + contentFont.getFontName(), + "[" + label + "] The font name of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -560,14 +1378,41 @@ void shouldProduceIdenticalOutput_whenUsingHeadRowHeight(File composite, File di List directData = IntegrationExcelDatas.ClassHeadRowHeight.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassHeadRowHeight.Composite.class, IntegrationExcelDatas.ClassHeadRowHeight.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + Assertions.assertEquals( + 40.0f, headRow.getHeightInPoints(), "[" + label + "] The head row height is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -579,14 +1424,43 @@ void shouldProduceIdenticalOutput_whenUsingContentRowHeight(File composite, File List directData = IntegrationExcelDatas.ClassContentRowHeight.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassContentRowHeight.Composite.class, IntegrationExcelDatas.ClassContentRowHeight.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + 30.0f, + data.getHeightInPoints(), + "[" + label + "] The content row height of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -598,14 +1472,51 @@ void shouldProduceIdenticalOutput_whenUsingOnceAbsoluteMerge(File composite, Fil List directData = IntegrationExcelDatas.ClassOnceAbsoluteMerge.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.ClassOnceAbsoluteMerge.Composite.class, IntegrationExcelDatas.ClassOnceAbsoluteMerge.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(2, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name", "Value"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(2, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals(CellType.STRING, data.getCell(1).getCellType()); + Assertions.assertEquals( + "Value" + i, + data.getCell(1).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[1] is unexpected."); + } + + // merged region + List merges = sheet.getMergedRegions(); + Assertions.assertEquals( + 1, merges.size(), "[" + label + "] The number of merged regions is unexpected."); + Assertions.assertEquals( + "A1:B1", merges.get(0).formatAsString(), "[" + label + "] The merged region is unexpected."); + }); } @ParameterizedTest @@ -617,14 +1528,41 @@ void shouldProduceIdenticalOutput_whenUsingHeadRowHeightAliasFor(File composite, List directData = IntegrationExcelDatas.AliasForHeadRowHeight.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForHeadRowHeight.Composite.class, IntegrationExcelDatas.AliasForHeadRowHeight.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + Assertions.assertEquals( + 50.0f, headRow.getHeightInPoints(), "[" + label + "] The head row height is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } @ParameterizedTest @@ -637,14 +1575,43 @@ void shouldProduceIdenticalOutput_whenUsingContentRowHeightAliasFor(File composi List directData = IntegrationExcelDatas.AliasForContentRowHeight.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForContentRowHeight.Composite.class, IntegrationExcelDatas.AliasForContentRowHeight.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals( + 35.0f, + data.getHeightInPoints(), + "[" + label + "] The content row height of row[" + i + "] is unexpected."); + } + }); } @ParameterizedTest @@ -658,14 +1625,51 @@ void shouldProduceIdenticalOutput_whenUsingOnceAbsoluteMergeAliasFor(File compos List directData = IntegrationExcelDatas.AliasForOnceAbsoluteMerge.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.AliasForOnceAbsoluteMerge.Composite.class, IntegrationExcelDatas.AliasForOnceAbsoluteMerge.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(2, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name", "Value"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(2, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + Assertions.assertEquals(CellType.STRING, data.getCell(1).getCellType()); + Assertions.assertEquals( + "Value" + i, + data.getCell(1).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[1] is unexpected."); + } + + // merged region + List merges = sheet.getMergedRegions(); + Assertions.assertEquals( + 1, merges.size(), "[" + label + "] The number of merged regions is unexpected."); + Assertions.assertEquals( + "A1:B2", merges.get(0).formatAsString(), "[" + label + "] The merged region is unexpected."); + }); } } @@ -687,14 +1691,83 @@ void shouldProduceIdenticalOutput_whenUsingMixedAnnotations(File composite, File IntegrationExcelDatas.MixedAll.compositeData(); List directData = IntegrationExcelDatas.MixedAll.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.MixedAll.Composite.class, IntegrationExcelDatas.MixedAll.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row + Row headRow = sheet.getRow(0); + Assertions.assertEquals(2, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Name", "Value"), label); + Assertions.assertEquals( + 40.0f, headRow.getHeightInPoints(), "[" + label + "] The head row height is unexpected."); + + // head style for column 0 + CellStyle headStyle0 = headRow.getCell(0).getCellStyle(); + Assertions.assertEquals( + HorizontalAlignment.CENTER, + headStyle0.getAlignment(), + "[" + label + "] The horizontal alignment of head row column[0] is unexpected."); + Assertions.assertEquals( + 42, + headStyle0.getFillForegroundColor(), + "[" + label + "] The fill foreground color of head row column[0] is unexpected."); + Assertions.assertEquals( + FillPatternType.SOLID_FOREGROUND, + headStyle0.getFillPattern(), + "[" + label + "] The fill pattern of head row column[0] is unexpected."); + + // column width + Assertions.assertEquals( + 25 * 256, + sheet.getColumnWidth(0), + "[" + label + "] The column width of column[0] is unexpected."); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(2, data.getPhysicalNumberOfCells()); + Assertions.assertEquals( + 30.0f, + data.getHeightInPoints(), + "[" + label + "] The content row height of row[" + i + "] is unexpected."); + + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + + Assertions.assertEquals(CellType.STRING, data.getCell(1).getCellType()); + Assertions.assertEquals( + "Value" + i, + data.getCell(1).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[1] is unexpected."); + + Font contentFont1 = + workbook.getFontAt(data.getCell(1).getCellStyle().getFontIndex()); + Assertions.assertTrue( + contentFont1.getItalic(), + "[" + label + "] The italic of row[" + i + "] column[1] font is unexpected."); + Assertions.assertEquals( + "Arial", + contentFont1.getFontName(), + "[" + label + "] The font name of row[" + i + "] column[1] is unexpected."); + } + }); } @ParameterizedTest @@ -708,14 +1781,39 @@ void shouldProduceIdenticalOutput_whenDirectAnnotationOverridesComposite(File co List directData = IntegrationExcelDatas.PriorityDirectOverComposite.directData(); - // When + Then - writeAndAssert( + // When + doWrite( composite, direct, IntegrationExcelDatas.PriorityDirectOverComposite.Composite.class, IntegrationExcelDatas.PriorityDirectOverComposite.Direct.class, compositeData, directData); + + // Then + WorkbookAsserts.build(composite, "Composite", direct, "Direct").assertMulti((label, workbook) -> { + Assertions.assertEquals(1, workbook.getNumberOfSheets()); + + Sheet sheet = workbook.getSheetAt(0); + Assertions.assertEquals(6, sheet.getPhysicalNumberOfRows()); + + // head row: direct annotation "Final Value" overrides composite alias "Value" + Row headRow = sheet.getRow(0); + Assertions.assertEquals(1, headRow.getPhysicalNumberOfCells()); + assertHeadNames(headRow, Arrays.asList("Final Value"), label); + + // data rows + for (int i = 0; i < 5; i++) { + Row data = sheet.getRow(i + 1); + + Assertions.assertEquals(1, data.getPhysicalNumberOfCells()); + Assertions.assertEquals(CellType.STRING, data.getCell(0).getCellType()); + Assertions.assertEquals( + "Name" + i, + data.getCell(0).getStringCellValue(), + "[" + label + "] The data of row[" + i + "] column[0] is unexpected."); + } + }); } } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java index b1640641f..6dbf52d49 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java @@ -19,199 +19,53 @@ package org.apache.fesod.sheet.annotation.composite; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; import java.io.File; +import java.util.ArrayList; import java.util.List; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.Font; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; +import java.util.function.BiConsumer; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; -import org.apache.poi.ss.util.CellRangeAddress; +import org.junit.jupiter.api.Assertions; /** - * Compares two Excel workbooks cell-by-cell for integration testing of composable annotations. - *

- * Verifies: cell values, cell styles (alignment, fill, border, data format), - * fonts (name, height, bold, italic, color, etc.), column widths, row heights, - * and merged regions. + * A simple assertion tool for workbooks. */ class WorkbookAsserts { - static void assertWorkbooksMatch(File expected, File actual) { - try (Workbook wb1 = WorkbookFactory.create(expected); - Workbook wb2 = WorkbookFactory.create(actual)) { - assertWorkbooksMatch(wb1, wb2); - } catch (AssertionError e) { - throw e; - } catch (Exception e) { - fail("Failed to read workbooks: " + e.getMessage(), e); - } - } + private final List list; - static void assertWorkbooksMatch(Workbook expected, Workbook actual) { - assertEquals(expected.getNumberOfSheets(), actual.getNumberOfSheets(), "Sheet count mismatch"); - for (int i = 0; i < expected.getNumberOfSheets(); i++) { - assertSheetMatches(expected.getSheetAt(i), actual.getSheetAt(i), i); - } + WorkbookAsserts(List list) { + this.list = list; } - // ---- Sheet ---- - - private static void assertSheetMatches(Sheet expected, Sheet actual, int sheetIdx) { - String ctx = "Sheet[" + sheetIdx + "]"; - - // Merged regions - List expMerged = expected.getMergedRegions(); - List actMerged = actual.getMergedRegions(); - assertEquals(expMerged.size(), actMerged.size(), ctx + " merged region count"); - for (int i = 0; i < expMerged.size(); i++) { - assertEquals( - expMerged.get(i).formatAsString(), - actMerged.get(i).formatAsString(), - ctx + " merged region[" + i + "]"); - } - - // Rows - int lastRow = Math.max(expected.getLastRowNum(), actual.getLastRowNum()); - for (int r = 0; r <= lastRow; r++) { - assertRowMatches(expected.getRow(r), actual.getRow(r), sheetIdx, r); - } + private static class FileMetadata { + final File file; + final String label; - // Column widths - int maxCol = Math.max(maxColumnIndex(expected), maxColumnIndex(actual)); - for (int c = 0; c <= maxCol; c++) { - assertEquals(expected.getColumnWidth(c), actual.getColumnWidth(c), ctx + " column[" + c + "] width"); + FileMetadata(File file, String label) { + this.file = file; + this.label = label; } } - // ---- Row ---- - - private static void assertRowMatches(Row expected, Row actual, int sheetIdx, int rowIdx) { - String ctx = "Sheet[" + sheetIdx + "].Row[" + rowIdx + "]"; - if (expected == null && actual == null) { - return; + static WorkbookAsserts build(Object... args) { + if (args.length % 2 != 0) { + throw new IllegalArgumentException("Arguments must be pairs of Label (String) and File (File)"); } - assertNotNull(expected, ctx + " missing in expected"); - assertNotNull(actual, ctx + " missing in actual"); - - assertEquals(expected.getHeightInPoints(), actual.getHeightInPoints(), ctx + " heightInPoints"); - - short maxCell = (short) Math.max( - expected.getLastCellNum() < 0 ? 0 : expected.getLastCellNum(), - actual.getLastCellNum() < 0 ? 0 : actual.getLastCellNum()); - for (int c = 0; c < maxCell; c++) { - assertCellMatches( - expected.getCell(c), - actual.getCell(c), - sheetIdx, - rowIdx, - c, - expected.getSheet().getWorkbook(), - actual.getSheet().getWorkbook()); + List files = new ArrayList<>(); + for (int i = 0; i < args.length; i += 2) { + files.add(new FileMetadata((File) args[i], (String) args[i + 1])); } + return new WorkbookAsserts(files); } - // ---- Cell ---- - - private static void assertCellMatches( - Cell expected, Cell actual, int sheetIdx, int rowIdx, int colIdx, Workbook expWb, Workbook actWb) { - String ctx = "Cell[" + rowIdx + "," + colIdx + "]"; - if (expected == null && actual == null) { - return; - } - assertNotNull(expected, ctx + " missing in expected"); - assertNotNull(actual, ctx + " missing in actual"); - - // Type - assertEquals(expected.getCellType(), actual.getCellType(), ctx + " type"); - - // Value - switch (expected.getCellType()) { - case STRING: - assertEquals(expected.getStringCellValue(), actual.getStringCellValue(), ctx + " value"); - break; - case NUMERIC: - assertEquals(expected.getNumericCellValue(), actual.getNumericCellValue(), ctx + " value"); - break; - case BOOLEAN: - assertEquals(expected.getBooleanCellValue(), actual.getBooleanCellValue(), ctx + " value"); - break; - case FORMULA: - assertEquals(expected.getCellFormula(), actual.getCellFormula(), ctx + " formula"); - break; - default: - break; - } - - // Style - assertStyleMatches(expected.getCellStyle(), actual.getCellStyle(), expWb, actWb, ctx); - } - - // ---- CellStyle ---- - - private static void assertStyleMatches( - CellStyle expected, CellStyle actual, Workbook expWb, Workbook actWb, String ctx) { - // Alignment - assertEquals(expected.getAlignment(), actual.getAlignment(), ctx + " alignment"); - assertEquals(expected.getVerticalAlignment(), actual.getVerticalAlignment(), ctx + " verticalAlignment"); - assertEquals(expected.getWrapText(), actual.getWrapText(), ctx + " wrapText"); - assertEquals(expected.getRotation(), actual.getRotation(), ctx + " rotation"); - assertEquals(expected.getIndention(), actual.getIndention(), ctx + " indent"); - - // Visibility / protection - assertEquals(expected.getHidden(), actual.getHidden(), ctx + " hidden"); - assertEquals(expected.getLocked(), actual.getLocked(), ctx + " locked"); - assertEquals(expected.getShrinkToFit(), actual.getShrinkToFit(), ctx + " shrinkToFit"); - - // Borders - assertEquals(expected.getBorderLeft(), actual.getBorderLeft(), ctx + " borderLeft"); - assertEquals(expected.getBorderRight(), actual.getBorderRight(), ctx + " borderRight"); - assertEquals(expected.getBorderTop(), actual.getBorderTop(), ctx + " borderTop"); - assertEquals(expected.getBorderBottom(), actual.getBorderBottom(), ctx + " borderBottom"); - assertEquals(expected.getLeftBorderColor(), actual.getLeftBorderColor(), ctx + " leftBorderColor"); - assertEquals(expected.getRightBorderColor(), actual.getRightBorderColor(), ctx + " rightBorderColor"); - assertEquals(expected.getTopBorderColor(), actual.getTopBorderColor(), ctx + " topBorderColor"); - assertEquals(expected.getBottomBorderColor(), actual.getBottomBorderColor(), ctx + " bottomBorderColor"); - - // Fill - assertEquals(expected.getFillPattern(), actual.getFillPattern(), ctx + " fillPattern"); - assertEquals(expected.getFillForegroundColor(), actual.getFillForegroundColor(), ctx + " fillForegroundColor"); - assertEquals(expected.getFillBackgroundColor(), actual.getFillBackgroundColor(), ctx + " fillBackgroundColor"); - - // Data format - assertEquals(expected.getDataFormat(), actual.getDataFormat(), ctx + " dataFormat"); - - // Font - assertFontMatches(expWb.getFontAt(expected.getFontIndex()), actWb.getFontAt(actual.getFontIndex()), ctx); - } - - // ---- Font ---- - - private static void assertFontMatches(Font expected, Font actual, String ctx) { - assertEquals(expected.getFontName(), actual.getFontName(), ctx + " fontName"); - assertEquals(expected.getFontHeightInPoints(), actual.getFontHeightInPoints(), ctx + " fontHeightInPoints"); - assertEquals(expected.getBold(), actual.getBold(), ctx + " bold"); - assertEquals(expected.getItalic(), actual.getItalic(), ctx + " italic"); - assertEquals(expected.getColor(), actual.getColor(), ctx + " color"); - assertEquals(expected.getUnderline(), actual.getUnderline(), ctx + " underline"); - assertEquals(expected.getStrikeout(), actual.getStrikeout(), ctx + " strikeout"); - assertEquals(expected.getTypeOffset(), actual.getTypeOffset(), ctx + " typeOffset"); - } - - // ---- Helpers ---- - - private static int maxColumnIndex(Sheet sheet) { - int max = 0; - for (Row row : sheet) { - if (row.getLastCellNum() > max) { - max = row.getLastCellNum(); + void assertMulti(BiConsumer consumer) { + for (FileMetadata metadata : list) { + try (Workbook workbook = WorkbookFactory.create(metadata.file)) { + consumer.accept(metadata.label, workbook); + } catch (Exception ex) { + Assertions.fail("Failed to process workbook [" + metadata.label + "]", ex); } } - return max; } }