View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2;
18  
19  import java.util.List;
20  import java.util.Map;
21  import java.util.stream.Collectors;
22  
23  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
24  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
25  import org.apache.commons.configuration2.tree.ImmutableNode;
26  import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
27  import org.apache.commons.lang3.StringUtils;
28  import org.w3c.dom.Element;
29  
30  /**
31   * <p>
32   * An internal class implementing list handling functionality for {@link XMLConfiguration}.
33   * </p>
34   * <p>
35   * When an XML document is loaded list properties defined as a string with multiple values separated by the list
36   * delimiter are split into multiple configuration nodes. When the configuration is saved the original format should be
37   * kept if possible. This class implements functionality to achieve this. Instances are used as references associated
38   * with configuration nodes so that the original format can be restored when the configuration is saved.
39   * </p>
40   */
41  final class XMLListReference {
42      /**
43       * Assigns an instance of this class as reference to the specified configuration node. This reference acts as a marker
44       * indicating that this node is subject to extended list handling.
45       *
46       * @param refs the mapping for node references
47       * @param node the affected configuration node
48       * @param elem the current XML element
49       */
50      public static void assignListReference(final Map<ImmutableNode, Object> refs, final ImmutableNode node, final Element elem) {
51          if (refs != null) {
52              refs.put(node, new XMLListReference(elem));
53          }
54      }
55  
56      /**
57       * Checks whether the specified node has an associated list reference. This marks the node as part of a list.
58       *
59       * @param node the node to be checked
60       * @param handler the reference handler
61       * @return a flag whether this node has a list reference
62       */
63      private static boolean hasListReference(final ImmutableNode node, final ReferenceNodeHandler handler) {
64          return handler.getReference(node) instanceof XMLListReference;
65      }
66  
67      /**
68       * Checks whether the specified node is the first node of a list. This is needed because all items of the list are
69       * collected and stored as value of the first list node. Note: This method requires that the passed in node is a list
70       * node, so {@link #isListNode(ImmutableNode, ReferenceNodeHandler)} must have returned <strong>true</strong> for it.
71       *
72       * @param node the configuration node
73       * @param handler the reference node handler
74       * @return a flag whether this is the first node of a list
75       */
76      public static boolean isFirstListItem(final ImmutableNode node, final ReferenceNodeHandler handler) {
77          final ImmutableNode parent = handler.getParent(node);
78          ImmutableNode firstItem = null;
79          int idx = 0;
80          while (firstItem == null) {
81              final ImmutableNode child = handler.getChild(parent, idx);
82              if (nameEquals(node, child)) {
83                  firstItem = child;
84              }
85              idx++;
86          }
87          return firstItem == node;
88      }
89  
90      /**
91       * Checks whether the specified configuration node has to be taken into account for list handling. This is the case if
92       * the node's parent has at least one child node with the same name which has a special list reference assigned. (Note
93       * that the passed in node does not necessarily have such a reference; if it has been added at a later point in time, it
94       * also has to become an item of the list.)
95       *
96       * @param node the configuration node
97       * @param handler the reference node handler
98       * @return a flag whether this node is relevant for list handling
99       */
100     public static boolean isListNode(final ImmutableNode node, final ReferenceNodeHandler handler) {
101         if (hasListReference(node, handler)) {
102             return true;
103         }
104 
105         final ImmutableNode parent = handler.getParent(node);
106         if (parent != null) {
107             for (int i = 0; i < handler.getChildrenCount(parent, null); i++) {
108                 final ImmutableNode child = handler.getChild(parent, i);
109                 if (hasListReference(child, handler) && nameEquals(node, child)) {
110                     return true;
111                 }
112             }
113         }
114         return false;
115     }
116 
117     /**
118      * Constructs the concatenated string value of all items comprising the list the specified node belongs to. This method
119      * is called when saving an {@link XMLConfiguration}. Then configuration nodes created for list items have to be
120      * collected again and transformed into a string defining all list elements.
121      *
122      * @param node the configuration node
123      * @param nodeHandler the reference node handler
124      * @param delimiterHandler the list delimiter handler of the configuration
125      * @return a string with all values of the current list
126      * @throws ConfigurationRuntimeException if the list delimiter handler does not support the transformation of list items
127      *         to a string
128      */
129     public static String listValue(final ImmutableNode node, final ReferenceNodeHandler nodeHandler, final ListDelimiterHandler delimiterHandler) {
130         // cannot be null if the current node is a list node
131         final ImmutableNode parent = nodeHandler.getParent(node);
132         final List<ImmutableNode> items = nodeHandler.getChildren(parent, node.getNodeName());
133         final List<Object> values = items.stream().map(ImmutableNode::getValue).collect(Collectors.toList());
134         try {
135             return String.valueOf(delimiterHandler.escapeList(values, ListDelimiterHandler.NOOP_TRANSFORMER));
136         } catch (final UnsupportedOperationException e) {
137             throw new ConfigurationRuntimeException("List handling not supported by the current ListDelimiterHandler! Make sure that the same delimiter "
138                     + "handler is used for loading and saving the configuration.", e);
139         }
140     }
141 
142     /**
143      * Helper method for comparing the names of two nodes.
144      *
145      * @param n1 node 1
146      * @param n2 node 2
147      * @return a flag whether these nodes have equal names
148      */
149     private static boolean nameEquals(final ImmutableNode n1, final ImmutableNode n2) {
150         return StringUtils.equals(n2.getNodeName(), n1.getNodeName());
151     }
152 
153     /** The wrapped XML element. */
154     private final Element element;
155 
156     /**
157      * Private constructor. No instances can be created from other classes.
158      *
159      * @param e the associated element
160      */
161     private XMLListReference(final Element e) {
162         element = e;
163     }
164 
165     /**
166      * Gets the associated element.
167      *
168      * @return the associated XML element
169      */
170     public Element getElement() {
171         return element;
172     }
173 }