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