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