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 java.io.IOException;
021import java.io.InputStream;
022import java.io.Reader;
023import java.io.StringReader;
024import java.io.StringWriter;
025import java.io.Writer;
026import java.net.URL;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.Iterator;
032import java.util.Map;
033
034import javax.xml.parsers.DocumentBuilder;
035import javax.xml.parsers.DocumentBuilderFactory;
036import javax.xml.parsers.ParserConfigurationException;
037import javax.xml.transform.OutputKeys;
038import javax.xml.transform.Result;
039import javax.xml.transform.Source;
040import javax.xml.transform.Transformer;
041import javax.xml.transform.dom.DOMSource;
042import javax.xml.transform.stream.StreamResult;
043
044import org.apache.commons.configuration2.convert.ListDelimiterHandler;
045import org.apache.commons.configuration2.ex.ConfigurationException;
046import org.apache.commons.configuration2.io.ConfigurationLogger;
047import org.apache.commons.configuration2.io.FileLocator;
048import org.apache.commons.configuration2.io.FileLocatorAware;
049import org.apache.commons.configuration2.io.InputStreamSupport;
050import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
051import org.apache.commons.configuration2.tree.ImmutableNode;
052import org.apache.commons.configuration2.tree.NodeTreeWalker;
053import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
054import org.apache.commons.lang3.StringUtils;
055import org.apache.commons.lang3.mutable.MutableObject;
056import org.w3c.dom.Attr;
057import org.w3c.dom.CDATASection;
058import org.w3c.dom.Document;
059import org.w3c.dom.Element;
060import org.w3c.dom.NamedNodeMap;
061import org.w3c.dom.Node;
062import org.w3c.dom.NodeList;
063import org.w3c.dom.Text;
064import org.xml.sax.EntityResolver;
065import org.xml.sax.InputSource;
066import org.xml.sax.SAXException;
067import org.xml.sax.SAXParseException;
068import org.xml.sax.helpers.DefaultHandler;
069
070/**
071 * <p>
072 * A specialized hierarchical configuration class that is able to parse XML documents.
073 * </p>
074 * <p>
075 * The parsed document will be stored keeping its structure. The class also tries to preserve as much information from
076 * the loaded XML document as possible, including comments and processing instructions. These will be contained in
077 * documents created by the {@code save()} methods, too.
078 * </p>
079 * <p>
080 * Like other file based configuration classes this class maintains the name and path to the loaded configuration file.
081 * These properties can be altered using several setter methods, but they are not modified by {@code save()} and
082 * {@code load()} methods. If XML documents contain relative paths to other documents (e.g. to a DTD), these references
083 * are resolved based on the path set for this configuration.
084 * </p>
085 * <p>
086 * By inheriting from {@link AbstractConfiguration} this class provides some extended functionality, e.g. interpolation
087 * of property values. Like in {@link PropertiesConfiguration} property values can contain delimiter characters (the
088 * comma ',' per default) and are then split into multiple values. This works for XML attributes and text content of
089 * elements as well. The delimiter can be escaped by a backslash. As an example consider the following XML fragment:
090 * </p>
091 *
092 * <pre>
093 * &lt;config&gt;
094 *   &lt;array&gt;10,20,30,40&lt;/array&gt;
095 *   &lt;scalar&gt;3\,1415&lt;/scalar&gt;
096 *   &lt;cite text="To be or not to be\, this is the question!"/&gt;
097 * &lt;/config&gt;
098 * </pre>
099 *
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 */
171public 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}