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