diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/ExcelView.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/ExcelView.java new file mode 100644 index 000000000..579246f0b --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/annotation/write/ExcelView.java @@ -0,0 +1,63 @@ +/* + * 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.write; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apache.fesod.sheet.write.builder.AbstractExcelWriterParameterBuilder; + +/** + * Annotation used for indicating view(s) that the property + * that is defined by field annotated is part of. + *

+ * An example annotation would be: + *

+ *  @ExcelView(asTypes = BasicView.class)
+ *  // Or
+ *  @ExcelView(asNames = "BasicView")
+ * 
+ * which would specify that field annotated would be included + * when processing (writing) Sheet identified by BasicView.class (or its subclass) or + * "BasicView". + * If multiple View class or string identifiers are included, the field will be part of all of them. + *

+ * + * @see AbstractExcelWriterParameterBuilder#groups(Class[]) + * @see AbstractExcelWriterParameterBuilder#groups(String[]) + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExcelView { + + /** + * View or views that annotated element is part of. Views are identified + * by classes, when a view type is selected, fields annotated with that type + * or any of subtypes are included. + */ + Class[] asTypes() default {}; + + /** + * View or views that annotated element is part of. Views are identified + * by strings. + */ + String[] asNames() default {}; +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java index 2e75fc3e3..dc45e9eca 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java @@ -67,6 +67,7 @@ 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; +import org.apache.fesod.sheet.write.view.WriteViewMatcher; public class ClassUtils { @@ -346,23 +347,30 @@ private static FieldCache doDeclaredFields(Class clazz, ConfigurationHolder c WriteHolder writeHolder = (WriteHolder) configurationHolder; + // ignore field by grouping + WriteViewMatcher writeViewMatcher = writeHolder.writeViewMatcher(); + boolean hasViews = (WriteViewMatcher.NOOP != writeViewMatcher); + // ignore field by include*/exclude* boolean needIgnore = !CollectionUtils.isEmpty(writeHolder.excludeColumnFieldNames()) || !CollectionUtils.isEmpty(writeHolder.excludeColumnIndexes()) || !CollectionUtils.isEmpty(writeHolder.includeColumnFieldNames()) - || !CollectionUtils.isEmpty(writeHolder.includeColumnIndexes()); + || !CollectionUtils.isEmpty(writeHolder.includeColumnIndexes()) + || hasViews; if (!needIgnore) { return fieldCache; } - // ignore filed + // ignore field Map tempSortedFieldMap = MapUtils.newHashMap(); int index = 0; for (Map.Entry entry : sortedFieldMap.entrySet()) { Integer key = entry.getKey(); FieldWrapper field = entry.getValue(); + boolean isGroupsMismatch = hasViews && !writeViewMatcher.matches(field.getField()); + // The current field needs to be ignored - if (writeHolder.ignore(field.getFieldName(), entry.getKey())) { + if (isGroupsMismatch || writeHolder.ignore(field.getFieldName(), entry.getKey())) { ignoreSet.add(field.getFieldName()); indexFieldMap.remove(index); } else { @@ -575,6 +583,7 @@ public static class FieldCacheKey { private Collection excludeColumnIndexes; private Collection includeColumnFieldNames; private Collection includeColumnIndexes; + private WriteViewMatcher writeViewMatcher; FieldCacheKey(Class clazz, ConfigurationHolder configurationHolder) { this.clazz = clazz; @@ -584,6 +593,7 @@ public static class FieldCacheKey { this.excludeColumnIndexes = writeHolder.excludeColumnIndexes(); this.includeColumnFieldNames = writeHolder.includeColumnFieldNames(); this.includeColumnIndexes = writeHolder.includeColumnIndexes(); + this.writeViewMatcher = writeHolder.writeViewMatcher(); } } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java index 012e5b3ae..47a530f53 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java @@ -27,10 +27,13 @@ import java.util.ArrayList; import java.util.Collection; +import org.apache.commons.lang3.ArrayUtils; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.metadata.AbstractParameterBuilder; import org.apache.fesod.sheet.write.handler.WriteHandler; import org.apache.fesod.sheet.write.metadata.WriteBasicParameter; +import org.apache.fesod.sheet.write.view.ClassBasedViewMatcher; +import org.apache.fesod.sheet.write.view.NameBasedViewMatcher; /** * Build ExcelBuilder @@ -170,4 +173,34 @@ public T orderByIncludeColumn(Boolean orderByIncludeColumn) { parameter().setOrderByIncludeColumn(orderByIncludeColumn); return self(); } + + /** + * Only write the fields marked by the following View class identifiers. + * + * @param types Target View class identifiers + * @throws IllegalArgumentException if the types is empty + * @return this + */ + public T groups(Class... types) { + if (ArrayUtils.isEmpty(types)) { + throw new IllegalArgumentException("Types must not be empty"); + } + parameter().setWriteViewMatcher(new ClassBasedViewMatcher(types)); + return self(); + } + + /** + * Only write to the fields marked by the following View string identifiers. + * + * @param names Target View string identifiers + * @throws IllegalArgumentException if the names is empty + * @return this + */ + public T groups(String... names) { + if (ArrayUtils.isEmpty(names)) { + throw new IllegalArgumentException("Names must not be empty"); + } + parameter().setWriteViewMatcher(new NameBasedViewMatcher(names)); + return self(); + } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java index 5dd959e71..3b134df28 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java @@ -34,6 +34,7 @@ import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.metadata.BasicParameter; import org.apache.fesod.sheet.write.handler.WriteHandler; +import org.apache.fesod.sheet.write.view.WriteViewMatcher; /** * Write basic parameter @@ -92,4 +93,9 @@ public class WriteBasicParameter extends BasicParameter { * Default is {@code false}. */ private Boolean orderByIncludeColumn; + + /** + * view-based matcher for sheet writing. + */ + private WriteViewMatcher writeViewMatcher; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java index 293f18adc..01db93eb2 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java @@ -72,6 +72,7 @@ import org.apache.fesod.sheet.write.style.SheetFreezePaneStrategy; import org.apache.fesod.sheet.write.style.column.AbstractHeadColumnWidthStyleStrategy; import org.apache.fesod.sheet.write.style.row.SimpleRowHeightStyleStrategy; +import org.apache.fesod.sheet.write.view.WriteViewMatcher; /** * Write holder configuration @@ -136,6 +137,11 @@ public abstract class AbstractWriteHolder extends AbstractHolder implements Writ */ private List> customConverterList; + /** + * view-based matcher for sheet writing. + */ + private WriteViewMatcher writeViewMatcher; + /** * Write handler */ @@ -263,6 +269,16 @@ public AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWrit this.includeColumnIndexes = writeBasicParameter.getIncludeColumnIndexes(); } + if (writeBasicParameter.getWriteViewMatcher() == null) { + if (parentAbstractWriteHolder == null) { + this.writeViewMatcher = WriteViewMatcher.NOOP; + } else { + this.writeViewMatcher = parentAbstractWriteHolder.getWriteViewMatcher(); + } + } else { + this.writeViewMatcher = writeBasicParameter.getWriteViewMatcher(); + } + // Initialization property this.excelWriteHeadProperty = new ExcelWriteHeadProperty(this, getClazz(), getHead()); @@ -599,4 +615,9 @@ public Collection excludeColumnIndexes() { public Collection excludeColumnFieldNames() { return getExcludeColumnFieldNames(); } + + @Override + public WriteViewMatcher writeViewMatcher() { + return getWriteViewMatcher(); + } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java index 7fdb1bcdc..df13e8ee6 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java @@ -29,6 +29,7 @@ import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.write.property.ExcelWriteHeadProperty; +import org.apache.fesod.sheet.write.view.WriteViewMatcher; /** * Get the corresponding Holder @@ -115,4 +116,9 @@ public interface WriteHolder extends ConfigurationHolder { * @return */ Collection excludeColumnFieldNames(); + + /** + * view-based matcher for sheet writing. + */ + WriteViewMatcher writeViewMatcher(); } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/ClassBasedViewMatcher.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/ClassBasedViewMatcher.java new file mode 100644 index 000000000..6a38a9a53 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/ClassBasedViewMatcher.java @@ -0,0 +1,65 @@ +/* + * 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.write.view; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import lombok.EqualsAndHashCode; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.fesod.sheet.annotation.write.ExcelView; + +/** + * View matcher that resolves view-based on class + * identifiers declared in {@code @ExcelView#asTypes()}. + */ +@EqualsAndHashCode +public class ClassBasedViewMatcher implements WriteViewMatcher { + + private final Collection> expectedGroups; + + public ClassBasedViewMatcher(Class... expectedGroups) { + this(ArrayUtils.isEmpty(expectedGroups) ? Collections.emptyList() : Arrays.asList(expectedGroups)); + } + + public ClassBasedViewMatcher(Collection> expectedGroups) { + if (CollectionUtils.isEmpty(expectedGroups)) { + throw new IllegalArgumentException("Type-based view groups must not be empty"); + } + this.expectedGroups = Collections.unmodifiableCollection(expectedGroups); + } + + @Override + public boolean matches(Field field) { + Class[] fieldGroups = Optional.ofNullable(field.getAnnotation(ExcelView.class)) + .map(ExcelView::asTypes) + .orElse(new Class[0]); + + if (ArrayUtils.isEmpty(fieldGroups)) { + return false; + } + + return Arrays.stream(fieldGroups).anyMatch(fieldGroup -> expectedGroups.stream() + .anyMatch(expectedGroup -> expectedGroup.isAssignableFrom(fieldGroup))); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/NameBasedViewMatcher.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/NameBasedViewMatcher.java new file mode 100644 index 000000000..d7020e349 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/NameBasedViewMatcher.java @@ -0,0 +1,65 @@ +/* + * 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.write.view; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import lombok.EqualsAndHashCode; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.fesod.sheet.annotation.write.ExcelView; + +/** + * View matcher that resolves view-based on string + * identifiers declared in {@code @ExcelView#asNames()}. + */ +@EqualsAndHashCode +public class NameBasedViewMatcher implements WriteViewMatcher { + + private final Collection expectedGroups; + + public NameBasedViewMatcher(String... expectedGroups) { + this(ArrayUtils.isEmpty(expectedGroups) ? Collections.emptyList() : Arrays.asList(expectedGroups)); + } + + public NameBasedViewMatcher(Collection expectedGroups) { + if (CollectionUtils.isEmpty(expectedGroups)) { + throw new IllegalArgumentException("Name-based view groups must not be empty"); + } + this.expectedGroups = Collections.unmodifiableCollection(expectedGroups); + } + + @Override + public boolean matches(Field field) { + String[] fieldGroups = Optional.ofNullable(field.getAnnotation(ExcelView.class)) + .map(ExcelView::asNames) + .orElse(new String[0]); + + if (ArrayUtils.isEmpty(fieldGroups)) { + return false; + } + + return Arrays.stream(fieldGroups).anyMatch(fieldGroup -> expectedGroups.stream() + .anyMatch(expectedGroup -> expectedGroup.equals(fieldGroup))); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/WriteViewMatcher.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/WriteViewMatcher.java new file mode 100644 index 000000000..e59992fa2 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/view/WriteViewMatcher.java @@ -0,0 +1,46 @@ +/* + * 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.write.view; + +import java.lang.reflect.Field; + +/** + * Strategy interface for determining whether Sheet writing should + * apply view-based matcher and whether a given field belongs to + * the active view(s). + */ +public interface WriteViewMatcher { + + /** + * A noop implementation for {@link WriteViewMatcher} + */ + WriteViewMatcher NOOP = new WriteViewMatcher() { + @Override + public boolean matches(Field field) { + return false; + } + }; + + /** + * Returns whether the given field is included in the active view(s) + * based on its {@code @ExcelView} annotation. + */ + boolean matches(Field field); +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java index da41bae8d..2d81c7a2a 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java @@ -30,6 +30,7 @@ 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.ExcelView; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.converters.string.StringStringConverter; import org.apache.fesod.sheet.enums.CacheLocationEnum; @@ -42,6 +43,9 @@ 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; +import org.apache.fesod.sheet.write.view.ClassBasedViewMatcher; +import org.apache.fesod.sheet.write.view.NameBasedViewMatcher; +import org.apache.fesod.sheet.write.view.WriteViewMatcher; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -84,6 +88,18 @@ private static class SimpleEntity { private Integer age; } + interface BasicView {} + + private static class ViewEntity { + @ExcelView(asTypes = BasicView.class) + @ExcelProperty("Name") + private String name; + + @ExcelView(asNames = "BasicView") + @ExcelProperty(value = "Age") + private Integer age; + } + private static class ComplexEntity { @ExcelProperty(index = 0) private String id; @@ -114,6 +130,7 @@ private static class FormatEntity { @Test void test_declaredFields_cache_memory() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.MEMORY); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); FieldCache cache1 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertNotNull(cache1); @@ -127,6 +144,7 @@ void test_declaredFields_cache_memory() { @Test void test_declaredFields_cache_ThreadLocal() throws NoSuchFieldException, IllegalAccessException { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.THREAD_LOCAL); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); FieldCache cache1 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertNotNull(cache1); @@ -140,6 +158,7 @@ void test_declaredFields_cache_ThreadLocal() throws NoSuchFieldException, Illega @Test void test_declaredFields_non_cache() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); FieldCache cache1 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); FieldCache cache2 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); @@ -151,6 +170,7 @@ void test_declaredFields_non_cache() { @Test void test_declaredFields_ordering() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); FieldCache fieldCache = ClassUtils.declaredFields(ComplexEntity.class, writeHolder); Map sortedMap = fieldCache.getSortedFieldMap(); @@ -171,6 +191,7 @@ void test_declaredFields_ordering() { @Test void test_declaredFields_ignore() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); FieldCache fieldCache = ClassUtils.declaredFields(ComplexEntity.class, writeHolder); @@ -183,6 +204,7 @@ void test_declaredFields_ignore() { @Test void test_declaredFields_WriteHolder_exclude() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); Mockito.when(writeHolder.excludeColumnFieldNames()).thenReturn(Collections.singleton("name")); Mockito.when(writeHolder.ignore(Mockito.anyString(), Mockito.anyInt())).thenReturn(false); @@ -202,6 +224,7 @@ void test_declaredFields_WriteHolder_exclude() { @Test void test_declaredFields_resort() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); Mockito.when(writeHolder.orderByIncludeColumn()).thenReturn(true); List include = Arrays.asList("age", "name"); @@ -221,6 +244,7 @@ void test_declaredFields_resort_byIndex() { Mockito.when(writeHolder.includeColumnFieldNames()).thenReturn(null); Mockito.when(writeHolder.includeColumnIndexes()).thenReturn(Arrays.asList(2, 0)); Mockito.when(writeHolder.ignore(Mockito.anyString(), Mockito.anyInt())).thenReturn(false); + Mockito.when(writeHolder.writeViewMatcher()).thenReturn(WriteViewMatcher.NOOP); FieldCache fieldCache = ClassUtils.declaredFields(ComplexEntity.class, writeHolder); Map sortedMap = fieldCache.getSortedFieldMap(); @@ -234,6 +258,34 @@ void test_declaredFields_resort_byIndex() { Assertions.assertEquals("id", sortedMap.get(1).getFieldName()); } + @Test + void test_declaredFields_typed_views_write() { + Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()) + .thenReturn(new ClassBasedViewMatcher(Collections.singleton(BasicView.class))); + + FieldCache fieldCache = ClassUtils.declaredFields(ViewEntity.class, writeHolder); + + Map sortedMap = fieldCache.getSortedFieldMap(); + + Assertions.assertEquals(1, sortedMap.size()); + Assertions.assertEquals("name", sortedMap.get(0).getFieldName()); + } + + @Test + void test_declaredFields_named_views_write() { + Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); + Mockito.when(writeHolder.writeViewMatcher()) + .thenReturn(new NameBasedViewMatcher(Collections.singleton("BasicView"))); + + FieldCache fieldCache = ClassUtils.declaredFields(ViewEntity.class, writeHolder); + + Map sortedMap = fieldCache.getSortedFieldMap(); + + Assertions.assertEquals(1, sortedMap.size()); + Assertions.assertEquals("age", sortedMap.get(0).getFieldName()); + } + @Test void test_declaredExcelContentProperty() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteMixedViewData.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteMixedViewData.java new file mode 100644 index 000000000..f502c5f9a --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteMixedViewData.java @@ -0,0 +1,44 @@ +/* + * 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.view; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fesod.sheet.annotation.write.ExcelView; + +@EqualsAndHashCode +@Data +public class WriteMixedViewData { + + @ExcelView( + asTypes = {WriteViewStrategy.BaseView.class}, + asNames = {"base"}) + private String string1; + + @ExcelView( + asTypes = {WriteViewStrategy.GroupA.class}, + asNames = {"detail", "export"}) + private String string2; + + @ExcelView(asNames = {"detail"}) + private String string3; + + private String defaultString; +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteNamedViewsData.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteNamedViewsData.java new file mode 100644 index 000000000..149c1ab77 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteNamedViewsData.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.view; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fesod.sheet.annotation.write.ExcelView; + +@EqualsAndHashCode +@Data +public class WriteNamedViewsData { + + @ExcelView(asNames = {"base"}) + private String string1; + + @ExcelView(asNames = {"base", "detail"}) + private String string2; + + @ExcelView(asNames = {"detail"}) + private String string3; + + @ExcelView(asNames = {"other"}) + private String string4; +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteSheetViewTests.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteSheetViewTests.java new file mode 100644 index 000000000..973eab791 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteSheetViewTests.java @@ -0,0 +1,236 @@ +/* + * 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.view; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.io.input.BOMInputStream; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.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.io.TempDir; + +/** + * Tests for the view-based export grouping feature using {@code @ExcelView}. + */ +class WriteSheetViewTests { + + private File write03; + private File write07; + private File writeCsv; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + write03 = createTmpFile(tempDir, "write03.xls"); + write07 = createTmpFile(tempDir, "write07.xlsx"); + writeCsv = createTmpFile(tempDir, "writeCsv.csv"); + } + + private File createTmpFile(Path dir, String filename) { + return new File(dir.resolve(filename).toString()); + } + + @FunctionalInterface + interface WriteExecutor { + void execute(File file, ExcelTypeEnum type, String sheetName) throws Exception; + } + + private void doTestAllFormatsAndVerify(List expectedHeads, WriteExecutor action) throws Exception { + ExcelTypeEnum[] types = {ExcelTypeEnum.XLS, ExcelTypeEnum.XLSX, ExcelTypeEnum.CSV}; + File[] files = {write03, write07, writeCsv}; + String sheetName = "TestSheet"; + + for (int i = 0; i < types.length; i++) { + File currentFile = files[i]; + ExcelTypeEnum currentType = types[i]; + + // Write + action.execute(currentFile, currentType, sheetName); + + // Verify + verifyHeaders(currentFile, currentType, sheetName, expectedHeads); + } + } + + private void verifyHeaders(File file, ExcelTypeEnum excelType, String sheetName, List expectedHeads) + throws Exception { + if (excelType == ExcelTypeEnum.CSV) { + try (InputStream is = BOMInputStream.builder() + .setInputStream(Files.newInputStream(file.toPath())) + .get(); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader); + + Map headerMap = parser.getHeaderMap(); + + Assertions.assertNotNull(headerMap, "CSV file is empty"); + + String[] headers = headerMap.keySet().toArray(new String[0]); + Assertions.assertEquals(expectedHeads.size(), headers.length, "CSV Header count mismatch"); + for (int i = 0; i < expectedHeads.size(); i++) { + Assertions.assertEquals(expectedHeads.get(i), headers[i], "CSV Header text mismatch"); + } + } + } else { + try (Workbook workbook = WorkbookFactory.create(file)) { + Sheet sheet = workbook.getSheet(sheetName); + + Row headRow = sheet.getRow(0); + Assertions.assertNotNull(headRow, "Excel header row is null"); + Assertions.assertEquals( + expectedHeads.size(), + headRow.getPhysicalNumberOfCells(), + "Excel Header count mismatch for " + excelType); + + for (int i = 0; i < expectedHeads.size(); i++) { + Assertions.assertEquals( + expectedHeads.get(i), + headRow.getCell(i).getStringCellValue(), + "Excel Header text mismatch for " + excelType); + } + } + } + } + + // ========================================================================= + // Test by Class Type + // ========================================================================= + + @Nested + class ClassBasedViewTests { + + @Test + void testWriteWithBaseAndSubTypes() throws Exception { + List expectedHeads = Arrays.asList("string1", "string2", "string5"); + + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteTypedViewsData.class) + .excelType(type) + .groups(WriteViewStrategy.BaseView.class) + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + + @Test + void testWriteWithExactViewMatch() throws Exception { + List expectedHeads = Arrays.asList("string2", "string3"); + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteTypedViewsData.class) + .excelType(type) + .groups(WriteViewStrategy.GroupA.class) + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + + @Test + void testWriteWithMultipleGroups() throws Exception { + List expectedHeads = Arrays.asList("string2", "string3", "string4"); + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteTypedViewsData.class) + .excelType(type) + .groups(WriteViewStrategy.GroupA.class, WriteViewStrategy.GroupB.class) + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + } + + // ========================================================================= + // Test by String Label Type + // ========================================================================= + + @Nested + class StringBasedViewTests { + + @Test + void testWriteWithSingleView() throws Exception { + List expectedHeads = Arrays.asList("string1", "string2"); + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteNamedViewsData.class) + .excelType(type) + .groups("base") + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + + @Test + void testWriteWithMultipleViews() throws Exception { + List expectedHeads = Arrays.asList("string1", "string2", "string3"); + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteNamedViewsData.class) + .excelType(type) + .groups("base", "detail") + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + } + + // ========================================================================= + // Test for Conflict and Override Strategies, and Default Behavior + // ========================================================================= + + @Nested + class ConflictAndEdgeCaseTests { + + @Test + void testTagOverridesViewWhenCalledLast() throws Exception { + // The tags called later should override the previous groups. + List expectedHeads = Arrays.asList("string2", "string3"); + + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteMixedViewData.class) + .excelType(type) + .groups(WriteViewStrategy.BaseView.class) + // Take effect + .groups("detail") + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + + @Test + void testWriteWithoutViewApi() throws Exception { + // Export all fields marked and unmarked with @ExcelView + List expectedHeads = Arrays.asList("string1", "string2", "string3", "defaultString"); + + doTestAllFormatsAndVerify(expectedHeads, (file, type, sheetName) -> FesodSheet.write(file) + .head(WriteMixedViewData.class) + .excelType(type) + .sheet(sheetName) + .doWrite(Collections.emptyList())); + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteTypedViewsData.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteTypedViewsData.java new file mode 100644 index 000000000..0cad2b8a2 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteTypedViewsData.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.view; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fesod.sheet.annotation.write.ExcelView; +import org.apache.fesod.sheet.view.WriteViewStrategy.BaseView; +import org.apache.fesod.sheet.view.WriteViewStrategy.ExtendGroupC; +import org.apache.fesod.sheet.view.WriteViewStrategy.GroupA; +import org.apache.fesod.sheet.view.WriteViewStrategy.GroupB; + +@EqualsAndHashCode +@Data +public class WriteTypedViewsData { + + @ExcelView(asTypes = {BaseView.class}) + private String string1; + + @ExcelView(asTypes = {BaseView.class, GroupA.class}) + private String string2; + + @ExcelView(asTypes = {GroupA.class, GroupB.class}) + private String string3; + + @ExcelView(asTypes = {GroupB.class}) + private String string4; + + @ExcelView(asTypes = {ExtendGroupC.class}) + private String string5; +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteViewStrategy.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteViewStrategy.java new file mode 100644 index 000000000..a6dd5d924 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/view/WriteViewStrategy.java @@ -0,0 +1,31 @@ +/* + * 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.view; + +public interface WriteViewStrategy { + + interface BaseView {} + + interface GroupA {} + + interface GroupB {} + + interface ExtendGroupC extends BaseView {} +} diff --git a/website/docs/sheet/help/annotation.md b/website/docs/sheet/help/annotation.md index 19287de69..d82761ea3 100644 --- a/website/docs/sheet/help/annotation.md +++ b/website/docs/sheet/help/annotation.md @@ -157,3 +157,12 @@ Define a freeze pane for an Excel sheet. The parameters are as follows: | rowSplit | 0 | Vertical position of freeze pane. | | leftmostColumn | -1 | Left column visible in right pane. By default, it's equal to `colSplit`. | | topRow | -1 | Top row visible in bottom pane. By default, it's equal to `rowSplit`. | + +### `@ExcelView` + +Defines the view(s) that the field belongs to. During spreadsheet writing, only fields whose declared views match the active view will be included. The parameters are as follows: + +| Name | Default Value | Description | +|---------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| asTypes | Empty | View or views that annotated element is part of. Views are identified by classes, when
a view type is selected, fields annotated with that type or any of subtypes are included. | +| asNames | Empty | View or views that annotated element is part of. Views are identified by strings. | diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/annotation.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/annotation.md index bcecf7f2b..c72815dda 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/annotation.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/annotation.md @@ -134,3 +134,12 @@ title: '注解' | rowSplit | 0 | 冻结窗格的垂直位置(即需要冻结的行数) | | leftmostColumn | -1 | 右侧窗格中可见的最左侧列。默认情况下,该值等于 `colSplit` | | topRow | -1 | 底部窗格中可见的最顶部行。默认情况下,该值等于 `rowSplit` | + +### `@ExcelView` + +定义字段所属视图。在执行电子表格写入时,只有与当前视图匹配的字段才会参与写入。具体参数如下: + +| 名称 | 默认值 | 描述 | +|---------|-----|-----------------------------------------------------------------------| +| asTypes | 空 | 被注解元素所属的一个或多个视图。视图由类进行标识,当选择一个视图类型时,用该类型
或其任何子类型注解的字段都将被包含在当前视图内。 | +| asNames | 空 | 该注解元素所属的一个或多个视图,视图由字符串进行标识。 |