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  
18  package org.apache.commons.configuration2;
19  
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.Reader;
23  import java.io.StringReader;
24  import java.io.StringWriter;
25  import java.io.Writer;
26  import java.net.URL;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.Map;
33  
34  import javax.xml.parsers.DocumentBuilder;
35  import javax.xml.parsers.DocumentBuilderFactory;
36  import javax.xml.parsers.ParserConfigurationException;
37  import javax.xml.transform.OutputKeys;
38  import javax.xml.transform.Result;
39  import javax.xml.transform.Source;
40  import javax.xml.transform.Transformer;
41  import javax.xml.transform.dom.DOMSource;
42  import javax.xml.transform.stream.StreamResult;
43  
44  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
45  import org.apache.commons.configuration2.ex.ConfigurationException;
46  import org.apache.commons.configuration2.io.ConfigurationLogger;
47  import org.apache.commons.configuration2.io.FileLocator;
48  import org.apache.commons.configuration2.io.FileLocatorAware;
49  import org.apache.commons.configuration2.io.InputStreamSupport;
50  import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
51  import org.apache.commons.configuration2.tree.ImmutableNode;
52  import org.apache.commons.configuration2.tree.NodeTreeWalker;
53  import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
54  import org.apache.commons.lang3.StringUtils;
55  import org.apache.commons.lang3.mutable.MutableObject;
56  import org.w3c.dom.Attr;
57  import org.w3c.dom.CDATASection;
58  import org.w3c.dom.Document;
59  import org.w3c.dom.Element;
60  import org.w3c.dom.NamedNodeMap;
61  import org.w3c.dom.Node;
62  import org.w3c.dom.NodeList;
63  import org.w3c.dom.Text;
64  import org.xml.sax.EntityResolver;
65  import org.xml.sax.InputSource;
66  import org.xml.sax.SAXException;
67  import org.xml.sax.SAXParseException;
68  import org.xml.sax.helpers.DefaultHandler;
69  
70  /**
71   * <p>
72   * A specialized hierarchical configuration class that is able to parse XML documents.
73   * </p>
74   * <p>
75   * The parsed document will be stored keeping its structure. The class also tries to preserve as much information from
76   * the loaded XML document as possible, including comments and processing instructions. These will be contained in
77   * documents created by the {@code save()} methods, too.
78   * </p>
79   * <p>
80   * Like other file based configuration classes this class maintains the name and path to the loaded configuration file.
81   * These properties can be altered using several setter methods, but they are not modified by {@code save()} and
82   * {@code load()} methods. If XML documents contain relative paths to other documents (for example to a DTD), these references
83   * are resolved based on the path set for this configuration.
84   * </p>
85   * <p>
86   * By inheriting from {@link AbstractConfiguration} this class provides some extended functionality, for example interpolation
87   * of property values. Like in {@link PropertiesConfiguration} property values can contain delimiter characters (the
88   * comma ',' per default) and are then split into multiple values. This works for XML attributes and text content of
89   * elements as well. The delimiter can be escaped by a backslash. As an example consider the following XML fragment:
90   * </p>
91   *
92   * <pre>
93   * &lt;config&gt;
94   *   &lt;array&gt;10,20,30,40&lt;/array&gt;
95   *   &lt;scalar&gt;3\,1415&lt;/scalar&gt;
96   *   &lt;cite text="To be or not to be\, this is the question!"/&gt;
97   * &lt;/config&gt;
98   * </pre>
99   *
100  * <p>
101  * Here the content of the {@code array} element will be split at the commas, so the {@code array} key will be assigned
102  * 4 values. In the {@code scalar} property and the {@code text} attribute of the {@code cite} element the comma is
103  * escaped, so that no splitting is performed.
104  * </p>
105  * <p>
106  * The configuration API allows setting multiple values for a single attribute, for example something like the following is
107  * legal (assuming that the default expression engine is used):
108  * </p>
109  *
110  * <pre>
111  * XMLConfiguration config = new XMLConfiguration();
112  * config.addProperty(&quot;test.dir[@name]&quot;, &quot;C:\\Temp\\&quot;);
113  * config.addProperty(&quot;test.dir[@name]&quot;, &quot;D:\\Data\\&quot;);
114  * </pre>
115  *
116  * <p>
117  * However, in XML such a constellation is not supported; an attribute can appear only once for a single element.
118  * Therefore, an attempt to save a configuration which violates this condition will throw an exception.
119  * </p>
120  * <p>
121  * Like other {@code Configuration} implementations, {@code XMLConfiguration} uses a {@link ListDelimiterHandler} object
122  * for controlling list split operations. Per default, a list delimiter handler object is set which disables this
123  * feature. XML has a built-in support for complex structures including list properties; therefore, list splitting is
124  * not that relevant for this configuration type. Nevertheless, by setting an alternative {@code ListDelimiterHandler}
125  * implementation, this feature can be enabled. It works as for any other concrete {@code Configuration} implementation.
126  * </p>
127  * <p>
128  * Whitespace in the content of XML documents is trimmed per default. In most cases this is desired. However, sometimes
129  * whitespace is indeed important and should be treated as part of the value of a property as in the following example:
130  * </p>
131  *
132  * <pre>
133  *   &lt;indent&gt;    &lt;/indent&gt;
134  * </pre>
135  *
136  * <p>
137  * Per default the spaces in the {@code indent} element will be trimmed resulting in an empty element. To tell
138  * {@code XMLConfiguration} that spaces are relevant the {@code xml:space} attribute can be used, which is defined in
139  * the <a href="https://www.w3.org/TR/REC-xml/#sec-white-space">XML specification</a>. This will look as follows:
140  * </p>
141  *
142  * <pre>
143  *   &lt;indent <strong>xml:space=&quot;preserve&quot;</strong>&gt;    &lt;/indent&gt;
144  * </pre>
145  *
146  * <p>
147  * The value of the {@code indent} property will now contain the spaces.
148  * </p>
149  * <p>
150  * {@code XMLConfiguration} implements the {@link FileBasedConfiguration} interface and thus can be used together with a
151  * file-based builder to load XML configuration files from various sources like files, URLs, or streams.
152  * </p>
153  * <p>
154  * Like other {@code Configuration} implementations, this class uses a {@code Synchronizer} object to control concurrent
155  * access. By choosing a suitable implementation of the {@code Synchronizer} interface, an instance can be made
156  * thread-safe or not. Note that access to most of the properties typically set through a builder is not protected by
157  * the {@code Synchronizer}. The intended usage is that these properties are set once at construction time through the
158  * builder and after that remain constant. If you wish to change such properties during life time of an instance, you
159  * have to use the {@code lock()} and {@code unlock()} methods manually to ensure that other threads see your changes.
160  * </p>
161  * <p>
162  * More information about the basic functionality supported by {@code XMLConfiguration} can be found at the user's guide
163  * at <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> Basic
164  * features and AbstractConfiguration</a>. There is also a separate chapter dealing with
165  * <a href="commons.apache.org/proper/commons-configuration/userguide/howto_xml.html"> XML Configurations</a> in
166  * special.
167  * </p>
168  *
169  * @since 1.0
170  */
171 public class XMLConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware, InputStreamSupport {
172 
173     /**
174      * A concrete {@code BuilderVisitor} that constructs XML documents.
175      */
176     static class XMLBuilderVisitor extends BuilderVisitor {
177 
178         /**
179          * Removes all attributes of the given element.
180          *
181          * @param elem the element.
182          */
183         private static void clearAttributes(final Element elem) {
184             final NamedNodeMap attributes = elem.getAttributes();
185             for (int i = 0; i < attributes.getLength(); i++) {
186                 elem.removeAttribute(attributes.item(i).getNodeName());
187             }
188         }
189 
190         /**
191          * Returns the only text node of an element for update. This method is called when the element's text changes. Then all
192          * text nodes except for the first are removed. A reference to the first is returned or <strong>null</strong> if there is no text
193          * node at all.
194          *
195          * @param elem the element.
196          * @return the first and only text node.
197          */
198         private static Text findTextNodeForUpdate(final Element elem) {
199             Text result = null;
200             // Find all Text nodes
201             final NodeList children = elem.getChildNodes();
202             final Collection<Node> textNodes = new ArrayList<>();
203             for (int i = 0; i < children.getLength(); i++) {
204                 final Node nd = children.item(i);
205                 if (nd instanceof Text) {
206                     if (result == null) {
207                         result = (Text) nd;
208                     } else {
209                         textNodes.add(nd);
210                     }
211                 }
212             }
213 
214             // We don't want CDATAs
215             if (result instanceof CDATASection) {
216                 textNodes.add(result);
217                 result = null;
218             }
219 
220             // Remove all but the first Text node
221             textNodes.forEach(elem::removeChild);
222             return result;
223         }
224 
225         /**
226          * Helper method for updating the values of all attributes of the specified node.
227          *
228          * @param node the affected node.
229          * @param elem the element that is associated with this node.
230          */
231         private static void updateAttributes(final ImmutableNode node, final Element elem) {
232             if (node != null && elem != null) {
233                 clearAttributes(elem);
234                 node.getAttributes().forEach((k, v) -> {
235                     if (v != null) {
236                         elem.setAttribute(k, v.toString());
237                     }
238                 });
239             }
240         }
241 
242         /** Stores the document to be constructed. */
243         private final Document document;
244 
245         /** The element mapping. */
246         private final Map<Node, Node> elementMapping;
247 
248         /** A mapping for the references for new nodes. */
249         private final Map<ImmutableNode, Element> newElements;
250 
251         /** Stores the list delimiter handler . */
252         private final ListDelimiterHandler listDelimiterHandler;
253 
254         /**
255          * Creates a new instance of {@code XMLBuilderVisitor}.
256          *
257          * @param docHelper the document helper
258          * @param handler the delimiter handler for properties with multiple values
259          */
260         public XMLBuilderVisitor(final XMLDocumentHelper docHelper, final ListDelimiterHandler handler) {
261             document = docHelper.getDocument();
262             elementMapping = docHelper.getElementMapping();
263             listDelimiterHandler = handler;
264             newElements = new HashMap<>();
265         }
266 
267         /**
268          * Helper method for accessing the element of the specified node.
269          *
270          * @param node the node.
271          * @param refHandler the {@code ReferenceNodeHandler}.
272          * @return the element of this node.
273          */
274         private Element getElement(final ImmutableNode node, final ReferenceNodeHandler refHandler) {
275             final Element elementNew = newElements.get(node);
276             if (elementNew != null) {
277                 return elementNew;
278             }
279 
280             // special treatment for root node of the hierarchy
281             final Object reference = refHandler.getReference(node);
282             final Node element;
283             if (reference instanceof XMLDocumentHelper) {
284                 element = ((XMLDocumentHelper) reference).getDocument().getDocumentElement();
285             } else if (reference instanceof XMLListReference) {
286                 element = ((XMLListReference) reference).getElement();
287             } else {
288                 element = (Node) reference;
289             }
290             return element != null ? (Element) elementMapping.get(element) : document.getDocumentElement();
291         }
292 
293         /**
294          * Updates the current XML document regarding removed nodes. The elements associated with removed nodes are removed from
295          * the document.
296          *
297          * @param refHandler the {@code ReferenceNodeHandler}.
298          */
299         public void handleRemovedNodes(final ReferenceNodeHandler refHandler) {
300             refHandler.removedReferences().stream().filter(Node.class::isInstance).forEach(ref -> removeReference(elementMapping.get(ref)));
301         }
302 
303         /**
304          * {@inheritDoc} This implementation ensures that the correct XML element is created and inserted between the given
305          * siblings.
306          */
307         @Override
308         protected void insert(final ImmutableNode newNode, final ImmutableNode parent, final ImmutableNode sibling1, final ImmutableNode sibling2,
309             final ReferenceNodeHandler refHandler) {
310             if (XMLListReference.isListNode(newNode, refHandler)) {
311                 return;
312             }
313 
314             final Element elem = document.createElement(newNode.getNodeName());
315             newElements.put(newNode, elem);
316             updateAttributes(newNode, elem);
317             if (newNode.getValue() != null) {
318                 final String txt = String.valueOf(listDelimiterHandler.escape(newNode.getValue(), ListDelimiterHandler.NOOP_TRANSFORMER));
319                 elem.appendChild(document.createTextNode(txt));
320             }
321             if (sibling2 == null) {
322                 getElement(parent, refHandler).appendChild(elem);
323             } else if (sibling1 != null) {
324                 getElement(parent, refHandler).insertBefore(elem, getElement(sibling1, refHandler).getNextSibling());
325             } else {
326                 getElement(parent, refHandler).insertBefore(elem, getElement(parent, refHandler).getFirstChild());
327             }
328         }
329 
330         /**
331          * Processes the specified document, updates element values, and adds new nodes to the hierarchy.
332          *
333          * @param refHandler the {@code ReferenceNodeHandler}.
334          */
335         public void processDocument(final ReferenceNodeHandler refHandler) {
336             updateAttributes(refHandler.getRootNode(), document.getDocumentElement());
337             NodeTreeWalker.INSTANCE.walkDFS(refHandler.getRootNode(), this, refHandler);
338         }
339 
340         /**
341          * Updates the associated XML elements when a node is removed.
342          *
343          * @param element the element to be removed.
344          */
345         private void removeReference(final Node element) {
346             final Node parentElem = element.getParentNode();
347             if (parentElem != null) {
348                 parentElem.removeChild(element);
349             }
350         }
351 
352         /**
353          * {@inheritDoc} This implementation determines the XML element associated with the given node. Then this element's
354          * value and attributes are set accordingly.
355          */
356         @Override
357         protected void update(final ImmutableNode node, final Object reference, final ReferenceNodeHandler refHandler) {
358             if (XMLListReference.isListNode(node, refHandler)) {
359                 if (XMLListReference.isFirstListItem(node, refHandler)) {
360                     final String value = XMLListReference.listValue(node, refHandler, listDelimiterHandler);
361                     updateElement(node, refHandler, value);
362                 }
363             } else {
364                 final Object value = listDelimiterHandler.escape(refHandler.getValue(node), ListDelimiterHandler.NOOP_TRANSFORMER);
365                 updateElement(node, refHandler, value);
366             }
367         }
368 
369         /**
370          * Updates the node's value if it represents an element node.
371          *
372          * @param element the element.
373          * @param value the new value.
374          */
375         private void updateElement(final Element element, final Object value) {
376             Text txtNode = findTextNodeForUpdate(element);
377             if (value == null) {
378                 // remove text
379                 if (txtNode != null) {
380                     element.removeChild(txtNode);
381                 }
382             } else {
383                 final String newValue = String.valueOf(value);
384                 if (txtNode == null) {
385                     txtNode = document.createTextNode(newValue);
386                     if (element.getFirstChild() != null) {
387                         element.insertBefore(txtNode, element.getFirstChild());
388                     } else {
389                         element.appendChild(txtNode);
390                     }
391                 } else {
392                     txtNode.setNodeValue(newValue);
393                 }
394             }
395         }
396 
397         private void updateElement(final ImmutableNode node, final ReferenceNodeHandler refHandler, final Object value) {
398             final Element element = getElement(node, refHandler);
399             updateElement(element, value);
400             updateAttributes(node, element);
401         }
402     }
403 
404     /** Constant for the default indent size. */
405     static final int DEFAULT_INDENT_SIZE = 2;
406 
407     /** Constant for output property name used on a transformer to specify the indent amount. */
408     static final String INDENT_AMOUNT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount";
409 
410     /** Constant for the default root element name. */
411     private static final String DEFAULT_ROOT_NAME = "configuration";
412 
413     /** Constant for the name of the space attribute. */
414     private static final String ATTR_SPACE = "xml:space";
415 
416     /** Constant for an internally used space attribute. */
417     private static final String ATTR_SPACE_INTERNAL = "config-xml:space";
418 
419     /** Constant for the xml:space value for preserving whitespace. */
420     private static final String VALUE_PRESERVE = "preserve";
421 
422     /** Schema Langauge key for the parser */
423     private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
424 
425     /** Schema Language for the parser */
426     private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
427 
428     /**
429      * Determines the number of child elements of this given node with the specified node name.
430      *
431      * @param parent the parent node.
432      * @param name the name in question.
433      * @return the number of child elements with this name.
434      */
435     private static int countChildElements(final Node parent, final String name) {
436         final NodeList childNodes = parent.getChildNodes();
437         int count = 0;
438         for (int i = 0; i < childNodes.getLength(); i++) {
439             final Node item = childNodes.item(i);
440             if (item instanceof Element && name.equals(((Element) item).getTagName())) {
441                 count++;
442             }
443         }
444         return count;
445     }
446 
447     /**
448      * Determines the value of a configuration node. This method mainly checks whether the text value is to be trimmed or
449      * not. This is normally defined by the trim flag. However, if the node has children and its content is only whitespace,
450      * then it makes no sense to store any value; this would only scramble layout when the configuration is saved again.
451      *
452      * @param content the text content of this node.
453      * @param hasChildren a flag whether the node has children.
454      * @param trimFlag the trim flag.
455      * @return the value to be stored for this node.
456      */
457     private static String determineValue(final String content, final boolean hasChildren, final boolean trimFlag) {
458         final boolean shouldTrim = trimFlag || StringUtils.isBlank(content) && hasChildren;
459         return shouldTrim ? content.trim() : content;
460     }
461 
462     /**
463      * Checks whether an element defines a complete list. If this is the case, extended list handling can be applied.
464      *
465      * @param element the element to be checked.
466      * @return a flag whether this is the only element defining the list.
467      */
468     private static boolean isSingleElementList(final Element element) {
469         final Node parentNode = element.getParentNode();
470         return countChildElements(parentNode, element.getTagName()) == 1;
471     }
472 
473     /**
474      * Helper method for initializing the attributes of a configuration node from the given XML element.
475      *
476      * @param element the current XML element
477      * @return a map with all attribute values extracted for the current node
478      */
479     private static Map<String, String> processAttributes(final Node element) {
480         final NamedNodeMap attributes = element.getAttributes();
481         final Map<String, String> attrmap = new HashMap<>();
482         for (int i = 0; i < attributes.getLength(); ++i) {
483             final Node w3cNode = attributes.item(i);
484             if (w3cNode instanceof Attr) {
485                 final Attr attr = (Attr) w3cNode;
486                 attrmap.put(attr.getName(), attr.getValue());
487             }
488         }
489         return attrmap;
490     }
491 
492     /**
493      * Checks whether the content of the current XML element should be trimmed. This method checks whether a
494      * {@code xml:space} attribute is present and evaluates its value. See
495      * <a href="https://www.w3.org/TR/REC-xml/#sec-white-space"> http://www.w3.org/TR/REC-xml/#sec-white-space</a> for more
496      * details.
497      *
498      * @param element the current XML element
499      * @param currentTrim the current trim flag
500      * @return a flag whether the content of this element should be trimmed
501      */
502     private static boolean shouldTrim(final Element element, final boolean currentTrim) {
503         final Attr attr = element.getAttributeNode(ATTR_SPACE);
504         if (attr == null) {
505             return currentTrim;
506         }
507         return !VALUE_PRESERVE.equals(attr.getValue());
508     }
509 
510     /** Stores the name of the root element. */
511     private String rootElementName;
512 
513     /** Stores the public ID from the DOCTYPE. */
514     private String publicID;
515 
516     /** Stores the system ID from the DOCTYPE. */
517     private String systemID;
518 
519     /** Stores the document builder that should be used for loading. */
520     private DocumentBuilder documentBuilder;
521 
522     /** Stores a flag whether DTD or Schema validation should be performed. */
523     private boolean validating;
524 
525     /** Stores a flag whether DTD or Schema validation is used. */
526     private boolean schemaValidation;
527 
528     /** The EntityResolver to use. */
529     private EntityResolver entityResolver = new DefaultEntityResolver();
530 
531     /** The current file locator. */
532     private FileLocator locator;
533 
534     /**
535      * Creates a new instance of {@code XMLConfiguration}.
536      */
537     public XMLConfiguration() {
538         initLogger(new ConfigurationLogger(XMLConfiguration.class));
539     }
540 
541     /**
542      * Creates a new instance of {@code XMLConfiguration} and copies the content of the passed in configuration into this
543      * object. Note that only the data of the passed in configuration will be copied. If, for instance, the other
544      * configuration is a {@code XMLConfiguration}, too, things like comments or processing instructions will be lost.
545      *
546      * @param c the configuration to copy.
547      * @since 1.4
548      */
549     public XMLConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
550         super(c);
551         rootElementName = c != null ? c.getRootElementName() : null;
552         initLogger(new ConfigurationLogger(XMLConfiguration.class));
553     }
554 
555     /**
556      * Helper method for building the internal storage hierarchy. The XML elements are transformed into node objects.
557      *
558      * @param node     a builder for the current node.
559      * @param refValue stores the text value of the element.
560      * @param element  the current XML element.
561      * @param elemRefs a map for assigning references objects to nodes; can be <strong>null</strong>, then reference objects are irrelevant.
562      * @param trim     a flag whether the text content of elements should be trimmed; this controls the whitespace handling.
563      * @param level    the current level in the hierarchy.
564      * @return a map with all attribute values extracted for the current node; this map also contains the value of the trim flag for this node under the key
565      *         {@value #ATTR_SPACE}.
566      */
567     private Map<String, String> constructHierarchy(final ImmutableNode.Builder node, final MutableObject<String> refValue, final Element element,
568         final Map<ImmutableNode, Object> elemRefs, final boolean trim, final int level) {
569         final boolean trimFlag = shouldTrim(element, trim);
570         final Map<String, String> attributes = processAttributes(element);
571         attributes.put(ATTR_SPACE_INTERNAL, String.valueOf(trimFlag));
572         final StringBuilder buffer = new StringBuilder();
573         final NodeList list = element.getChildNodes();
574         boolean hasChildren = false;
575 
576         for (int i = 0; i < list.getLength(); i++) {
577             final Node w3cNode = list.item(i);
578             if (w3cNode instanceof Element) {
579                 final Element child = (Element) w3cNode;
580                 final ImmutableNode.Builder childNode = new ImmutableNode.Builder();
581                 childNode.name(child.getTagName());
582                 final MutableObject<String> refChildValue = new MutableObject<>();
583                 final Map<String, String> attrmap = constructHierarchy(childNode, refChildValue, child, elemRefs, trimFlag, level + 1);
584                 final boolean childTrim = Boolean.parseBoolean(attrmap.remove(ATTR_SPACE_INTERNAL));
585                 childNode.addAttributes(attrmap);
586                 final ImmutableNode newChild = createChildNodeWithValue(node, childNode, child, refChildValue.get(), childTrim, attrmap, elemRefs);
587                 if (elemRefs != null && !elemRefs.containsKey(newChild)) {
588                     elemRefs.put(newChild, child);
589                 }
590                 hasChildren = true;
591             } else if (w3cNode instanceof Text) {
592                 final Text data = (Text) w3cNode;
593                 buffer.append(data.getData());
594             }
595         }
596 
597         boolean childrenFlag = false;
598         if (hasChildren || trimFlag) {
599             childrenFlag = hasChildren || attributes.size() > 1;
600         }
601         final String text = determineValue(buffer.toString(), childrenFlag, trimFlag);
602         if (!text.isEmpty() || !childrenFlag && level != 0) {
603             refValue.setValue(text);
604         }
605         return attributes;
606     }
607 
608     /**
609      * Creates a new child node, assigns its value, and adds it to its parent. This method also deals with elements whose value is a list. In this case multiple
610      * child elements must be added. The return value is the first child node which was added.
611      *
612      * @param parent   the builder for the parent element.
613      * @param child    the builder for the child element.
614      * @param elem     the associated XML element.
615      * @param value    the value of the child element.
616      * @param trim     flag whether texts of elements should be trimmed.
617      * @param attrmap  a map with the attributes of the current node.
618      * @param elemRefs a map for assigning references objects to nodes; can be <strong>null</strong>, then reference objects are irrelevant.
619      * @return the first child node added to the parent.
620      */
621     private ImmutableNode createChildNodeWithValue(final ImmutableNode.Builder parent, final ImmutableNode.Builder child, final Element elem,
622         final String value, final boolean trim, final Map<String, String> attrmap, final Map<ImmutableNode, Object> elemRefs) {
623         final ImmutableNode addedChildNode;
624         final Collection<String> values;
625 
626         if (value != null) {
627             values = getListDelimiterHandler().split(value, trim);
628         } else {
629             values = Collections.emptyList();
630         }
631 
632         if (values.size() > 1) {
633             final Map<ImmutableNode, Object> refs = isSingleElementList(elem) ? elemRefs : null;
634             final Iterator<String> it = values.iterator();
635             // Create new node for the original child's first value
636             child.value(it.next());
637             addedChildNode = child.create();
638             parent.addChild(addedChildNode);
639             XMLListReference.assignListReference(refs, addedChildNode, elem);
640 
641             // add multiple new children
642             while (it.hasNext()) {
643                 final ImmutableNode.Builder c = new ImmutableNode.Builder();
644                 c.name(addedChildNode.getNodeName());
645                 c.value(it.next());
646                 c.addAttributes(attrmap);
647                 final ImmutableNode newChild = c.create();
648                 parent.addChild(newChild);
649                 XMLListReference.assignListReference(refs, newChild, null);
650             }
651         } else {
652             if (values.size() == 1) {
653                 // we will have to replace the value because it might
654                 // contain escaped delimiters
655                 child.value(values.iterator().next());
656             }
657             addedChildNode = child.create();
658             parent.addChild(addedChildNode);
659         }
660 
661         return addedChildNode;
662     }
663 
664     /**
665      * Creates a DOM document from the internal tree of configuration nodes.
666      *
667      * @return the new document.
668      * @throws ConfigurationException if an error occurs.
669      */
670     private Document createDocument() throws ConfigurationException {
671         final ReferenceNodeHandler handler = getReferenceHandler();
672         final XMLDocumentHelper docHelper = (XMLDocumentHelper) handler.getReference(handler.getRootNode());
673         final XMLDocumentHelper newHelper = docHelper == null ? XMLDocumentHelper.forNewDocument(getRootElementName()) : docHelper.createCopy();
674 
675         final XMLBuilderVisitor builder = new XMLBuilderVisitor(newHelper, getListDelimiterHandler());
676         builder.handleRemovedNodes(handler);
677         builder.processDocument(handler);
678         initRootElementText(newHelper.getDocument(), getModel().getNodeHandler().getRootNode().getValue());
679         return newHelper.getDocument();
680     }
681 
682     /**
683      * Creates the {@code DocumentBuilder} to be used for loading files. This implementation checks whether a specific
684      * {@code DocumentBuilder} has been set. If this is the case, this one is used. Otherwise a default builder is created.
685      * Depending on the value of the validating flag this builder will be a validating or a non validating
686      * {@code DocumentBuilder}.
687      *
688      * @return the {@code DocumentBuilder} for loading configuration files.
689      * @throws ParserConfigurationException if an error occurs.
690      * @since 1.2
691      */
692     protected DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
693         if (getDocumentBuilder() != null) {
694             return getDocumentBuilder();
695         }
696         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
697         if (isValidating()) {
698             factory.setValidating(true);
699             if (isSchemaValidation()) {
700                 factory.setNamespaceAware(true);
701                 factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
702             }
703         }
704 
705         final DocumentBuilder result = factory.newDocumentBuilder();
706         result.setEntityResolver(this.entityResolver);
707 
708         if (isValidating()) {
709             // register an error handler which detects validation errors
710             result.setErrorHandler(new DefaultHandler() {
711                 @Override
712                 public void error(final SAXParseException ex) throws SAXException {
713                     throw ex;
714                 }
715             });
716         }
717         return result;
718     }
719 
720     /**
721      * Creates and initializes the transformer used for save operations. This base implementation initializes all of the
722      * default settings like indentation mode and the DOCTYPE. Derived classes may overload this method if they have
723      * specific needs.
724      *
725      * @return the transformer to use for a save operation.
726      * @throws ConfigurationException if an error occurs.
727      * @since 1.3
728      */
729     protected Transformer createTransformer() throws ConfigurationException {
730         final Transformer transformer = XMLDocumentHelper.createTransformer();
731 
732         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
733         transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, Integer.toString(DEFAULT_INDENT_SIZE));
734         if (locator != null && locator.getEncoding() != null) {
735             transformer.setOutputProperty(OutputKeys.ENCODING, locator.getEncoding());
736         }
737         if (publicID != null) {
738             transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicID);
739         }
740         if (systemID != null) {
741             transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemID);
742         }
743 
744         return transformer;
745     }
746 
747     /**
748      * Gets the XML document this configuration was loaded from. The return value is <strong>null</strong> if this configuration
749      * was not loaded from a XML document.
750      *
751      * @return the XML document this configuration was loaded from.
752      */
753     public Document getDocument() {
754         final XMLDocumentHelper docHelper = getDocumentHelper();
755         return docHelper != null ? docHelper.getDocument() : null;
756     }
757 
758     /**
759      * Gets the {@code DocumentBuilder} object that is used for loading documents. If no specific builder has been set,
760      * this method returns <strong>null</strong>.
761      *
762      * @return the {@code DocumentBuilder} for loading new documents.
763      * @since 1.2
764      */
765     public DocumentBuilder getDocumentBuilder() {
766         return documentBuilder;
767     }
768 
769     /**
770      * Gets the helper object for managing the underlying document.
771      *
772      * @return the {@code XMLDocumentHelper}.
773      */
774     private XMLDocumentHelper getDocumentHelper() {
775         final ReferenceNodeHandler handler = getReferenceHandler();
776         return (XMLDocumentHelper) handler.getReference(handler.getRootNode());
777     }
778 
779     /**
780      * Gets the EntityResolver.
781      *
782      * @return The EntityResolver.
783      * @since 1.7
784      */
785     public EntityResolver getEntityResolver() {
786         return this.entityResolver;
787     }
788 
789     /**
790      * Gets the public ID of the DOCTYPE declaration from the loaded XML document. This is <strong>null</strong> if no document has
791      * been loaded yet or if the document does not contain a DOCTYPE declaration with a public ID.
792      *
793      * @return the public ID.
794      * @since 1.3
795      */
796     public String getPublicID() {
797         return syncReadValue(publicID, false);
798     }
799 
800     /**
801      * Gets the extended node handler with support for references.
802      *
803      * @return the {@code ReferenceNodeHandler}.
804      */
805     private ReferenceNodeHandler getReferenceHandler() {
806         return getSubConfigurationParentModel().getReferenceNodeHandler();
807     }
808 
809     /**
810      * Gets the name of the root element. If this configuration was loaded from a XML document, the name of this
811      * document's root element is returned. Otherwise it is possible to set a name for the root element that will be used
812      * when this configuration is stored.
813      *
814      * @return the name of the root element.
815      */
816     @Override
817     protected String getRootElementNameInternal() {
818         final Document doc = getDocument();
819         if (doc == null) {
820             return rootElementName == null ? DEFAULT_ROOT_NAME : rootElementName;
821         }
822         return doc.getDocumentElement().getNodeName();
823     }
824 
825     /**
826      * Gets the system ID of the DOCTYPE declaration from the loaded XML document. This is <strong>null</strong> if no document has
827      * been loaded yet or if the document does not contain a DOCTYPE declaration with a system ID.
828      *
829      * @return the system ID.
830      * @since 1.3
831      */
832     public String getSystemID() {
833         return syncReadValue(systemID, false);
834     }
835 
836     /**
837      * {@inheritDoc} Stores the passed in locator for the upcoming IO operation.
838      */
839     @Override
840     public void initFileLocator(final FileLocator loc) {
841         locator = loc;
842     }
843 
844     /**
845      * Initializes this configuration from an XML document.
846      *
847      * @param docHelper the helper object with the document to be parsed.
848      * @param elemRefs a flag whether references to the XML elements should be set.
849      */
850     private void initProperties(final XMLDocumentHelper docHelper, final boolean elemRefs) {
851         setPublicID(docHelper.getSourcePublicID());
852         setSystemID(docHelper.getSourceSystemID());
853         initProperties(docHelper, elemRefs, docHelper.getDocument().getDocumentElement());
854     }
855 
856     private void initProperties(final XMLDocumentHelper docHelper, final boolean elemRefs, final Element element) {
857         final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
858         final MutableObject<String> rootValue = new MutableObject<>();
859         final Map<ImmutableNode, Object> elemRefMap = elemRefs ? new HashMap<>() : null;
860         final Map<String, String> attributes = constructHierarchy(rootBuilder, rootValue, element, elemRefMap, true, 0);
861         attributes.remove(ATTR_SPACE_INTERNAL);
862         final ImmutableNode top = rootBuilder.value(rootValue.get()).addAttributes(attributes).create();
863         getSubConfigurationParentModel().mergeRoot(top, element.getTagName(), elemRefMap, elemRefs ? docHelper : null, this);
864     }
865 
866     /**
867      * Sets the text of the root element of a newly created XML Document.
868      *
869      * @param doc the document.
870      * @param value the new text to be set.
871      */
872     private void initRootElementText(final Document doc, final Object value) {
873         final Element elem = doc.getDocumentElement();
874         final NodeList children = elem.getChildNodes();
875 
876         // Remove all existing text nodes
877         for (int i = 0; i < children.getLength(); i++) {
878             final Node nd = children.item(i);
879             if (nd.getNodeType() == Node.TEXT_NODE) {
880                 elem.removeChild(nd);
881             }
882         }
883 
884         if (value != null) {
885             // Add a new text node
886             elem.appendChild(doc.createTextNode(String.valueOf(value)));
887         }
888     }
889 
890     /**
891      * Returns the value of the schemaValidation flag.
892      *
893      * @return the schemaValidation flag.
894      * @since 1.7
895      */
896     public boolean isSchemaValidation() {
897         return schemaValidation;
898     }
899 
900     /**
901      * Returns the value of the validating flag.
902      *
903      * @return the validating flag.
904      * @since 1.2
905      */
906     public boolean isValidating() {
907         return validating;
908     }
909 
910     /**
911      * Loads a configuration file from the specified input source.
912      *
913      * @param source the input source.
914      * @throws ConfigurationException if an error occurs.
915      */
916     private void load(final InputSource source) throws ConfigurationException {
917         if (locator == null) {
918             throw new ConfigurationException(
919                 "Load operation not properly initialized! Do not call read(InputStream) directly, but use a FileHandler to load a configuration.");
920         }
921 
922         try {
923             final URL sourceURL = locator.getSourceURL();
924             if (sourceURL != null) {
925                 source.setSystemId(sourceURL.toString());
926             }
927 
928             final DocumentBuilder builder = createDocumentBuilder();
929             final Document newDocument = builder.parse(source);
930             final Document oldDocument = getDocument();
931             initProperties(XMLDocumentHelper.forSourceDocument(newDocument), oldDocument == null);
932         } catch (final SAXParseException spe) {
933             throw new ConfigurationException(spe, "Error parsing system ID %s", source.getSystemId());
934         } catch (final Exception e) {
935             getLogger().debug("Unable to load the configuration: " + e);
936             throw new ConfigurationException("Unable to load the configuration", e);
937         }
938     }
939 
940     /**
941      * Loads the configuration from the given XML DOM Element.
942      * <p>
943      * This method can be used to initialize the configuration from an XML element in a document that has already been parsed. This is especially useful if the
944      * configuration is only a part of a larger XML document.
945      * </p>
946      *
947      * @param element the input element.
948      * @throws ConfigurationException if an error occurs.
949      * @since 2.14.0
950      */
951     public void read(final Element element) throws ConfigurationException {
952         try {
953             initProperties(getDocumentHelper(), getDocument() == null, element);
954         } catch (final Exception e) {
955             getLogger().debug("Unable to load the configuration: " + e);
956             throw new ConfigurationException(e, "Unable to load the configuration %s", element);
957         }
958     }
959 
960     /**
961      * Loads the configuration from the given input stream. This is analogous to {@link #read(Reader)}, but data is read
962      * from a stream. Note that this method will be called most time when reading an XML configuration source. By reading
963      * XML documents directly from an input stream, the file's encoding can be correctly dealt with.
964      *
965      * @param in the input stream.
966      * @throws ConfigurationException if an error occurs.
967      * @throws IOException if an IO error occurs.
968      */
969     @Override
970     public void read(final InputStream in) throws ConfigurationException, IOException {
971         load(new InputSource(in));
972     }
973 
974     /**
975      * Loads the configuration from the given reader. Note that the {@code clear()} method is not called, so the properties
976      * contained in the loaded file will be added to the current set of properties.
977      *
978      * @param in the reader.
979      * @throws ConfigurationException if an error occurs.
980      * @throws IOException if an IO error occurs.
981      */
982     @Override
983     public void read(final Reader in) throws ConfigurationException, IOException {
984         load(new InputSource(in));
985     }
986 
987     /**
988      * Sets the {@code DocumentBuilder} object to be used for loading documents. This method makes it possible to specify
989      * the exact document builder. So an application can create a builder, configure it for its special needs, and then pass
990      * it to this method.
991      *
992      * @param documentBuilder the document builder to be used; if undefined, a default builder will be used.
993      * @since 1.2
994      */
995     public void setDocumentBuilder(final DocumentBuilder documentBuilder) {
996         this.documentBuilder = documentBuilder;
997     }
998 
999     /**
1000      * Sets a new EntityResolver. Setting this will cause RegisterEntityId to have no effect.
1001      *
1002      * @param resolver The EntityResolver to use.
1003      * @since 1.7
1004      */
1005     public void setEntityResolver(final EntityResolver resolver) {
1006         this.entityResolver = resolver;
1007     }
1008 
1009     /**
1010      * Sets the public ID of the DOCTYPE declaration. When this configuration is saved, a DOCTYPE declaration will be
1011      * constructed that contains this public ID.
1012      *
1013      * @param publicID the public ID.
1014      * @since 1.3
1015      */
1016     public void setPublicID(final String publicID) {
1017         syncWrite(() -> this.publicID = publicID, false);
1018     }
1019 
1020     /**
1021      * Sets the name of the root element. This name is used when this configuration object is stored in an XML file. Note
1022      * that setting the name of the root element works only if this configuration has been newly created. If the
1023      * configuration was loaded from an XML file, the name cannot be changed and an {@code UnsupportedOperationException}
1024      * exception is thrown. Whether this configuration has been loaded from an XML document or not can be found out using
1025      * the {@code getDocument()} method.
1026      *
1027      * @param name the name of the root element.
1028      */
1029     public void setRootElementName(final String name) {
1030         beginRead(true);
1031         try {
1032             if (getDocument() != null) {
1033                 throw new UnsupportedOperationException("The name of the root element cannot be changed when loaded from an XML document!");
1034             }
1035             rootElementName = name;
1036         } finally {
1037             endRead();
1038         }
1039     }
1040 
1041     /**
1042      * Sets the value of the schemaValidation flag. This flag determines whether DTD or Schema validation should be used.
1043      * This flag is evaluated only if no custom {@code DocumentBuilder} was set. If set to true the XML document must
1044      * contain a schemaLocation definition that provides resolvable hints to the required schemas.
1045      *
1046      * @param schemaValidation the validating flag.
1047      * @since 1.7
1048      */
1049     public void setSchemaValidation(final boolean schemaValidation) {
1050         this.schemaValidation = schemaValidation;
1051         if (schemaValidation) {
1052             this.validating = true;
1053         }
1054     }
1055 
1056     /**
1057      * Sets the system ID of the DOCTYPE declaration. When this configuration is saved, a DOCTYPE declaration will be
1058      * constructed that contains this system ID.
1059      *
1060      * @param systemID the system ID.
1061      * @since 1.3
1062      */
1063     public void setSystemID(final String systemID) {
1064         syncWrite(() -> this.systemID = systemID, false);
1065     }
1066 
1067     /**
1068      * Sets the value of the validating flag. This flag determines whether DTD/Schema validation should be performed when
1069      * loading XML documents. This flag is evaluated only if no custom {@code DocumentBuilder} was set.
1070      *
1071      * @param validating the validating flag.
1072      * @since 1.2
1073      */
1074     public void setValidating(final boolean validating) {
1075         if (!schemaValidation) {
1076             this.validating = validating;
1077         }
1078     }
1079 
1080     /**
1081      * Validate the document against the Schema.
1082      *
1083      * @throws ConfigurationException if the validation fails.
1084      */
1085     public void validate() throws ConfigurationException {
1086         syncWrite(() -> {
1087             try {
1088                 final StringWriter writer = new StringWriter();
1089                 final Result result = new StreamResult(writer);
1090                 XMLDocumentHelper.transform(createTransformer(), new DOMSource(createDocument()), result);
1091                 final Reader reader = new StringReader(writer.getBuffer().toString());
1092                 createDocumentBuilder().parse(new InputSource(reader));
1093             } catch (final SAXException | IOException | ParserConfigurationException pce) {
1094                 throw new ConfigurationException("Validation failed", pce);
1095             }
1096         }, false);
1097     }
1098 
1099     /**
1100      * Saves the configuration to the specified writer.
1101      *
1102      * @param writer the writer used to save the configuration.
1103      * @throws ConfigurationException if an error occurs.
1104      * @throws IOException if an IO error occurs.
1105      */
1106     @Override
1107     public void write(final Writer writer) throws ConfigurationException, IOException {
1108         write(writer, createTransformer());
1109     }
1110 
1111     /**
1112      * Saves the configuration to the specified writer.
1113      *
1114      * @param writer the writer used to save the configuration.
1115      * @param transformer How to transform this configuration.
1116      * @throws ConfigurationException if an error occurs.
1117      * @since 2.7.0
1118      */
1119     public void write(final Writer writer, final Transformer transformer) throws ConfigurationException {
1120         final Source source = new DOMSource(createDocument());
1121         final Result result = new StreamResult(writer);
1122         XMLDocumentHelper.transform(transformer, source, result);
1123     }
1124 }