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

thiagohp pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 040a09026f8092c33c44486bf8ec5b123d75ea1c
Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br>
AuthorDate: Fri Oct 25 19:59:51 2024 -0300

    TAP5-2793: new component for for rendering recursive data
---
 .../apache/tapestry5/commons/RecursiveValue.java   |  42 +++
 .../tapestry5/commons/RecursiveValueProvider.java  |  39 +++
 .../tapestry5/corelib/components/Recursive.java    | 316 +++++++++++++++++++++
 .../corelib/components/RecursiveBody.java          |  67 +++++
 .../tapestry5/internal/RecursiveContext.java       |  68 +++++
 .../integration/app1/CoreBehaviorsTests.java       |  35 ++-
 .../tapestry5/integration/app1/data/Category.java  |  47 +++
 .../tapestry5/integration/app1/pages/Index.java    |   2 +
 .../integration/app1/pages/RecursiveDemo.java      |  43 +++
 .../integration/app1/services/AppModule.java       |  58 +++-
 .../integration/app1/pages/RecursiveDemo.tml       |  39 +++
 .../tapestry5/ioc/modules/TapestryIOCModule.java   |   9 +
 12 files changed, 759 insertions(+), 6 deletions(-)

diff --git 
a/commons/src/main/java/org/apache/tapestry5/commons/RecursiveValue.java 
b/commons/src/main/java/org/apache/tapestry5/commons/RecursiveValue.java
new file mode 100644
index 000000000..87313f5b7
--- /dev/null
+++ b/commons/src/main/java/org/apache/tapestry5/commons/RecursiveValue.java
@@ -0,0 +1,42 @@
+// Licensed to 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.tapestry5.commons;
+
+import java.util.List;
+
+/**
+ * <p>
+ * Interface that represents a parent-children relationship. It's used
+ * in the Tapestry's <code>Recursive</code> component.
+ * </p>
+ * <p>
+ * This was contributed by <a href="https://www.pubfactory.com";>KGL 
PubFactory</a>.
+ * </p>
+ * @since 5.9.0
+ */
+public interface RecursiveValue<T> 
+{
+
+    /**
+     * Returns the list of children for a given value.
+     * @return a {@link java.util.List}.
+     */
+    List<RecursiveValue<?>> getChildren();
+    
+    /**
+     * Returns the original object related to this value.
+     * @return an {@link Object}
+     */
+    T getValue();
+    
+}
\ No newline at end of file
diff --git 
a/commons/src/main/java/org/apache/tapestry5/commons/RecursiveValueProvider.java
 
