XMLListReference.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under 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.commons.configuration2;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.configuration2.convert.ListDelimiterHandler;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Element;

/**
 * <p>
 * An internal class implementing list handling functionality for {@link XMLConfiguration}.
 * </p>
 * <p>
 * When an XML document is loaded list properties defined as a string with multiple values separated by the list
 * delimiter are split into multiple configuration nodes. When the configuration is saved the original format should be
 * kept if possible. This class implements functionality to achieve this. Instances are used as references associated
 * with configuration nodes so that the original format can be restored when the configuration is saved.
 * </p>
 */
final class XMLListReference {
    /**
     * Assigns an instance of this class as reference to the specified configuration node. This reference acts as a marker
     * indicating that this node is subject to extended list handling.
     *
     * @param refs the mapping for node references
     * @param node the affected configuration node
     * @param elem the current XML element
     */
    public static void assignListReference(final Map<ImmutableNode, Object> refs, final ImmutableNode node, final Element elem) {
        if (refs != null) {
            refs.put(node, new XMLListReference(elem));
        }
    }

    /**
     * Checks whether the specified node has an associated list reference. This marks the node as part of a list.
     *
     * @param node the node to be checked
     * @param handler the reference handler
     * @return a flag whether this node has a list reference
     */
    private static boolean hasListReference(final ImmutableNode node, final ReferenceNodeHandler handler) {
        return handler.getReference(node) instanceof XMLListReference;
    }

    /**
     * Checks whether the specified node is the first node of a list. This is needed because all items of the list are
     * collected and stored as value of the first list node. Note: This method requires that the passed in node is a list
     * node, so {@link #isListNode(ImmutableNode, ReferenceNodeHandler)} must have returned <strong>true</strong> for it.
     *
     * @param node the configuration node
     * @param handler the reference node handler
     * @return a flag whether this is the first node of a list
     */
    public static boolean isFirstListItem(final ImmutableNode node, final ReferenceNodeHandler handler) {
        final ImmutableNode parent = handler.getParent(node);
        ImmutableNode firstItem = null;
        int idx = 0;
        while (firstItem == null) {
            final ImmutableNode child = handler.getChild(parent, idx);
            if (nameEquals(node, child)) {
                firstItem = child;
            }
            idx++;
        }
        return firstItem == node;
    }

    /**
     * Checks whether the specified configuration node has to be taken into account for list handling. This is the case if
     * the node's parent has at least one child node with the same name which has a special list reference assigned. (Note
     * that the passed in node does not necessarily have such a reference; if it has been added at a later point in time, it
     * also has to become an item of the list.)
     *
     * @param node the configuration node
     * @param handler the reference node handler
     * @return a flag whether this node is relevant for list handling
     */
    public static boolean isListNode(final ImmutableNode node, final ReferenceNodeHandler handler) {
        if (hasListReference(node, handler)) {
            return true;
        }

        final ImmutableNode parent = handler.getParent(node);
        if (parent != null) {
            for (int i = 0; i < handler.getChildrenCount(parent, null); i++) {
                final ImmutableNode child = handler.getChild(parent, i);
                if (hasListReference(child, handler) && nameEquals(node, child)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Constructs the concatenated string value of all items comprising the list the specified node belongs to. This method
     * is called when saving an {@link XMLConfiguration}. Then configuration nodes created for list items have to be
     * collected again and transformed into a string defining all list elements.
     *
     * @param node the configuration node
     * @param nodeHandler the reference node handler
     * @param delimiterHandler the list delimiter handler of the configuration
     * @return a string with all values of the current list
     * @throws ConfigurationRuntimeException if the list delimiter handler does not support the transformation of list items
     *         to a string
     */
    public static String listValue(final ImmutableNode node, final ReferenceNodeHandler nodeHandler, final ListDelimiterHandler delimiterHandler) {
        // cannot be null if the current node is a list node
        final ImmutableNode parent = nodeHandler.getParent(node);
        final List<ImmutableNode> items = nodeHandler.getChildren(parent, node.getNodeName());
        final List<Object> values = items.stream().map(ImmutableNode::getValue).collect(Collectors.toList());
        try {
            return String.valueOf(delimiterHandler.escapeList(values, ListDelimiterHandler.NOOP_TRANSFORMER));
        } catch (final UnsupportedOperationException e) {
            throw new ConfigurationRuntimeException("List handling not supported by " + "the current ListDelimiterHandler! Make sure that the same delimiter "
                    + "handler is used for loading and saving the configuration.", e);
        }
    }

    /**
     * Helper method for comparing the names of two nodes.
     *
     * @param n1 node 1
     * @param n2 node 2
     * @return a flag whether these nodes have equal names
     */
    private static boolean nameEquals(final ImmutableNode n1, final ImmutableNode n2) {
        return StringUtils.equals(n2.getNodeName(), n1.getNodeName());
    }

    /** The wrapped XML element. */
    private final Element element;

    /**
     * Private constructor. No instances can be created from other classes.
     *
     * @param e the associated element
     */
    private XMLListReference(final Element e) {
        element = e;
    }

    /**
     * Gets the associated element.
     *
     * @return the associated XML element
     */
    public Element getElement() {
        return element;
    }
}