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