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