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