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