You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2024/10/25 23:01:33 UTC
(tapestry-5) branch master updated (0419f80a3 -> 040a09026)
This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
from 0419f80a3 TAP5-2792: POST with body throws exception
new cb35c7e2c TAP5-2793: renaming test app1's Recursive to SelfRecursive
new 040a09026 TAP5-2793: new component for for rendering recursive data
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../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 +++++
tapestry-core/src/test/app1/Index.tml | 2 +-
tapestry-core/src/test/app1/RecursiveDemo.tml | 5 -
tapestry-core/src/test/app1/SelfRecursiveDemo.tml | 5 +
.../integration/app1/CoreBehaviorsTests.java | 41 ++-
.../{Recursive.java => SelfRecursive.java} | 4 +-
.../data/{BirthdayReminder.java => Category.java} | 40 ++-
.../tapestry5/integration/app1/pages/Index.java | 6 +-
.../integration/app1/pages/RecursiveDemo.java | 29 +-
...vironmentalDemo.java => SelfRecursiveDemo.java} | 4 +-
.../integration/app1/services/AppModule.java | 58 +++-
.../{Recursive.tml => SelfRecursive.tml} | 2 +-
.../integration/app1/pages/RecursiveDemo.tml | 39 +++
.../tapestry5/ioc/modules/TapestryIOCModule.java | 9 +
18 files changed, 731 insertions(+), 45 deletions(-)
create mode 100644 commons/src/main/java/org/apache/tapestry5/commons/RecursiveValue.java
create mode 100644 commons/src/main/java/org/apache/tapestry5/commons/RecursiveValueProvider.java
create mode 100644 tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Recursive.java
create mode 100644 tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/RecursiveBody.java
create mode 100644 tapestry-core/src/main/java/org/apache/tapestry5/internal/RecursiveContext.java
delete mode 100644 tapestry-core/src/test/app1/RecursiveDemo.tml
create mode 100644 tapestry-core/src/test/app1/SelfRecursiveDemo.tml
rename tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/{Recursive.java => SelfRecursive.java} (88%)
copy tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/{BirthdayReminder.java => Category.java} (55%)
copy tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/{EnvironmentalDemo.java => SelfRecursiveDemo.java} (87%)
rename tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/{Recursive.tml => SelfRecursive.tml} (54%)
create mode 100644 tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.tml
(tapestry-5) 01/02: TAP5-2793: renaming test app1's Recursive to SelfRecursive
Posted by th...@apache.org.
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 cb35c7e2c16963c4f0c8d9d79b1649fa9f1be7e2
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Fri Oct 25 16:24:55 2024 -0300
TAP5-2793: renaming test app1's Recursive to SelfRecursive
---
tapestry-core/src/test/app1/Index.tml | 2 +-
tapestry-core/src/test/app1/RecursiveDemo.tml | 5 -----
tapestry-core/src/test/app1/SelfRecursiveDemo.tml | 5 +++++
.../org/apache/tapestry5/integration/app1/CoreBehaviorsTests.java | 6 +++---
.../app1/components/{Recursive.java => SelfRecursive.java} | 4 ++--
.../java/org/apache/tapestry5/integration/app1/pages/Index.java | 4 +++-
.../app1/pages/{RecursiveDemo.java => SelfRecursiveDemo.java} | 4 ++--
.../app1/components/{Recursive.tml => SelfRecursive.tml} | 2 +-
8 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/tapestry-core/src/test/app1/Index.tml b/tapestry-core/src/test/app1/Index.tml
index 36a8fbf7c..e47d20d44 100644
--- a/tapestry-core/src/test/app1/Index.tml
+++ b/tapestry-core/src/test/app1/Index.tml
@@ -87,7 +87,7 @@
</li>
<li>
- <a href="recursivedemo">Recursive Demo</a>
+ <a href="recursivedemo">Self-Recursive Demo</a>
-- check for handling of recursive components
</li>
diff --git a/tapestry-core/src/test/app1/RecursiveDemo.tml b/tapestry-core/src/test/app1/RecursiveDemo.tml
deleted file mode 100644
index ff71df5ca..000000000
--- a/tapestry-core/src/test/app1/RecursiveDemo.tml
+++ /dev/null
@@ -1,5 +0,0 @@
-<html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
-
- This page contains a <t:recursive>Recursive component</t:recursive>, so it will never be able to render itself.
-
-</html>
diff --git a/tapestry-core/src/test/app1/SelfRecursiveDemo.tml b/tapestry-core/src/test/app1/SelfRecursiveDemo.tml
new file mode 100644
index 000000000..6433fe6b4
--- /dev/null
+++ b/tapestry-core/src/test/app1/SelfRecursiveDemo.tml
@@ -0,0 +1,5 @@
+<html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
+
+ This page contains a <t:selfrecursive>Recursive component</t:selfrecursive>, so it will never be able to render itself.
+
+</html>
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 e5f11d07d..b5734b110 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
@@ -542,12 +542,12 @@ public class CoreBehaviorsTests extends App1TestCase
@Test
public void recursive_components_are_identified_as_errors()
{
- openLinks("Recursive Demo");
+ openLinks("Self-Recursive Demo");
assertTextPresent(
EXCEPTION_PROCESSING_REQUEST,
- "The template for component org.apache.tapestry5.integration.app1.components.Recursive is recursive (contains another direct or indirect reference to component org.apache.tapestry5.integration.app1.components.Recursive). This is not supported (components may not contain themselves).",
- "component is <t:recursive>recursive</t:recursive>, so we\'ll see a failure.");
+ "The template for component org.apache.tapestry5.integration.app1.components.SelfRecursive is recursive (contains another direct or indirect reference to component org.apache.tapestry5.integration.app1.components.SelfRecursive). This is not supported (components may not contain themselves).",
+ "component is <t:SelfRecursive>recursive</t:SelfRecursive>, so we\'ll see a failure.");
}
@Test
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Recursive.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/SelfRecursive.java
similarity index 88%
rename from tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Recursive.java
rename to tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/SelfRecursive.java
index 9df84ca3c..f61cbcf0b 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Recursive.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/SelfRecursive.java
@@ -1,4 +1,4 @@
-// Copyright 2007 The Apache Software Foundation
+// Copyright 2007, 2024 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
package org.apache.tapestry5.integration.app1.components;
-public class Recursive
+public class SelfRecursive
{
}
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 8566ff258..75328a876 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
@@ -625,7 +625,9 @@ public class Index
new Item("ObjectEditorDemo","Object Editor Demo","Edit Bean with address objects"),
- new Item("IfDemo","If Demo","If component with all its options")
+ new Item("IfDemo","If Demo","If component with all its options"),
+
+ new Item("SelfRecursiveDemo", "Self-Recursive Demo", "check for handling of self-recursive components")
);
static
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/SelfRecursiveDemo.java
similarity index 87%
rename from tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/RecursiveDemo.java
rename to tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/SelfRecursiveDemo.java
index 30232a7e4..65e8d0722 100644
--- 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/SelfRecursiveDemo.java
@@ -1,4 +1,4 @@
-// Copyright 2007 The Apache Software Foundation
+// Copyright 2007, 2024 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
package org.apache.tapestry5.integration.app1.pages;
-public class RecursiveDemo
+public class SelfRecursiveDemo
{
}
diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Recursive.tml b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/SelfRecursive.tml
similarity index 54%
rename from tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Recursive.tml
rename to tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/SelfRecursive.tml
index 9f6884582..18bfdce0d 100644
--- a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Recursive.tml
+++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/SelfRecursive.tml
@@ -1,7 +1,7 @@
<div xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<p>
- This component is <t:recursive>recursive</t:recursive>, so we'll see a failure.
+ This component is <t:SelfRecursive>recursive</t:SelfRecursive>, so we'll see a failure.
</p>
<t:body/>
(tapestry-5) 02/02: TAP5-2793: new component for for rendering recursive data
Posted by th...@apache.org.
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 <th...@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);
+ }
+
}