This is an automated email from the ASF dual-hosted git repository. thiagohp pushed a commit to branch javax in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 86a5f2fd148c1835c86047bba93155317a9c4d93 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 274646023..7acdeaf40 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 229c09e9c..22210bf9f 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); + } + }