This is an automated email from the ASF dual-hosted git repository.
psxjoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fesod.git
The following commit(s) were added to refs/heads/main by this push:
new d7fae5a4 feat: add fluent header api for no-bean mode (#943)
d7fae5a4 is described below
commit d7fae5a40f5a537582663df86c50cf413154a5f6
Author: Bengbengbalabalabeng
<[email protected]>
AuthorDate: Wed Jul 8 19:44:17 2026 +0800
feat: add fluent header api for no-bean mode (#943)
* feat: add fluent header api for no-bean mode
* docs: refine method summary Javadoc for head(List), head(Consumer), and
head(Class).
---
.../sheet/metadata/AbstractParameterBuilder.java | 40 +++-
.../fesod/sheet/metadata/DefaultHeadBuilder.java | 133 +++++++++++++
.../apache/fesod/sheet/metadata/HeadBuilder.java | 134 +++++++++++++
.../fesod/sheet/metadata/HeadBuilderTest.java | 219 +++++++++++++++++++++
4 files changed, 520 insertions(+), 6 deletions(-)
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 e2966ea5..de25c3c0 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
@@ -29,6 +29,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.function.Consumer;
import org.apache.fesod.common.util.ListUtils;
import org.apache.fesod.sheet.converters.Converter;
import org.apache.fesod.sheet.enums.CacheLocationEnum;
@@ -39,17 +40,39 @@ import org.apache.fesod.sheet.enums.CacheLocationEnum;
*
*/
public abstract class AbstractParameterBuilder<T extends
AbstractParameterBuilder, C extends BasicParameter> {
+
/**
- * You can only choose one of the {@link #head(List)} and {@link
#head(Class)}
+ * Configure sheet headers dynamically using a raw {@code
List<List<String>>}.
*
- * @param head
- * @return
+ * <p>
+ * <strong>Note:</strong> Use of this method is mutually exclusive with
{@link #head(Class)}.
+ * </p>
+ *
+ * @param head the raw header data list
+ * @see #head(Consumer)
+ * @return this builder
*/
public T head(List<List<String>> head) {
parameter().setHead(toMutableListIfNecessary(head));
return self();
}
+ /**
+ * Configure sheet headers dynamically using a {@code HeadBuilder}
consumer.
+ *
+ * <p>
+ * <strong>Note:</strong> Use of this method is mutually exclusive with
{@link #head(Class)}.
+ * </p>
+ *
+ * @param headBuilderConsumer the consumer to configure the headers
+ * @see #head(List)
+ * @return this builder
+ */
+ public T head(Consumer<HeadBuilder> headBuilderConsumer) {
+ parameter().setHead(DefaultHeadBuilder.define(headBuilderConsumer));
+ return self();
+ }
+
/**
* Ensures and returns a mutable head list.
*
@@ -68,10 +91,15 @@ public abstract class AbstractParameterBuilder<T extends
AbstractParameterBuilde
}
/**
- * You can only choose one of the {@link #head(List)} and {@link
#head(Class)}
+ * Configure sheet headers using a Java model.
*
- * @param clazz
- * @return
+ * <p>
+ * <strong>Note:</strong> Use of this method is mutually exclusive with
+ * {@link #head(List)} and {@link #head(Consumer)}.
+ * </p>
+ *
+ * @param clazz the Java model class
+ * @return this builder
*/
public T head(Class<?> clazz) {
parameter().setClazz(clazz);
diff --git
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/DefaultHeadBuilder.java
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/DefaultHeadBuilder.java
new file mode 100644
index 00000000..dec891cf
--- /dev/null
+++
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/DefaultHeadBuilder.java
@@ -0,0 +1,133 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Standard implementation of {@code HeadBuilder}.
+ */
+class DefaultHeadBuilder implements HeadBuilder {
+
+ private final List<List<String>> columns;
+ private final List<String> prefixes;
+
+ DefaultHeadBuilder() {
+ this(new ArrayList<>(), new ArrayList<>());
+ }
+
+ DefaultHeadBuilder(List<List<String>> columns, List<String> prefixes) {
+ this.columns = columns;
+ this.prefixes = prefixes;
+ }
+
+ static List<List<String>> define(Consumer<HeadBuilder>
headBuilderConsumer) {
+ Validate.notNull(headBuilderConsumer, "headBuilderConsumer must not be
null");
+
+ DefaultHeadBuilder builder = new DefaultHeadBuilder();
+ headBuilderConsumer.accept(builder);
+ return builder.toHead();
+ }
+
+ /**
+ * Define a single column with a fixed head names (containing at least one
column).
+ *
+ * @param headName the first header name (must not be {@code null})
+ * @param subHeadNames optional sublevel header names
+ * @return this for builder chains
+ */
+ @Override
+ public HeadBuilder column(String headName, String... subHeadNames) {
+ Validate.notNull(headName, "header name must not be null");
+
+ int initialCapacity = prefixes.size() + ((subHeadNames == null) ? 0 :
subHeadNames.length) + 1;
+ List<String> current = new ArrayList<>(initialCapacity);
+ current.addAll(prefixes);
+ current.add(headName);
+
+ if (subHeadNames != null) {
+ Validate.noNullElements(subHeadNames, "sub-header names must not
contain null elements");
+ Collections.addAll(current, subHeadNames);
+ }
+ columns.add(current);
+ return this;
+ }
+
+ /**
+ * Define a single column with a repeating header name at multiple levels.
+ *
+ * @param headName the header name to repeat (must not be {@code null})
+ * @param repeat the number of times to repeat the name
+ * @return this for builder chains
+ */
+ @Override
+ public HeadBuilder column(String headName, int repeat) {
+ Validate.notNull(headName, "header name must not be null");
+ Validate.isTrue(repeat > 0, "header repeat must be greater than 0");
+
+ int initialCapacity = prefixes.size() + repeat;
+ List<String> current = new ArrayList<>(initialCapacity);
+ current.addAll(prefixes);
+
+ for (int i = 0; i < repeat; i++) {
+ current.add(headName);
+ }
+
+ columns.add(current);
+ return this;
+ }
+
+ /**
+ * Declare multiple shared parent header names to group sub-headers.
+ *
+ * @param parentHeadNames the shared parent header names (must not be
{@code null} or empty)
+ * @param subHeadBuilderConsumer the consumer to define sub-headers
+ * @return this for builder chains
+ */
+ @Override
+ public HeadBuilder columns(List<String> parentHeadNames,
Consumer<HeadBuilder> subHeadBuilderConsumer) {
+ Validate.notEmpty(parentHeadNames, "parent header names must not be
null or empty");
+ Validate.noNullElements(parentHeadNames, "parent header names must not
contain null elements");
+ Validate.notNull(subHeadBuilderConsumer, "subHeadBuilderConsumer must
not be null");
+
+ int previousSize = this.prefixes.size();
+ this.prefixes.addAll(parentHeadNames);
+
+ subHeadBuilderConsumer.accept(this);
+
+ while (this.prefixes.size() > previousSize) {
+ this.prefixes.remove(this.prefixes.size() - 1);
+ }
+ return this;
+ }
+
+ /**
+ * Return the heads.
+ *
+ * @return the list of head
+ */
+ List<List<String>> toHead() {
+ return columns;
+ }
+}
diff --git
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/HeadBuilder.java
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/HeadBuilder.java
new file mode 100644
index 00000000..39b3984a
--- /dev/null
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/HeadBuilder.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * A builder for configuring sheet headers in No-Bean mode.
+ *
+ * <p>Example - complex headers:</p>
+ * <pre>
+ * {@code FesodSheet.write(pathname)
+ * .head(builder -> builder
+ * .column("ID", 2)
+ * .columns("User Info", sub -> sub.column("Name").column("Age"))
+ * .column("Others", "Remark")
+ * )
+ * // Equivalent to:
+ * List<List<String>> head = new ArrayList<>();
+ * head.add(new ArrayList<>(Arrays.asList("ID", "ID")));
+ * head.add(new ArrayList<>(Arrays.asList("User Info", "Name")));
+ * head.add(new ArrayList<>(Arrays.asList("User Info", "Age")));
+ * head.add(new ArrayList<>(Arrays.asList("Others", "Remark")));
+ *
+ * FesodSheet.write(pathname)
+ * .head(head)}
+ * </pre>
+ *
+ * <p>Example - single-level headers:</p>
+ * <pre>
+ * {@code FesodSheet.write(pathname)
+ * .head(HeadBuilder.forSimple("ID", "Name", "Age", "Remark"))
+ * // Equivalent to:
+ * List<List<String>> head = new ArrayList<>();
+ * head.add(new ArrayList<>(Arrays.asList("ID")));
+ * head.add(new ArrayList<>(Arrays.asList("Name")));
+ * head.add(new ArrayList<>(Arrays.asList("Age")));
+ * head.add(new ArrayList<>(Arrays.asList("Remark")));
+ *
+ * FesodSheet.write(pathname)
+ * .head(head)}
+ * </pre>
+ *
+ * @see AbstractParameterBuilder#head(Consumer)
+ */
+public interface HeadBuilder {
+
+ /**
+ * Define a single column with a fixed head names (containing at least one
column).
+ *
+ * @param headName the first header name (must not be {@code null})
+ * @param subHeadNames optional sublevel header names
+ * @return this for builder chains
+ */
+ HeadBuilder column(String headName, String... subHeadNames);
+
+ /**
+ * Define a single column with a repeating header name at multiple levels.
+ *
+ * @param headName the header name to repeat (must not be {@code null})
+ * @param repeat the number of times to repeat the name
+ * @return this for builder chains
+ */
+ HeadBuilder column(String headName, int repeat);
+
+ /**
+ * Declare a shared parent header name to group sub-headers.
+ *
+ * @param parentHeadName the shared parent header name (must not be {@code
null})
+ * @param subHeadBuilderConsumer the consumer to define sub-headers
+ * @return this for builder chains
+ */
+ default HeadBuilder columns(String parentHeadName, Consumer<HeadBuilder>
subHeadBuilderConsumer) {
+ return columns(Collections.singletonList(parentHeadName),
subHeadBuilderConsumer);
+ }
+
+ /**
+ * Declare multiple shared parent header names to group sub-headers.
+ *
+ * @param parentHeadNames the shared parent header names (must not be
{@code null} or empty)
+ * @param subHeadBuilderConsumer the consumer to define sub-headers
+ * @return this for builder chains
+ */
+ HeadBuilder columns(List<String> parentHeadNames, Consumer<HeadBuilder>
subHeadBuilderConsumer);
+
+ /**
+ * Build a simple single-level header list (containing at least one
column).
+ *
+ * @param headName the first header name (must not be {@code null})
+ * @param otherHeadNames optional other header names
+ * @return a simple single-level header list
+ */
+ static List<List<String>> forSimple(String headName, String...
otherHeadNames) {
+ Validate.notNull(headName, "header name must not be null");
+
+ int initialCapacity = ((otherHeadNames == null) ? 0 :
otherHeadNames.length) + 1;
+ List<List<String>> result = new ArrayList<>(initialCapacity);
+
+ List<String> firstHead = new ArrayList<>(1);
+ firstHead.add(headName);
+ result.add(firstHead);
+
+ if (otherHeadNames != null) {
+ Validate.noNullElements(otherHeadNames, "other header names must
not contain null elements");
+ for (String otherHeadName : otherHeadNames) {
+ List<String> otherHead = new ArrayList<>(1);
+ otherHead.add(otherHeadName);
+ result.add(otherHead);
+ }
+ }
+ return result;
+ }
+}
diff --git
a/fesod-sheet/src/test/java/org/apache/fesod/sheet/metadata/HeadBuilderTest.java
b/fesod-sheet/src/test/java/org/apache/fesod/sheet/metadata/HeadBuilderTest.java
new file mode 100644
index 00000000..d0d15590
--- /dev/null
+++
b/fesod-sheet/src/test/java/org/apache/fesod/sheet/metadata/HeadBuilderTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.fesod.sheet.testkit.Tags;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link DefaultHeadBuilder} and {@link HeadBuilder}.
+ */
+@Tag(Tags.UNIT)
+class HeadBuilderTest {
+
+ static Consumer<HeadBuilder> withEmpty() {
+ return b -> {};
+ }
+
+ @Test
+ void forSimple_singleName() {
+ List<List<String>> head = HeadBuilder.forSimple("ID");
+
+ Assertions.assertEquals(1, head.size());
+ Assertions.assertEquals(Arrays.asList("ID"), head.get(0));
+ }
+
+ @Test
+ void forSimple_multipleNames() {
+ List<List<String>> head = HeadBuilder.forSimple("ID", "Name", "Age");
+
+ Assertions.assertEquals(3, head.size());
+ Assertions.assertEquals(Arrays.asList("ID"), head.get(0));
+ Assertions.assertEquals(Arrays.asList("Name"), head.get(1));
+ Assertions.assertEquals(Arrays.asList("Age"), head.get(2));
+ }
+
+ @Test
+ void forSimple_nullFirstNameThrows() {
+ Assertions.assertThrows(NullPointerException.class, () ->
HeadBuilder.forSimple(null, "Name"));
+ }
+
+ @Test
+ void forSimple_nullOtherNameThrows() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
HeadBuilder.forSimple("ID", "Name", null));
+ }
+
+ @Test
+ void define_nullConsumerThrows() {
+ Assertions.assertThrows(NullPointerException.class, () ->
DefaultHeadBuilder.define(null));
+ }
+
+ @Test
+ void define_emptyConsumerReturnsEmptyHead() {
+ List<List<String>> head = DefaultHeadBuilder.define(withEmpty());
+
+ Assertions.assertNotNull(head);
+ Assertions.assertTrue(head.isEmpty());
+ }
+
+ @Test
+ void column_singleName() {
+ List<List<String>> head = DefaultHeadBuilder.define(b ->
b.column("ID"));
+
+ Assertions.assertEquals(1, head.size());
+ Assertions.assertEquals(Arrays.asList("ID"), head.get(0));
+ }
+
+ @Test
+ void column_withSubHeadNames() {
+ List<List<String>> head = DefaultHeadBuilder.define(b -> b.column("A",
"B", "C"));
+
+ Assertions.assertEquals(1, head.size());
+ Assertions.assertEquals(Arrays.asList("A", "B", "C"), head.get(0));
+ }
+
+ @Test
+ void column_nullHeadNameThrows() {
+ Assertions.assertThrows(NullPointerException.class, () ->
DefaultHeadBuilder.define(b -> b.column(null, "B")));
+ }
+
+ @Test
+ void column_nullSubHeadNameThrows() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
DefaultHeadBuilder.define(b -> b.column("A", "B", null)));
+ }
+
+ @Test
+ void column_repeatOnce() {
+ List<List<String>> head = DefaultHeadBuilder.define(b -> b.column("X",
1));
+
+ Assertions.assertEquals(Arrays.asList("X"), head.get(0));
+ }
+
+ @Test
+ void column_repeatMultiple() {
+ List<List<String>> head = DefaultHeadBuilder.define(b -> b.column("X",
3));
+
+ Assertions.assertEquals(1, head.size());
+ Assertions.assertEquals(Arrays.asList("X", "X", "X"), head.get(0));
+ }
+
+ @Test
+ void column_repeatZeroThrows() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
DefaultHeadBuilder.define(b -> b.column("X", 0)));
+ }
+
+ @Test
+ void column_repeatNegativeThrows() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
DefaultHeadBuilder.define(b -> b.column("X", -2)));
+ }
+
+ @Test
+ void column_repeat_nullHeadNameThrows() {
+ Assertions.assertThrows(NullPointerException.class, () ->
DefaultHeadBuilder.define(b -> b.column(null, 2)));
+ }
+
+ @Test
+ void columns_singleParentAppliesPrefixToSubColumns() {
+ List<List<String>> head = DefaultHeadBuilder.define(
+ b -> b.columns("User Info", sub ->
sub.column("Name").column("Age")));
+
+ Assertions.assertEquals(2, head.size());
+ Assertions.assertEquals(Arrays.asList("User Info", "Name"),
head.get(0));
+ Assertions.assertEquals(Arrays.asList("User Info", "Age"),
head.get(1));
+ }
+
+ @Test
+ void columns_multipleParentNamesStackAsPrefix() {
+ List<List<String>> head = DefaultHeadBuilder.define(
+ b -> b.columns(Arrays.asList("P", "Q"), sub ->
sub.column("A").column("B")));
+
+ Assertions.assertEquals(2, head.size());
+ Assertions.assertEquals(Arrays.asList("P", "Q", "A"), head.get(0));
+ Assertions.assertEquals(Arrays.asList("P", "Q", "B"), head.get(1));
+ }
+
+ @Test
+ void columns_prefixesAreRestoredAfterBlock() {
+ List<List<String>> head = DefaultHeadBuilder.define(
+ b -> b.columns("P", sub -> sub.column("A")).column("B"));
+
+ Assertions.assertEquals(2, head.size());
+ Assertions.assertEquals(Arrays.asList("P", "A"), head.get(0));
+ Assertions.assertEquals(Arrays.asList("B"), head.get(1));
+ }
+
+ @Test
+ void columns_nestedParentsConcatenate() {
+ List<List<String>> head = DefaultHeadBuilder.define(
+ b -> b.columns("Outer", outer -> outer.columns("Inner", inner
-> inner.column("Leaf"))));
+
+ Assertions.assertEquals(1, head.size());
+ Assertions.assertEquals(Arrays.asList("Outer", "Inner", "Leaf"),
head.get(0));
+ }
+
+ @Test
+ void columns_emptyParentListIsNotAllowed() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> DefaultHeadBuilder.define(b ->
b.columns(Collections.emptyList(), sub -> sub.column("A"))));
+ }
+
+ @Test
+ void columns_nullParentNamesThrows() {
+ Assertions.assertThrows(
+ NullPointerException.class,
+ () -> DefaultHeadBuilder.define(b -> b.columns((List<String>)
null, withEmpty())));
+ }
+
+ @Test
+ void columns_nullElementInParentNamesThrows() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> DefaultHeadBuilder.define(b ->
b.columns(Arrays.asList("P", null), withEmpty())));
+ }
+
+ @Test
+ void columns_nullConsumerThrows() {
+ Assertions.assertThrows(
+ NullPointerException.class,
+ () -> DefaultHeadBuilder.define(b ->
b.columns(Arrays.asList("P"), (Consumer<HeadBuilder>) null)));
+ }
+
+ @Test
+ void complexExampleFromJavadoc() {
+ List<List<String>> head = DefaultHeadBuilder.define(b ->
b.column("ID", 2)
+ .columns("User Info", sub -> sub.column("Name").column("Age"))
+ .column("Others", "Remark"));
+
+ Assertions.assertEquals(4, head.size());
+ Assertions.assertEquals(Arrays.asList("ID", "ID"), head.get(0));
+ Assertions.assertEquals(Arrays.asList("User Info", "Name"),
head.get(1));
+ Assertions.assertEquals(Arrays.asList("User Info", "Age"),
head.get(2));
+ Assertions.assertEquals(Arrays.asList("Others", "Remark"),
head.get(3));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]