This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-configuration.git


The following commit(s) were added to refs/heads/master by this push:
     new d3a464c9a [CONFIGURATION-857] Preserve duplicate non-String scalar 
values in flatten() (#645)
d3a464c9a is described below

commit d3a464c9ae931a05e437990f3357380e6d65169d
Author: MarĂ­a Galbis <[email protected]>
AuthorDate: Wed Jul 15 20:02:34 2026 +0200

    [CONFIGURATION-857] Preserve duplicate non-String scalar values in 
flatten() (#645)
    
    * Enhance flattening logic to handle duplicate scalar values in collections
    
    * Fix invalid whitespace and tabs
    
    * Restore the comment explaining why Path is treated as a scalar
    
    * Reuse the active recursion-path set when flattening array elements, 
centralize cycle checks for recursive containers, and keep scalar values 
outside cycle detection
    
    * add coverage for array and mixed-container cycles
    
    * document the expected size calculation in the cyclic list test
    
    * clarify the size calculation formula in the flattening test for scalar 
values
---
 .../convert/AbstractListDelimiterHandler.java      | 45 +++++++++++++---------
 .../TestAbstractConfigurationBasicFeatures.java    | 28 ++++++++++++++
 .../TestPropertiesConfiguration.java               |  7 +++-
 .../convert/TestDefaultListDelimiterHandler.java   | 26 +++++++++++++
 4 files changed, 86 insertions(+), 20 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/configuration2/convert/AbstractListDelimiterHandler.java
 
b/src/main/java/org/apache/commons/configuration2/convert/AbstractListDelimiterHandler.java
index 0c0aa542e..e041ed6ca 100644
--- 
a/src/main/java/org/apache/commons/configuration2/convert/AbstractListDelimiterHandler.java
+++ 
b/src/main/java/org/apache/commons/configuration2/convert/AbstractListDelimiterHandler.java
@@ -20,6 +20,7 @@ import java.lang.reflect.Array;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Set;
@@ -42,24 +43,25 @@ public abstract class AbstractListDelimiterHandler 
implements ListDelimiterHandl
     static Collection<?> flatten(final ListDelimiterHandler handler, final 
Object value, final int limit, final Set<Object> dejaVu) {
         if (value instanceof String) {
             return handler.split((String) value, true);
+        } else if (!isRecursiveContainer(value)) {
+            return value != null ? Collections.singletonList(value) : 
Collections.emptyList();
+        }
+        if (!dejaVu.add(value)) {
+            return Collections.emptyList();
         }
-        dejaVu.add(value);
         final Collection<Object> result = new LinkedList<>();
-        if (value instanceof Path) {
-            // Don't handle as an Iterable.
-            result.add(value);
-        } else if (value instanceof Iterable) {
-            flattenIterator(handler, result, ((Iterable<?>) value).iterator(), 
limit, dejaVu);
-        } else if (value instanceof Iterator) {
-            flattenIterator(handler, result, (Iterator<?>) value, limit, 
dejaVu);
-        } else if (value != null) {
-            if (value.getClass().isArray()) {
+        try {
+            if (value instanceof Iterable) {
+                flattenIterator(handler, result, ((Iterable<?>) 
value).iterator(), limit, dejaVu);
+            } else if (value instanceof Iterator) {
+                flattenIterator(handler, result, (Iterator<?>) value, limit, 
dejaVu);
+            } else if (value.getClass().isArray()) {
                 for (int len = Array.getLength(value), idx = 0, size = 0; idx 
< len && size < limit; idx++, size = result.size()) {
-                    result.addAll(handler.flatten(Array.get(value, idx), limit 
- size));
+                    result.addAll(flatten(handler, Array.get(value, idx), 
limit - size, dejaVu));
                 }
-            } else {
-                result.add(value);
             }
+        } finally {
+            dejaVu.remove(value);
         }
         return result;
     }
@@ -77,12 +79,19 @@ public abstract class AbstractListDelimiterHandler 
implements ListDelimiterHandl
             final Set<Object> dejaVue) {
         int size = target.size();
         while (size < limit && iterator.hasNext()) {
-            final Object next = iterator.next();
-            if (!dejaVue.contains(next)) {
-                target.addAll(flatten(handler, next, limit - size, dejaVue));
-                size = target.size();
-            }
+            target.addAll(flatten(handler, iterator.next(), limit - size, 
dejaVue));
+            size = target.size();
+        }
+    }
+
+    private static boolean isRecursiveContainer(final Object value) {
+        if (value instanceof Path) {
+            // Don't handle as an Iterable.
+            return false;
         }
+        return value instanceof Iterator
+                || value instanceof Iterable
+                || value != null && value.getClass().isArray();
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestAbstractConfigurationBasicFeatures.java
 
b/src/test/java/org/apache/commons/configuration2/TestAbstractConfigurationBasicFeatures.java
index 604661e48..91223128c 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestAbstractConfigurationBasicFeatures.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestAbstractConfigurationBasicFeatures.java
@@ -26,6 +26,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -676,6 +678,32 @@ public class TestAbstractConfigurationBasicFeatures {
         assertEquals(expected, result);
     }
 
+    /**
+     * Tests typed list conversion for delimited values with duplicates.
+     */
+    @Test
+    void testGetListTypedWithDuplicatesAndDelimiterHandling() {
+        final BaseConfiguration config = new BaseConfiguration();
+        config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
+
+        config.addProperty("list.strings", Arrays.asList("a", "b", "a"));
+        config.addProperty("list.strings2", Arrays.asList("", "", "a"));
+        config.addProperty("list.ints", Arrays.asList(1, 2, 1));
+        config.addProperty("list.booleans", Arrays.asList(true, false, true));
+        config.addProperty("list.doubles", Arrays.asList(1.5, 2.5, 1.5));
+        config.addProperty("list.paths", Arrays.asList(Paths.get("path1"), 
Paths.get("path2"), Paths.get("path1")));
+
+        assertEquals(Arrays.asList("a", "b", "a"), 
config.getList(String.class, "list.strings"));
+        assertEquals(Arrays.asList("", "", "a"), config.getList(String.class, 
"list.strings2"));
+        assertEquals(Arrays.asList(1, 2, 1), config.getList(Integer.class, 
"list.ints"));
+        assertEquals(Arrays.asList(Boolean.TRUE, Boolean.FALSE, Boolean.TRUE), 
config.getList(Boolean.class, "list.booleans"));
+        assertEquals(Arrays.asList(1.5d, 2.5d, 1.5d), 
config.getList(Double.class, "list.doubles"));
+        assertEquals(
+                Arrays.asList(Paths.get("path1"), Paths.get("path2"), 
Paths.get("path1")),
+                config.getList(Path.class, "list.paths")
+        );
+    }
+
     /**
      * Tests getList() for single non-string values.
      */
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
 
b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
index ced8e3968..6ea513bc3 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
@@ -511,13 +511,16 @@ public class TestPropertiesConfiguration {
     void testCompress840ArrayListCycle(final int size) {
         final ArrayList<Object> object = new ArrayList<>();
         for (int i = 0; i < size; i++) {
-            object.add(i);
+            object.add(String.valueOf(i));
             object.add(object);
             object.add(new ArrayList<>(object));
         }
         final Collection<?> result = testCompress840(object);
         assertNotNull(result);
-        assertEquals(size, result.size());
+        // At each iteration, the previous flattened values and the new scalar 
appear twice:
+        // once in the original list and once in its copy. Therefore, f(n) = 2 
* (f(n - 1) + 1),
+        // with f(0) = 0, which gives f(n) = 2^(n + 1) - 2.
+        assertEquals((1 << (size + 1)) - 2, result.size());
         object.add(object);
         testCompress840(object);
     }
diff --git 
a/src/test/java/org/apache/commons/configuration2/convert/TestDefaultListDelimiterHandler.java
 
b/src/test/java/org/apache/commons/configuration2/convert/TestDefaultListDelimiterHandler.java
index 5a97c98ed..5d40362a2 100644
--- 
a/src/test/java/org/apache/commons/configuration2/convert/TestDefaultListDelimiterHandler.java
+++ 
b/src/test/java/org/apache/commons/configuration2/convert/TestDefaultListDelimiterHandler.java
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
@@ -175,4 +176,29 @@ public class TestDefaultListDelimiterHandler {
     void testSplitUnexpectedEscape() {
         checkSplit("\\x, \\,y, \\", true, "\\x", ",y", "\\");
     }
+
+    /**
+     * Tests whether flatten() skips a recursive array reference while keeping 
the reachable leaf values.
+     */
+    @Test
+    void testFlattenArrayCycle() {
+        final Object[] array = new Object[2];
+        array[0] = "value1,value2";
+        array[1] = array;
+
+        assertIterableEquals(Arrays.asList("value1", "value2"), 
handler.flatten(array, Integer.MAX_VALUE));
+    }
+
+    /**
+     * Tests whether flatten() skips a recursive list-array cycle while 
keeping the reachable leaf values.
+     */
+    @Test
+    void testFlattenMixedListAndArrayCycle() {
+        final List<Object> list = new ArrayList<>();
+        final Object[] array = {list};
+        list.add("value1,value2");
+        list.add(array);
+
+        assertIterableEquals(Arrays.asList("value1", "value2"), 
handler.flatten(list, Integer.MAX_VALUE));
+    }
 }

Reply via email to