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.Strings;
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      /**
44       * Assigns an instance of this class as reference to the specified configuration node. This reference acts as a marker
45       * indicating that this node is subject to extended list handling.
46       *
47       * @param refs the mapping for node references
48       * @param node the affected configuration node
49       * @param elem the current XML element
50       */
51      public static void assignListReference(final Map<ImmutableNode, Object> refs, final ImmutableNode node, final Element elem) {
52          if (refs != null) {
53              refs.put(node, new XMLListReference(elem));
54          }
55      }
56  
57      /**
58       * Checks whether the specified node has an associated list reference. This marks the node as part of a list.
59       *
60       * @param node the node to be checked
61       * @param handler the reference handler
62       * @return a flag whether this node has a list reference
63       */
64      private static boolean hasListReference(final ImmutableNode node, final ReferenceNodeHandler handler) {
65          return handler.getReference(node) instanceof XMLListReference;
66      }
67  
68      /**
69       * Checks whether the specified node is the first node of a list. This is needed because all items of the list are
70       * collected and stored as value of the first list node. Note: This method requires that the passed in node is a list
71       * node, so {@link #isListNode(ImmutableNode, ReferenceNodeHandler)} must have returned <strong>true</strong> for it.
72       *
73       * @param node the configuration node
74       * @param handler the reference node handler
75       * @return a flag whether this is the first node of a list
76       */
77      public static boolean isFirstListItem(final ImmutableNode node, final ReferenceNodeHandler handler) {
78          final ImmutableNode parent = handler.getParent(node);
79          ImmutableNode firstItem = null;
80          int idx = 0;
81          while (firstItem == null) {
82              final ImmutableNode child = handler.getChild(parent, idx);
83              if (nameEquals(node, child)) {
84                  firstItem = child;
85              }
86              idx++;
87          }
88          return firstItem == node;
89      }
90  
91      /**
92       * Checks whether the specified configuration node has to be taken into account for list handling. This is the case if
93       * the node's parent has at least one child node with the same name which has a special list reference assigned. (Note
94       * that the passed in node does not necessarily have such a reference; if it has been added at a later point in time, it
95       * also has to become an item of the list.)
96       *
97       * @param node the configuration node
98       * @param handler the reference node handler
99       * @return a flag whether this node is relevant for list handling
100      */
101     public static boolean isListNode(final ImmutableNode node, final ReferenceNodeHandler handler) {
102         if (hasListReference(node, handler)) {
103             return true;
104         }
105 
106         final ImmutableNode parent = handler.getParent(node);
107         if (parent != null) {
108             for (int i = 0; i < handler.getChildrenCount(parent, null); i++) {
109                 final ImmutableNode child = handler.getChild(parent, i);
110                 if (hasListReference(child, handler) && nameEquals(node, child)) {
111                     return true;
112                 }
113             }
114         }
115         return false;
116     }
117 
118     /**
119      * Constructs the concatenated string value of all items comprising the list the specified node belongs to. This method
120      * is called when saving an {@link XMLConfiguration}. Then configuration nodes created for list items have to be
121      * collected again and transformed into a string defining all list elements.
122      *
123      * @param node the configuration node
124      * @param nodeHandler the reference node handler
125      * @param delimiterHandler the list delimiter handler of the configuration
126      * @return a string with all values of the current list
127      * @throws ConfigurationRuntimeException if the list delimiter handler does not support the transformation of list items
128      *         to a string
129      */
130     public static String listValue(final ImmutableNode node, final ReferenceNodeHandler nodeHandler, final ListDelimiterHandler delimiterHandler) {
131         // cannot be null if the current node is a list node
132         final ImmutableNode parent = nodeHandler.getParent(node);
133         final List<ImmutableNode> items = nodeHandler.getChildren(parent, node.getNodeName());
134         final List<Object> values = items.stream().map(ImmutableNode::getValue).collect(Collectors.toList());
135         try {
136             return String.valueOf(delimiterHandler.escapeList(values, ListDelimiterHandler.NOOP_TRANSFORMER));
137         } catch (final UnsupportedOperationException e) {
138             throw new ConfigurationRuntimeException("List handling not supported by the current ListDelimiterHandler! Make sure that the same delimiter "
139                     + "handler is used for loading and saving the configuration.", e);
140         }
141     }
142 
143     /**
144      * Helper method for comparing the names of two nodes.
145      *
146      * @param n1 node 1
147      * @param n2 node 2
148      * @return a flag whether these nodes have equal names
149      */
150     private static boolean nameEquals(final ImmutableNode n1, final ImmutableNode n2) {
151         return Strings.CS.equals(n2.getNodeName(), n1.getNodeName());
152     }
153 
154     /** The wrapped XML element. */
155     private final Element element;
156 
157     /**
158      * Private constructor. No instances can be created from other classes.
159      *
160      * @param e the associated element
161      */
162     private XMLListReference(final Element e) {
163         element = e;
164     }
165 
166     /**
167      * Gets the associated element.
168      *
169      * @return the associated XML element
170      */
171     public Element getElement() {
172         return element;
173     }
174 }