    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}