001// Licensed to the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import java.net.URLEncoder; 016import java.util.ArrayList; 017import java.util.HashMap; 018import java.util.HashSet; 019import java.util.Iterator; 020import java.util.List; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.Set; 024 025import org.apache.commons.lang3.StringUtils; 026import org.apache.tapestry5.BindingConstants; 027import org.apache.tapestry5.ComponentResources; 028import org.apache.tapestry5.MarkupWriter; 029import org.apache.tapestry5.annotations.Parameter; 030import org.apache.tapestry5.annotations.Property; 031import org.apache.tapestry5.commons.RecursiveValue; 032import org.apache.tapestry5.commons.RecursiveValueProvider; 033import org.apache.tapestry5.dom.Element; 034import org.apache.tapestry5.dom.Node; 035import org.apache.tapestry5.internal.RecursiveContext; 036import org.apache.tapestry5.ioc.annotations.Inject; 037import org.apache.tapestry5.services.Environment; 038import org.apache.tapestry5.services.javascript.JavaScriptSupport; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * <p> 044 * {@link Loop}-like component that renders its templates recursively based on {@link Recursive} parent-child relationships. 045 * The objects should have one or more corresponding {@link RecursiveValueProvider} 046 * implementations to convert them to {@link Recursive} instances. 047 * The insertion point for rendering children is defined by the 048 * {@link RecursiveBody} component, which can only be used once inside 049 * a <code>Recursive</code> instance. 050 * </p> 051 * <p> 052 * This was contributed by <a href="https://www.pubfactory.com">KGL PubFactory</a>. 053 * </p> 054 * @since 5.9.0 055 */ 056public class Recursive implements RecursiveContext.Provider 057{ 058 059 private static final Logger logger = LoggerFactory.getLogger(Recursive.class); 060 061 static final String RECURSIVE_INSERTION_POINT_ELEMENT_NAME = "recursiveInsertionPoint"; 062 063 static final String ITERATION_WRAPPER_ELEMENT_NAME = "iterationWrapper"; 064 065 static final String PLACEHOLDER_PREFIX = "placeholder-"; 066 067 private static final int ZERO = 0; 068 069 /** 070 * A list containing the objects instances to be rendered. It can be just the root 071 * elements (the ones without a parent) or all of them: this component takes care of both scenarios. 072 */ 073 @Parameter(required = true, allowNull = false) 074 private Iterable<?> source; 075 076 /** 077 * The max depth to render. A value of null or <= 0 will result in rendering the entire tree. 078 */ 079 @Parameter(required = false, allowNull = true) 080 private Integer depth; 081 082 /** 083 * The desired client id, which defaults to the component's id. If ever using nested Recursive 084 * components, it is critical that each use a unique clientId. This value is the root used to 085 * build placeholder identifiers elements that are used to in the cleanupRender phase to restructure 086 * the DOM into the necessary recursive tree structure. If a nested Recursive component is using 087 * the same clientId, it can end up mixing up the nodes of the two recursive trees. 088 */ 089 @Parameter(value = "prop:resources.id", defaultPrefix = BindingConstants.LITERAL) 090 private String clientId; 091 092 /** 093 * The current depth of the recursion. 094 */ 095 @Parameter 096 private int currentDepth; 097 098 /** 099 * Current value being rendered. 100 */ 101 @Parameter 102 private Object value; 103 104 private Iterator<RecursiveValue<?>> iterator; 105 106 @Inject 107 @Property 108 private ComponentResources resources; 109 110 private RecursiveValue<?> recursiveValue; 111 112 @Inject 113 private Environment environment; 114 115 @Inject 116 private RecursiveValueProvider recursiveValueProvider; 117 118 @Inject 119 private JavaScriptSupport javaScriptSupport; 120 121 private Map<String, String> childToParentMap; 122 123 private Map<String, RecursiveValue<?>> idToRecursiveValueMap; 124 125 private Map<String, Integer> idToDepthMap; 126 127 private Element wrapper; 128 129 private List<Element> iterationWrappers; 130 131 private List<Element> placeholders; 132 133 private Set<String> ids; 134 135 void setupRender(MarkupWriter writer) { 136 137 idToRecursiveValueMap = new HashMap<>(); 138 idToDepthMap = new HashMap<>(); 139 ids = new HashSet<>(); 140 iterationWrappers = new ArrayList<Element>(); 141 placeholders = new ArrayList<Element>(); 142 wrapper = writer.element("wrapper"); 143 childToParentMap = new HashMap<String, String>(); 144 environment.push(RecursiveContext.class, new RecursiveContext(this)); 145 146 // Visit values in tree order 147 List<RecursiveValue<?>> toStack = new ArrayList<RecursiveValue<?>>(); 148 Iterator<?> sourceIterator = source.iterator(); 149 int i = 1; 150 while (sourceIterator.hasNext()) { 151 Object valueFromSource = sourceIterator.next(); 152 RecursiveValue<?> value; 153 if (valueFromSource instanceof RecursiveValue) { 154 value = (RecursiveValue<?>) valueFromSource; 155 } 156 else { 157 value = recursiveValueProvider.get(valueFromSource); 158 } 159 if (value == null) { 160 throw new RuntimeException("No RecursiveValue object provided for " + value + ". You may need to write a RecursiveValueProvider."); 161 } 162 addToStack(value, toStack, getClientId(String.valueOf(i))); 163 i++; 164 } 165 166 iterator = toStack.iterator(); 167 recursiveValue = iterator.hasNext() ? iterator.next() : null; 168 169 } 170 171 private void addToStack(RecursiveValue<?> value, List<RecursiveValue<?>> stack, String id) { 172 173 String parentId = StringUtils.substringAfter(childToParentMap.get(id), PLACEHOLDER_PREFIX); 174 Integer itemDepth = parentId == null ? ZERO : idToDepthMap.get(parentId) + 1; 175 176 if (depth == null || depth <= 0 || depth > itemDepth) { 177 178 // avoiding having the same value rendered twice 179 if (!stack.contains(value)) { 180 stack.add(value); 181 idToRecursiveValueMap.put(id, value); 182 idToDepthMap.put(id, itemDepth); 183 } 184 int i = 1; 185 final List<RecursiveValue<?>> children = value.getChildren(); 186 if (children != null && !children.isEmpty()) { 187 for (RecursiveValue<?> child : children) { 188 if (!ids.contains(id)) { 189 final String childId = id + "-" + i; 190 childToParentMap.put(childId, getPlaceholderClientId(id)); 191 addToStack(child, stack, childId); 192 ids.add(childId); 193 i++; 194 } 195 else { 196 throw new RuntimeException("Two different objects with the same id: " + id); 197 } 198 } 199 } 200 } 201 } 202 203 boolean beginRender(MarkupWriter writer) { 204 final boolean continueRendering = recursiveValue != null; 205 206 // Implemented this way so we don't have to rely on RecursiveValue implementations 207 // to have a good hashCode() implementation. 208 String id = findCurrentRecursiveValueId(recursiveValue); 209 currentDepth = idToDepthMap.get(id) != null ? idToDepthMap.get(id) : 0; 210 iterationWrappers.add(writer.element(ITERATION_WRAPPER_ELEMENT_NAME, "id", id)); 211 value = recursiveValue != null ? recursiveValue.getValue() : null; 212 return continueRendering; 213 } 214 215 private String findCurrentRecursiveValueId(RecursiveValue<?> recursiveValue) { 216 String id = null; 217 final Set<Entry<String, RecursiveValue<?>>> entrySet = idToRecursiveValueMap.entrySet(); 218 for (Entry<String, RecursiveValue<?>> entry : entrySet) { 219 if (entry.getValue() == recursiveValue) { 220 id = entry.getKey(); 221 } 222 } 223 return id; 224 } 225 226 boolean afterRender(MarkupWriter writer) { 227 writer.end(); // iterationWrapper 228 recursiveValue = iterator.hasNext() ? iterator.next() : null; 229 return recursiveValue == null; 230 } 231 232 void cleanupRender(MarkupWriter writer) { 233 writer.end(); // wrapper 234 environment.pop(RecursiveContext.class); 235 236 // place the elements inside the correct placeholders 237 for (Element iterationWrapper : iterationWrappers) { 238 final String id = iterationWrapper.getAttribute("id"); 239 final String parentId = childToParentMap.get(id); 240 if (parentId != null) { 241 Element placeholder = wrapper.getElementById(parentId); 242 if (placeholder != null) { 243 // not using iterationWrapper.moveToBottom(placeholder) because of a bug on Tapestry 5.1.0.x 244 for (Node node : iterationWrapper.getChildren()) { 245 try { 246 node.moveToBottom(placeholder); 247 } 248 catch (IllegalArgumentException e) { 249 logger.error(e.getMessage() + " " + node + " " + placeholder); 250 } 251 } 252 } 253 // iterationWrapper.moveToBottom(placeholder); 254 iterationWrapper.remove(); 255 } 256 else { 257 // not using iterationWrapper.pop() because of a bug on Tapestry 5.1.0.x. 258 // important to iterate over the children in reverse order so that when we move them after the 259 // iterationWrapper, they're in the same order they were in before the move 260 List<Node> children = iterationWrapper.getChildren(); 261 for (int i = children.size(); i > 0; i --) { 262 children.get(i - 1).moveAfter(iterationWrapper); 263 } 264 iterationWrapper.remove(); 265 } 266 } 267 268 // remove the placeholders 269 for (Element placeholder : placeholders) { 270 placeholder.pop(); 271 } 272 273 childToParentMap.clear(); 274 childToParentMap = null; 275 placeholders.clear(); 276 placeholders = null; 277 iterationWrappers.clear(); 278 iterationWrappers = null; 279 wrapper.pop(); 280 } 281 282 public String getClientId() { 283 return clientId; 284 } 285 286 public String getClientId(String value) { 287 return getClientId() + "-" + encode(value); 288 } 289 290 public String getPlaceholderClientId(String value) { 291 return PLACEHOLDER_PREFIX + encode(value); 292 } 293 294 @SuppressWarnings("deprecation") 295 final private String encode(String value) { 296 // TODO: when Java 8 support is dropped, change line 297 // below to URLEncoder.encode(value, StandardCharsets.UTF_8); 298 return URLEncoder.encode(value); 299 } 300 301 @Override 302 public RecursiveValue<?> getCurrent() { 303 return recursiveValue; 304 } 305 306 @Override 307 public String getClientIdForCurrent() { 308 return getPlaceholderClientId(findCurrentRecursiveValueId(getCurrent())); 309 } 310 311 @Override 312 public void registerPlaceholder(Element element) { 313 placeholders.add(element); 314 } 315 316}