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