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