b/commons/src/main/java/org/apache/tapestry5/commons/RecursiveValueProvider.java
new file mode 100644
index 000000000..61840a711
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/tapestry5/commons/RecursiveValueProvider.java
@@ -0,0 +1,39 @@
+// Licensed to 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.tapestry5.commons;
+
+import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
+
+/**
+ * <p>
+ * Interface implemented by classes which converts objects to
+ * {@link RecursiveValue} instances.
+ * </p>
+ * <p>
+ * This was contributed by <a href="https://www.pubfactory.com";>KGL 
PubFactory</a>.
+ * </p>
+ * @since 5.9.0
+ */
+@UsesOrderedConfiguration(RecursiveValueProvider.class)
+public interface RecursiveValueProvider
+{
+
+    /**
+     * Returns a {@link RecursiveValue} for this <code>object</code> or
+     * returns <code>null</code> if the object isn't handled.
+     * @param object an {@link Object}.
+     * @return a {@link RecursiveValue} or <code>null</code>.
+     */
+    RecursiveValue<?> get(Object object);
+    
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Recursive.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Recursive.java
new file mode 100644
index 000000000..ca3129d06
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Recursive.java
@@ -0,0 +1,316 @@
+// Licensed to 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.tapestry5.corelib.components;
+
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.tapestry5.BindingConstants;
+import org.apache.tapestry5.ComponentResources;
+import org.apache.tapestry5.MarkupWriter;
+import org.apache.tapestry5.annotations.Parameter;
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.commons.RecursiveValue;
+import org.apache.tapestry5.commons.RecursiveValueProvider;
+import org.apache.tapestry5.dom.Element;
+import org.apache.tapestry5.dom.Node;
+import org.apache.tapestry5.internal.RecursiveContext;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.services.Environment;
+import org.apache.tapestry5.services.javascript.JavaScriptSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <p>
+ * {@link Loop}-like component that renders its templates recursively based on 
{@link Recursive} parent-child relationships.
+ * The objects should have one or more corresponding {@link 
RecursiveValueProvider}
+ * implementations to convert them to {@link Recursive} instances.
+ * The insertion point for rendering children is defined by the 
+ * {@link RecursiveBody} component, which can only be used once inside
+ * a <code>Recursive</code> instance. 
+ * </p>
+ * <p>
+ * This was contributed by <a href="https://www.pubfactory.com";>KGL 
PubFactory</a>.
+ * </p>
+ * @since 5.9.0
+ */
+public class Recursive implements RecursiveContext.Provider
+{
+    
+    private static final Logger logger = 
LoggerFactory.getLogger(Recursive.class);
+
+    static final String RECURSIVE_INSERTION_POINT_ELEMENT_NAME = 
"recursiveInsertionPoint";
+    
+    static final String ITERATION_WRAPPER_ELEMENT_NAME = "iterationWrapper";
+    
+    static final String PLACEHOLDER_PREFIX = "placeholder-";
+
+    private static final int ZERO = 0;
+
+    /**
+     * A list containing the objects instances to be rendered. It can be just 
the root
+     * elements (the ones without a parent) or all of them: this component 
takes care of both scenarios.
+     */
+    @Parameter(required = true, allowNull = false)
+    private Iterable<?> source;
+    
+    /**
+     * The max depth to render.  A value of null or <= 0 will result in 
rendering the entire tree.
+     */
+    @Parameter(required = false, allowNull = true)
+    private Integer depth;
+    
+   /**
+     * The desired client id, which defaults to the component's id.  If ever 
using nested Recursive
+     * components, it is critical that each use a unique clientId.  This value 
is the root used to
+     * build placeholder identifiers elements that are used to in the 
cleanupRender phase to restructure
+     * the DOM into the necessary recursive tree structure.  If a nested 
Recursive component is using
+     * the same clientId, it can end up mixing up the nodes of the two 
recursive trees.
+     */
+    @Parameter(value = "prop:resources.id", defaultPrefix = 
BindingConstants.LITERAL)
+    private String clientId;
+    
+    /**
+     * The current depth of the recursion.
+     */
+    @Parameter
+    private int currentDepth;
+    
+    /**
+     * Current value being rendered.
+     */
+    @Parameter
+    private Object value;
+    
+    private Iterator<RecursiveValue<?>> iterator;
+    
+    @Inject
+    @Property
+    private ComponentResources resources;
+    
+    private RecursiveValue<?> recursiveValue;
+    
+    @Inject
+    private Environment environment;
+    
+    @Inject
+    private RecursiveValueProvider recursiveValueProvider;
+    
+    @Inject
+    private JavaScriptSupport javaScriptSupport;
+    
+    private Map<String, String> childToParentMap;
+    
+    private Map<String, RecursiveValue<?>> idToRecursiveValueMap;
+    
+    private Map<String, Integer> idToDepthMap;
+    
+    private Element wrapper;
+    
+    private List<Element> iterationWrappers;
+    
+    private List<Element> placeholders;
+    
+    private Set<String> ids;
+    
+    void setupRender(MarkupWriter writer) {
+        
+        idToRecursiveValueMap = new HashMap<>();
+        idToDepthMap = new HashMap<>();
+        ids = new HashSet<>();
+        iterationWrappers = new ArrayList<Element>();
+        placeholders = new ArrayList<Element>();
+        wrapper = writer.element("wrapper");
+        childToParentMap = new HashMap<String, String>();
+        environment.push(RecursiveContext.class, new RecursiveContext(this));
+        
+        // Visit values in tree order
+        List<RecursiveValue<?>> toStack = new ArrayList<RecursiveValue<?>>();
+        Iterator<?> sourceIterator = source.iterator();
+        int i = 1;
+        while (sourceIterator.hasNext()) {
+            Object valueFromSource = sourceIterator.next();
+            RecursiveValue<?> value;
+            if (valueFromSource instanceof RecursiveValue) {
+                value = (RecursiveValue<?>) valueFromSource;
+            }
+            else {
+                value = recursiveValueProvider.get(valueFromSource);
+            }
+            if (value == null) {
+                throw new RuntimeException("No RecursiveValue object provided 
for " + value + ". You may need to write a RecursiveValueProvider.");
+            }
+            addToStack(value, toStack, getClientId(String.valueOf(i)));
+            i++;
+        }
+            
+        iterator = toStack.iterator();
+        recursiveValue = iterator.hasNext() ? iterator.next() : null;
+        
+    }
+
+    private void addToStack(RecursiveValue<?> value, List<RecursiveValue<?>> 
stack, String id) {
+        
+        String parentId = StringUtils.substringAfter(childToParentMap.get(id), 
PLACEHOLDER_PREFIX);
+        Integer itemDepth = parentId == null ? ZERO : 
idToDepthMap.get(parentId) + 1;
+        
+        if (depth == null || depth <= 0 || depth > itemDepth) {
+
+            // avoiding having the same value rendered twice
+            if (!stack.contains(value)) {
+                stack.add(value);
+                idToRecursiveValueMap.put(id, value);
+                idToDepthMap.put(id,  itemDepth);
+            }
+            int i = 1;
+            final List<RecursiveValue<?>> children = value.getChildren();
+            if (children != null && !children.isEmpty()) {
+                for (RecursiveValue<?> child : children) {
+                    if (!ids.contains(id)) {
+                        final String childId = id + "-" + i;
+                        childToParentMap.put(childId, 
getPlaceholderClientId(id));
+                        addToStack(child, stack, childId);
+                        ids.add(childId);
+                        i++;
+                    }
+                    else {
+                        throw new RuntimeException("Two different objects with 
the same id: " + id);
+                    }
+                }
+            }
+        }
+    }
+    
+    boolean beginRender(MarkupWriter writer) {
+        final boolean continueRendering = recursiveValue != null;
+        
+        // Implemented this way so we don't have to rely on RecursiveValue 
implementations
+        // to have a good hashCode() implementation.
+        String id = findCurrentRecursiveValueId(recursiveValue);
+        currentDepth = idToDepthMap.get(id) != null ? idToDepthMap.get(id) : 0;
+        iterationWrappers.add(writer.element(ITERATION_WRAPPER_ELEMENT_NAME, 
"id", id));
+        value = recursiveValue != null ? recursiveValue.getValue() : null;
+        return continueRendering; 
+    }
+
+    private String findCurrentRecursiveValueId(RecursiveValue<?> 
recursiveValue) {
+        String id = null;
+        final Set<Entry<String, RecursiveValue<?>>> entrySet = 
idToRecursiveValueMap.entrySet();
+        for (Entry<String, RecursiveValue<?>> entry : entrySet) {
+            if (entry.getValue() == recursiveValue) {
+                id = entry.getKey();
+            }
+        }
+        return id;
+    }
+    
+    boolean afterRender(MarkupWriter writer) {
+        writer.end(); // iterationWrapper
+        recursiveValue = iterator.hasNext() ? iterator.next() : null;
+        return recursiveValue == null;
+    }
+    
+    void cleanupRender(MarkupWriter writer) {
+        writer.end(); // wrapper
+        environment.pop(RecursiveContext.class);
+        
+        // place the elements inside the correct placeholders
+        for (Element iterationWrapper : iterationWrappers) {
+            final String id = iterationWrapper.getAttribute("id");
+            final String parentId = childToParentMap.get(id);
+            if (parentId != null) {
+                Element placeholder = wrapper.getElementById(parentId);
+                if (placeholder != null) {
+                    // not using iterationWrapper.moveToBottom(placeholder) 
because of a bug on Tapestry 5.1.0.x
+                    for (Node node : iterationWrapper.getChildren()) {
+                        try {
+                            node.moveToBottom(placeholder);
+                        }
+                        catch (IllegalArgumentException e) {
+                            logger.error(e.getMessage() + " " + node + " " + 
placeholder);
+                        }
+                    }
+                }
+                // iterationWrapper.moveToBottom(placeholder);
+                iterationWrapper.remove();
+            }
+            else {
+                // not using iterationWrapper.pop() because of a bug on 
Tapestry 5.1.0.x.
+                // important to iterate over the children in reverse order so 
that when we move them after the
+                // iterationWrapper, they're in the same order they were in 
before the move
+                List<Node> children = iterationWrapper.getChildren();
+                for (int i = children.size(); i > 0; i --) {
+                    children.get(i - 1).moveAfter(iterationWrapper);
+                }
+                iterationWrapper.remove();
+            }
+        }
+        
+        // remove the placeholders
+        for (Element placeholder : placeholders) {
+            placeholder.pop();
+        }
+        
+        childToParentMap.clear();
+        childToParentMap = null;
+        placeholders.clear();
+        placeholders = null;
+        iterationWrappers.clear();
+        iterationWrappers = null;
+        wrapper.pop();
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+    
+    public String getClientId(String value) {
+        return getClientId() + "-" + encode(value);
+    }
+    
+    public String getPlaceholderClientId(String value) {
+        return PLACEHOLDER_PREFIX + encode(value);
+    }
+    
+    @SuppressWarnings("deprecation")
+    final private String encode(String value) {
+        // TODO: when Java 8 support is dropped, change line 
+        // below to URLEncoder.encode(value, StandardCharsets.UTF_8);
+        return URLEncoder.encode(value);
+    }
+
+    @Override
+    public RecursiveValue<?> getCurrent() {
+        return recursiveValue;
+    }
+
+    @Override
+    public String getClientIdForCurrent() {
+        return 
getPlaceholderClientId(findCurrentRecursiveValueId(getCurrent()));
+    }
+
+    @Override
+    public void registerPlaceholder(Element element) {
+        placeholders.add(element);
+    }
+
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/RecursiveBody.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/RecursiveBody.java
new file mode 100644
index 000000000..6bb8aa925
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/RecursiveBody.java
@@ -0,0 +1,67 @@
+// Licensed to 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.tapestry5.corelib.components;
+
+import org.apache.tapestry5.MarkupWriter;
+import org.apache.tapestry5.annotations.Environmental;
+import org.apache.tapestry5.dom.Element;
+import org.apache.tapestry5.internal.RecursiveContext;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.services.Environment;
+
+/**
+ * <p>
+ * Component that marks the place in the template the
+ * recursion should happen. It should only be used inside
+ * {@link Recursive}, otherwise an exception will be
+ * thrown.
+ * </p>
+ * <p>
+ * This was contributed by <a href="https://www.pubfactory.com";>KGL 
PubFactory</a>.
+ * </p>
+ * @since 5.9.0
+ */
+public class RecursiveBody 
+{
+    
+    @Inject
+    private Environment environment;
+    
+    @Environmental
+    private RecursiveContext context;
+
+    void beginRender(MarkupWriter writer) 
+    {
+        final RecursiveContext recursiveContext = 
environment.peek(RecursiveContext.class);
+        if (recursiveContext != null) 
+        {
+            final Element placeholder = writer
+                    .element(
+                            Recursive.RECURSIVE_INSERTION_POINT_ELEMENT_NAME,
+                            "id", recursiveContext.getId());
+            context.registerPlaceholder(placeholder);
+        }
+        
+    }
+
+    boolean beginRenderTemplate(MarkupWriter writer) 
+    {
+        return false; // throw away any body this component instance might 
have in its declaration
+    }
+
+    void afterRender(MarkupWriter writer) 
+    {
+        writer.end();
+    }
+
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/RecursiveContext.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/RecursiveContext.java
new file mode 100644
index 000000000..c677b7022
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/RecursiveContext.java
@@ -0,0 +1,68 @@
+// Licensed to 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.tapestry5.internal;
+
+import org.apache.tapestry5.commons.RecursiveValue;
+import org.apache.tapestry5.corelib.components.Recursive;
+import org.apache.tapestry5.corelib.components.RecursiveBody;
+import org.apache.tapestry5.dom.Element;
+
+/**
+ * <p>
+ * Class that makes the link between {@link Recursive} and {@link 
RecursiveBody}. 
+ * </p>
+ * <p>
+ * This was contributed by <a href="https://www.pubfactory.com";>KGL 
PubFactory</a>.
+ * </p>
+ * @since 5.9.0
+ */
+final public class RecursiveContext
+{
+
+    private final Provider provider;
+
+    public RecursiveContext(Provider provider) 
+    {
+        this.provider = provider;
+    }
+
+    public String getId() 
+    {
+        return provider.getClientIdForCurrent();
+    }
+
+    public RecursiveValue<?> getCurrent() 
+    {
+        return provider.getCurrent();
+    }
+
+    public Object getValue() 
+    {
+        return provider.getCurrent().getValue();
+    }
+
+    public void registerPlaceholder(Element element) 
+    {
+        provider.registerPlaceholder(element);
+    }
+
+    public static interface Provider 
+    {
+        RecursiveValue<?> getCurrent();
+
+        String getClientIdForCurrent();
+
+        void registerPlaceholder(Element element);
+    }
+    
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CoreBehaviorsTests.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CoreBehaviorsTests.java
index b5734b110..3cd3d1e1d 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CoreBehaviorsTests.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CoreBehaviorsTests.java
@@ -1825,9 +1825,42 @@ public class CoreBehaviorsTests extends App1TestCase
     }
 
     @Test
-    public void event_handler_that_overrides_abstract_method_invoked_once() {
+    public void event_handler_that_overrides_abstract_method_invoked_once() 
+    {
         openLinks("Event Handler Override Demo", "Trigger");
 
         assertTextSeries("//ul[@id='method-names']/li[%d]", 1, "sub-class", 
"DONE");
     }
+    
+    @Test
+    public void recursive_component() 
+    {
+        openLinks("Recursive Demo");
+
+        final String locatorEnd = "/span";
+        final String level1format = "//ul[@id='%s']/li[%d]" + locatorEnd;
+        final String level2format = level1format.replace(locatorEnd, "") + 
"/ul/li[%d]" + locatorEnd;
+        final String level3format = level2format.replace(locatorEnd, "") + 
"/ul/li[%d]" + locatorEnd;
+        final String noMaxDepth = "noMaxDepth";
+        final String maxDepth2 = "maxDepth2";
+        
+        assertText(String.format(level1format, noMaxDepth, 1), "Category 1");
+        assertText(String.format(level2format, noMaxDepth, 1, 1), "Category 
1.1");
+        assertText(String.format(level3format, noMaxDepth, 1, 1, 1), "Category 
1.1.1");
+        assertText(String.format(level2format, noMaxDepth, 1, 2), "Category 
1.2");
+        assertText(String.format(level1format, noMaxDepth, 3), "Category 3");
+        assertText(String.format(level2format, noMaxDepth, 3, 2), "Category 
3.2");
+        assertText(String.format(level3format, noMaxDepth, 3, 1, 1), "Category 
3.1.1");
+
+        assertText(String.format(level1format, maxDepth2, 1), "Category 1");
+        assertText(String.format(level2format, maxDepth2, 1, 1), "Category 
1.1");
+        assertText(String.format(level2format, maxDepth2, 1, 2), "Category 
1.2");
+        assertText(String.format(level1format, maxDepth2, 3), "Category 3");
+        assertText(String.format(level2format, maxDepth2, 3, 2), "Category 
3.2");
+        
+        assertFalse(isElementPresent("//ul[@id='maxDepth2']//li//li//li"),
+                "Depth set to 2, so we shouldn't have 3rd level list items.");
+
+    }
+    
 }
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/Category.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/Category.java
new file mode 100644
index 000000000..40acf57c8
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/Category.java
@@ -0,0 +1,47 @@
+// Licensed to 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.tapestry5.integration.app1.data;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class Category
+{
+
+    final String name;
+    
+    final List<Category> children;
+    
+    public Category(String name, Category ... children)
+    {
+        this(name, Arrays.asList(children));
+    }
+
+    public Category(String name, List<Category> children) 
+    {
+        super();
+        this.name = name;
+        this.children = children;
+    }
+    
+    public String getName() 
+    {
+        return name;
+    }
+
+    public List<Category> getChildren() 
+    {
+        return children;
+    }
+
+}
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
index 75328a876..5fcc49528 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
@@ -627,6 +627,8 @@ public class Index
                     
                     new Item("IfDemo","If Demo","If component with all its 
options"),
                     
+                    new Item("RecursiveDemo","Recursive Demo","Recursive 
component example"),
+                    
                     new Item("SelfRecursiveDemo", "Self-Recursive Demo", 
"check for handling of self-recursive components")
                 );
 
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.java
new file mode 100644
index 000000000..cedaf12b8
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.java
@@ -0,0 +1,43 @@
+// Licensed to 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.tapestry5.integration.app1.pages;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.integration.app1.data.Category;
+
+public class RecursiveDemo
+{
+    
+    @Property
+    private Category category;
+    
+    public List<Category> getCategories()
+    {
+        List<Category> categories = new ArrayList<>();
+        
+        for (int i = 1; i <= 3; i++)
+        {
+            final String name = "Category " + i;
+            categories.add(new Category(name,
+                    new Category(name + ".1", new Category(name + ".1.1")), 
+                    new Category(name + ".2")));
+        }
+        
+        return categories;
+        
+    }
+
+}
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
index 3851f684d..6801928b8 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
@@ -22,12 +22,15 @@ import java.lang.annotation.Target;
 import java.net.URL;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.apache.tapestry5.SymbolConstants;
 import org.apache.tapestry5.ValueEncoder;
 import org.apache.tapestry5.commons.Configuration;
 import org.apache.tapestry5.commons.MappedConfiguration;
 import org.apache.tapestry5.commons.OrderedConfiguration;
+import org.apache.tapestry5.commons.RecursiveValue;
+import org.apache.tapestry5.commons.RecursiveValueProvider;
 import org.apache.tapestry5.commons.Resource;
 import org.apache.tapestry5.commons.util.CollectionFactory;
 import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
@@ -37,18 +40,16 @@ import org.apache.tapestry5.http.services.RequestFilter;
 import org.apache.tapestry5.http.services.RequestHandler;
 import org.apache.tapestry5.http.services.Response;
 import org.apache.tapestry5.integration.app1.data.Address;
+import org.apache.tapestry5.integration.app1.data.Category;
 import org.apache.tapestry5.integration.app1.data.Entity;
 import org.apache.tapestry5.integration.app1.data.ToDoItem;
 import org.apache.tapestry5.integration.app1.data.Track;
 import org.apache.tapestry5.internal.services.GenericValueEncoderFactory;
 import org.apache.tapestry5.ioc.ServiceBinder;
 import org.apache.tapestry5.ioc.annotations.Contribute;
-import org.apache.tapestry5.ioc.annotations.ImportModule;
 import org.apache.tapestry5.ioc.annotations.Marker;
 import org.apache.tapestry5.ioc.annotations.Value;
 import org.apache.tapestry5.ioc.services.ServiceOverride;
-import org.apache.tapestry5.modules.Bootstrap4Module;
-import org.apache.tapestry5.modules.NoBootstrapModule;
 import org.apache.tapestry5.services.BeanBlockContribution;
 import org.apache.tapestry5.services.BeanBlockSource;
 import org.apache.tapestry5.services.ComponentClassResolver;
@@ -57,8 +58,6 @@ import org.apache.tapestry5.services.LibraryMapping;
 import org.apache.tapestry5.services.ResourceDigestGenerator;
 import org.apache.tapestry5.services.ValueEncoderFactory;
 import org.apache.tapestry5.services.ValueLabelProvider;
-import org.apache.tapestry5.services.compatibility.Compatibility;
-import org.apache.tapestry5.services.compatibility.Trait;
 import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService;
 import org.apache.tapestry5.services.pageload.PagePreloader;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
@@ -429,4 +428,53 @@ public class AppModule
                 ? ReferenceType.STRONG : null);
     }
 
+    /**
+     * Builds the {@link RecursiveValueProvider} service.
+     * @since 5.9.0
+     */
+    public static void 
contributeRecursiveValueProvider(OrderedConfiguration<RecursiveValueProvider> 
configuration) 
+    {
+        configuration.add("Category", new CategoryRecursiveValueProvider());
+    }
+    
+    private static final class CategoryRecursiveValueProvider implements 
RecursiveValueProvider
+    {
+
+        @Override
+        public RecursiveValue<?> get(Object object) 
+        {
+            return (object instanceof Category) ? new 
CategoryRecursiveValue((Category) object) : null;
+        }
+        
+    }
+    
+    private static final class CategoryRecursiveValue implements 
RecursiveValue<Category>
+    {
+        
+        public CategoryRecursiveValue(Category category) {
+            super();
+            this.category = category;
+        }
+
+        final private Category category;
+
+        @Override
+        public List<RecursiveValue<?>> getChildren() {
+            return category.getChildren().stream()
+                    .map(CategoryRecursiveValue::new)
+                    .collect(Collectors.toList());
+        }
+
+        @Override
+        public Category getValue() {
+            return category;
+        }
+
+        @Override
+        public String toString() {
+            return category.getName();
+        }
+        
+    }
+
 }
diff --git 
a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.tml
 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.tml
new file mode 100644
index 000000000..f85b6f6b5
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.tml
@@ -0,0 +1,39 @@
+<html t:type="Border" 
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd";>
+<h1>Recursive Demo</h1>
+
+<p> Simple example of Recursive component usage</p>
+
+<p>
+       No maximum depth.
+</p>
+
+<ul id="noMaxDepth">
+       <t:recursive t:source="categories" value="category">
+               <li>
+                       <span>${category.name}</span>
+                       <ul t:type="If" t:test="!category.children.empty">
+                               <t:RecursiveBody/>
+                       </ul>
+               </li>
+       
+       </t:recursive>
+</ul>
+
+<p>
+       Maximum depth set to 2.
+</p>
+
+
+<ul id="maxDepth2">
+       <t:recursive t:source="categories" value="category" depth="2">
+               <li>
+                       <span>${category.name}</span>
+                       <ul t:type="If" t:test="!category.children.empty">
+                               <t:RecursiveBody/>
+                       </ul>
+               </li>
+       </t:recursive>
+</ul>
+
+
+</html>
diff --git 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/modules/TapestryIOCModule.java
 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/modules/TapestryIOCModule.java
index b6067c2e9..2aeed2235 100644
--- 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/modules/TapestryIOCModule.java
+++ 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/modules/TapestryIOCModule.java
@@ -399,4 +399,13 @@ public final class TapestryIOCModule
                 () ->  periodicExecutor.init());
     }
     
+    /**
+     * Builds the {@link RecursiveValueProvider} service.
+     * @since 5.9.0
+     */
+    public static RecursiveValueProvider 
buildRecursiveValueProvider(List<RecursiveValueProvider> providers, 
ChainBuilder chainBuilder) 
+    {
+        return chainBuilder.build(RecursiveValueProvider.class, providers);
+    }
+    
 }

Reply via email to