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