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 new file mode 100644 index 000000000..df82d5bcb --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AliasFor.java @@ -0,0 +1,73 @@ +/* + * 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 source annotation. + */ + private final String customAttribute; + + /** + * 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, 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 new file mode 100644 index 000000000..a1e840181 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationAttributes.java @@ -0,0 +1,86 @@ +/* + * 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; + } + + public boolean isAnnotationTypeEqual(Class 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"); + 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..a11ae4794 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReader.java @@ -0,0 +1,62 @@ +/* + * 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(Boolean.TRUE); + } + + public AnnotationMetadataReader(Boolean enableMetaMarked) { + this( + new DefaultAnnotationMetadataResolver(), + enableMetaMarked, + ConcurrentReferenceHashMap.builder() + .get()); + } + + public AnnotationMetadataReader( + AnnotationMetadataResolver resolver, + Boolean enableMetaMarked, + Map elementAnnotation) { + super(resolver, enableMetaMarked); + 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..d28275a65 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/DefaultAnnotationMetadataResolver.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.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())); + } + + 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) { + 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..4677e5be1 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/HierarchicalAnnotationScanner.java @@ -0,0 +1,149 @@ +/* + * 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 final Boolean enableMetaMarked; + + protected HierarchicalAnnotationScanner(AnnotationMetadataResolver metadataResolver, Boolean enableMetaMarked) { + this.metadataResolver = metadataResolver; + this.enableMetaMarked = enableMetaMarked; + } + + 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 (isMetaMarkedEnabled() && 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; + } + + private boolean isMetaMarkedEnabled() { + return Boolean.TRUE.equals(enableMetaMarked); + } + + /** + * 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.getCustomAttribute())); + } + } + } +} 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/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 85e008428..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,12 +25,16 @@ 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.FontProperty; @@ -53,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 */ @@ -94,11 +94,25 @@ public Head( Field field, String fieldName, List headNameList, + 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 { @@ -112,4 +126,29 @@ public Head( 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 7c4903b62..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,12 +36,16 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.fesod.common.util.StringUtils; +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; /** @@ -53,14 +58,15 @@ @Slf4j public class ExcelHeadProperty { - /** - * Custom class - */ - private Class headClazz; /** * The types of head */ private HeadKindEnum headKind; + + /** + * Custom class descriptor + */ + private AnnotatedTypeDescriptor typeDescriptor; /** * The number of rows in the line with the most rows */ @@ -70,8 +76,11 @@ public class ExcelHeadProperty { */ private Map headMap; + 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; @@ -83,13 +92,13 @@ 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, head.get(i), Boolean.FALSE, Boolean.TRUE)); headIndex++; } headKind = HeadKindEnum.STRING; } // convert headClazz to head - initColumnProperties(configurationHolder); + initColumnProperties(headClazz, configurationHolder); initHeadRowNumber(); if (log.isDebugEnabled()) { @@ -117,18 +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; } - 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; } @@ -137,29 +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); } } - Head head = new Head(index, field.getField(), field.getFieldName(), tmpHeadList, 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/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..679cf39e2 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/AnnotationMetadataReaderTest.java @@ -0,0 +1,412 @@ +/* + * 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.List; +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; + } + + /** + * 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) + static String columnWidthField; + + @ComposableColumnWidth(15) + static String composableField; + + @ComposableColumnWidthRenamed(width = 18) + static String renamedAliasField; + + 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); + } + + // ---- 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 + 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..d23459db6 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/CompositeAnnotationTest.java @@ -0,0 +1,1229 @@ +/* + * 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.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; +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.getEnableMetaMarked()).thenReturn(true); + 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.getFieldDescriptor().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(2); + Assertions.assertNotNull(head); + AnnotationMap fieldAnnotationMap = head.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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)); + } + + @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 + class ClassLevelCompositeAnnotationTest { + + @Test + void shouldIncludeComposableAndInnerAnnotation_whenAliasForPresent() { + // given - ExcelModelWithComposableClassAnnotation has @ComposableColumnWidth(25) + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithComposableClassAnnotation.class, null); + + // then + AnnotationMap classAnnotationMap = property.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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)); + } + + @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 + 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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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.getFieldDescriptor().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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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)); + } + + @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 + 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..7d767df89 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/DirectAnnotationTest.java @@ -0,0 +1,843 @@ +/* + * 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.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; +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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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.getFieldDescriptor().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)); + } + + @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 ---- + + 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.getTypeDescriptor().getAnnotationMap()); + } + + @Test + void shouldSetNullHeadClazzAnnotationMap_whenHeadClazzHasNoAnnotations() { + // given - ExcelModelWithoutAnnotations has no class-level annotations + + // when + ExcelHeadProperty property = + new ExcelHeadProperty(configurationHolder, ExcelModelWithoutAnnotations.class, null); + + // then + Assertions.assertNull(property.getTypeDescriptor().getAnnotationMap()); + } + + @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.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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.getTypeDescriptor().getAnnotationMap(); + 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)); + } + + @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 + 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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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.getTypeDescriptor().getAnnotationMap(); + 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.getFieldDescriptor().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.getFieldDescriptor().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)); + } + + @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..6f65d80cf --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/IntegrationCompositeAnnotationTest.java @@ -0,0 +1,1819 @@ +/* + * 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.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; +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. + *

+ * 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 { + + 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 doWrite( + File compositeFile, + File directFile, + Class compositeClass, + Class directClass, + List compositeData, + List directData) + throws Exception { + + 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); + } + } + + 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."); + } + } + + private Date dateOf(int year, int month, int day) { + return Date.from(LocalDate.of(year, month, day) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + } + + // ==================================================================== + // 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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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."); + }); + } + } + + // ==================================================================== + // 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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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 + @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 + 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."); + }); + } + } + + // ==================================================================== + // 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 + 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 + @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 + 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/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..6dbf52d49 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/annotation/composite/WorkbookAsserts.java @@ -0,0 +1,71 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.junit.jupiter.api.Assertions; + +/** + * A simple assertion tool for workbooks. + */ +class WorkbookAsserts { + + private final List list; + + WorkbookAsserts(List list) { + this.list = list; + } + + private static class FileMetadata { + final File file; + final String label; + + FileMetadata(File file, String label) { + this.file = file; + this.label = label; + } + } + + static WorkbookAsserts build(Object... args) { + if (args.length % 2 != 0) { + throw new IllegalArgumentException("Arguments must be pairs of Label (String) and File (File)"); + } + 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); + } + + 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); + } + } + } +} 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 -> {