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