XMLConfiguration.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.configuration2;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.Reader;
  21. import java.io.StringReader;
  22. import java.io.StringWriter;
  23. import java.io.Writer;
  24. import java.net.URL;
  25. import java.util.ArrayList;
  26. import java.util.Collection;
  27. import java.util.Collections;
  28. import java.util.HashMap;
  29. import java.util.Iterator;
  30. import java.util.Map;

  31. import javax.xml.parsers.DocumentBuilder;
  32. import javax.xml.parsers.DocumentBuilderFactory;
  33. import javax.xml.parsers.ParserConfigurationException;
  34. import javax.xml.transform.OutputKeys;
  35. import javax.xml.transform.Result;
  36. import javax.xml.transform.Source;
  37. import javax.xml.transform.Transformer;
  38. import javax.xml.transform.dom.DOMSource;
  39. import javax.xml.transform.stream.StreamResult;

  40. import org.apache.commons.configuration2.convert.ListDelimiterHandler;
  41. import org.apache.commons.configuration2.ex.ConfigurationException;
  42. import org.apache.commons.configuration2.io.ConfigurationLogger;
  43. import org.apache.commons.configuration2.io.FileLocator;
  44. import org.apache.commons.configuration2.io.FileLocatorAware;
  45. import org.apache.commons.configuration2.io.InputStreamSupport;
  46. import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
  47. import org.apache.commons.configuration2.tree.ImmutableNode;
  48. import org.apache.commons.configuration2.tree.NodeTreeWalker;
  49. import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
  50. import org.apache.commons.lang3.StringUtils;
  51. import org.apache.commons.lang3.mutable.MutableObject;
  52. import org.w3c.dom.Attr;
  53. import org.w3c.dom.CDATASection;
  54. import org.w3c.dom.Document;
  55. import org.w3c.dom.Element;
  56. import org.w3c.dom.NamedNodeMap;
  57. import org.w3c.dom.Node;
  58. import org.w3c.dom.NodeList;
  59. import org.w3c.dom.Text;
  60. import org.xml.sax.EntityResolver;
  61. import org.xml.sax.InputSource;
  62. import org.xml.sax.SAXException;
  63. import org.xml.sax.SAXParseException;
  64. import org.xml.sax.helpers.DefaultHandler;

  65. /**
  66.  * <p>
  67.  * A specialized hierarchical configuration class that is able to parse XML documents.
  68.  * </p>
  69.  * <p>
  70.  * The parsed document will be stored keeping its structure. The class also tries to preserve as much information from
  71.  * the loaded XML document as possible, including comments and processing instructions. These will be contained in
  72.  * documents created by the {@code save()} methods, too.
  73.  * </p>
  74.  * <p>
  75.  * Like other file based configuration classes this class maintains the name and path to the loaded configuration file.
  76.  * These properties can be altered using several setter methods, but they are not modified by {@code save()} and
  77.  * {@code load()} methods. If XML documents contain relative paths to other documents (for example to a DTD), these references
  78.  * are resolved based on the path set for this configuration.
  79.  * </p>
  80.  * <p>
  81.  * By inheriting from {@link AbstractConfiguration} this class provides some extended functionality, for example interpolation
  82.  * of property values. Like in {@link PropertiesConfiguration} property values can contain delimiter characters (the
  83.  * comma ',' per default) and are then split into multiple values. This works for XML attributes and text content of
  84.  * elements as well. The delimiter can be escaped by a backslash. As an example consider the following XML fragment:
  85.  * </p>
  86.  *
  87.  * <pre>
  88.  * &lt;config&gt;
  89.  *   &lt;array&gt;10,20,30,40&lt;/array&gt;
  90.  *   &lt;scalar&gt;3\,1415&lt;/scalar&gt;
  91.  *   &lt;cite text="To be or not to be\, this is the question!"/&gt;
  92.  * &lt;/config&gt;
  93.  * </pre>
  94.  *
  95.  * <p>
  96.  * Here the content of the {@code array} element will be split at the commas, so the {@code array} key will be assigned
  97.  * 4 values. In the {@code scalar} property and the {@code text} attribute of the {@code cite} element the comma is
  98.  * escaped, so that no splitting is performed.
  99.  * </p>
  100.  * <p>
  101.  * The configuration API allows setting multiple values for a single attribute, for example something like the following is
  102.  * legal (assuming that the default expression engine is used):
  103.  * </p>
  104.  *
  105.  * <pre>
  106.  * XMLConfiguration config = new XMLConfiguration();
  107.  * config.addProperty(&quot;test.dir[@name]&quot;, &quot;C:\\Temp\\&quot;);
  108.  * config.addProperty(&quot;test.dir[@name]&quot;, &quot;D:\\Data\\&quot;);
  109.  * </pre>
  110.  *
  111.  * <p>
  112.  * However, in XML such a constellation is not supported; an attribute can appear only once for a single element.
  113.  * Therefore, an attempt to save a configuration which violates this condition will throw an exception.
  114.  * </p>
  115.  * <p>
  116.  * Like other {@code Configuration} implementations, {@code XMLConfiguration} uses a {@link ListDelimiterHandler} object
  117.  * for controlling list split operations. Per default, a list delimiter handler object is set which disables this
  118.  * feature. XML has a built-in support for complex structures including list properties; therefore, list splitting is
  119.  * not that relevant for this configuration type. Nevertheless, by setting an alternative {@code ListDelimiterHandler}
  120.  * implementation, this feature can be enabled. It works as for any other concrete {@code Configuration} implementation.
  121.  * </p>
  122.  * <p>
  123.  * Whitespace in the content of XML documents is trimmed per default. In most cases this is desired. However, sometimes
  124.  * whitespace is indeed important and should be treated as part of the value of a property as in the following example:
  125.  * </p>
  126.  *
  127.  * <pre>
  128.  *   &lt;indent&gt;    &lt;/indent&gt;
  129.  * </pre>
  130.  *
  131.  * <p>
  132.  * Per default the spaces in the {@code indent} element will be trimmed resulting in an empty element. To tell
  133.  * {@code XMLConfiguration} that spaces are relevant the {@code xml:space} attribute can be used, which is defined in
  134.  * the <a href="http://www.w3.org/TR/REC-xml/#sec-white-space">XML specification</a>. This will look as follows:
  135.  * </p>
  136.  *
  137.  * <pre>
  138.  *   &lt;indent <strong>xml:space=&quot;preserve&quot;</strong>&gt;    &lt;/indent&gt;
  139.  * </pre>
  140.  *
  141.  * <p>
  142.  * The value of the {@code indent} property will now contain the spaces.
  143.  * </p>
  144.  * <p>
  145.  * {@code XMLConfiguration} implements the {@link FileBasedConfiguration} interface and thus can be used together with a
  146.  * file-based builder to load XML configuration files from various sources like files, URLs, or streams.
  147.  * </p>
  148.  * <p>
  149.  * Like other {@code Configuration} implementations, this class uses a {@code Synchronizer} object to control concurrent
  150.  * access. By choosing a suitable implementation of the {@code Synchronizer} interface, an instance can be made
  151.  * thread-safe or not. Note that access to most of the properties typically set through a builder is not protected by
  152.  * the {@code Synchronizer}. The intended usage is that these properties are set once at construction time through the
  153.  * builder and after that remain constant. If you wish to change such properties during life time of an instance, you
  154.  * have to use the {@code lock()} and {@code unlock()} methods manually to ensure that other threads see your changes.
  155.  * </p>
  156.  * <p>
  157.  * More information about the basic functionality supported by {@code XMLConfiguration} can be found at the user's guide
  158.  * at <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> Basic
  159.  * features and AbstractConfiguration</a>. There is also a separate chapter dealing with
  160.  * <a href="commons.apache.org/proper/commons-configuration/userguide/howto_xml.html"> XML Configurations</a> in
  161.  * special.
  162.  * </p>
  163.  *
  164.  * @since 1.0
  165.  */
  166. public class XMLConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware, InputStreamSupport {
  167.     /**
  168.      * A concrete {@code BuilderVisitor} that can construct XML documents.
  169.      */
  170.     static class XMLBuilderVisitor extends BuilderVisitor {
  171.         /**
  172.          * Removes all attributes of the given element.
  173.          *
  174.          * @param elem the element
  175.          */
  176.         private static void clearAttributes(final Element elem) {
  177.             final NamedNodeMap attributes = elem.getAttributes();
  178.             for (int i = 0; i < attributes.getLength(); i++) {
  179.                 elem.removeAttribute(attributes.item(i).getNodeName());
  180.             }
  181.         }

  182.         /**
  183.          * Returns the only text node of an element for update. This method is called when the element's text changes. Then all
  184.          * text nodes except for the first are removed. A reference to the first is returned or <strong>null</strong> if there is no text
  185.          * node at all.
  186.          *
  187.          * @param elem the element
  188.          * @return the first and only text node
  189.          */
  190.         private static Text findTextNodeForUpdate(final Element elem) {
  191.             Text result = null;
  192.             // Find all Text nodes
  193.             final NodeList children = elem.getChildNodes();
  194.             final Collection<Node> textNodes = new ArrayList<>();
  195.             for (int i = 0; i < children.getLength(); i++) {
  196.                 final Node nd = children.item(i);
  197.                 if (nd instanceof Text) {
  198.                     if (result == null) {
  199.                         result = (Text) nd;
  200.                     } else {
  201.                         textNodes.add(nd);
  202.                     }
  203.                 }
  204.             }

  205.             // We don't want CDATAs
  206.             if (result instanceof CDATASection) {
  207.                 textNodes.add(result);
  208.                 result = null;
  209.             }

  210.             // Remove all but the first Text node
  211.             textNodes.forEach(elem::removeChild);
  212.             return result;
  213.         }

  214.         /**
  215.          * Helper method for updating the values of all attributes of the specified node.
  216.          *
  217.          * @param node the affected node
  218.          * @param elem the element that is associated with this node
  219.          */
  220.         private static void updateAttributes(final ImmutableNode node, final Element elem) {
  221.             if (node != null && elem != null) {
  222.                 clearAttributes(elem);
  223.                 node.getAttributes().forEach((k, v) -> {
  224.                     if (v != null) {
  225.                         elem.setAttribute(k, v.toString());
  226.                     }
  227.                 });
  228.             }
  229.         }

  230.         /** Stores the document to be constructed. */
  231.         private final Document document;

  232.         /** The element mapping. */
  233.         private final Map<Node, Node> elementMapping;

  234.         /** A mapping for the references for new nodes. */
  235.         private final Map<ImmutableNode, Element> newElements;

  236.         /** Stores the list delimiter handler . */
  237.         private final ListDelimiterHandler listDelimiterHandler;

  238.         /**
  239.          * Creates a new instance of {@code XMLBuilderVisitor}.
  240.          *
  241.          * @param docHelper the document helper
  242.          * @param handler the delimiter handler for properties with multiple values
  243.          */
  244.         public XMLBuilderVisitor(final XMLDocumentHelper docHelper, final ListDelimiterHandler handler) {
  245.             document = docHelper.getDocument();
  246.             elementMapping = docHelper.getElementMapping();
  247.             listDelimiterHandler = handler;
  248.             newElements = new HashMap<>();
  249.         }

  250.         /**
  251.          * Helper method for accessing the element of the specified node.
  252.          *
  253.          * @param node the node
  254.          * @param refHandler the {@code ReferenceNodeHandler}
  255.          * @return the element of this node
  256.          */
  257.         private Element getElement(final ImmutableNode node, final ReferenceNodeHandler refHandler) {
  258.             final Element elementNew = newElements.get(node);
  259.             if (elementNew != null) {
  260.                 return elementNew;
  261.             }

  262.             // special treatment for root node of the hierarchy
  263.             final Object reference = refHandler.getReference(node);
  264.             final Node element;
  265.             if (reference instanceof XMLDocumentHelper) {
  266.                 element = ((XMLDocumentHelper) reference).getDocument().getDocumentElement();
  267.             } else if (reference instanceof XMLListReference) {
  268.                 element = ((XMLListReference) reference).getElement();
  269.             } else {
  270.                 element = (Node) reference;
  271.             }
  272.             return element != null ? (Element) elementMapping.get(element) : document.getDocumentElement();
  273.         }

  274.         /**
  275.          * Updates the current XML document regarding removed nodes. The elements associated with removed nodes are removed from
  276.          * the document.
  277.          *
  278.          * @param refHandler the {@code ReferenceNodeHandler}
  279.          */
  280.         public void handleRemovedNodes(final ReferenceNodeHandler refHandler) {
  281.             refHandler.removedReferences().stream().filter(Node.class::isInstance).forEach(ref -> removeReference(elementMapping.get(ref)));
  282.         }

  283.         /**
  284.          * {@inheritDoc} This implementation ensures that the correct XML element is created and inserted between the given
  285.          * siblings.
  286.          */
  287.         @Override
  288.         protected void insert(final ImmutableNode newNode, final ImmutableNode parent, final ImmutableNode sibling1, final ImmutableNode sibling2,
  289.             final ReferenceNodeHandler refHandler) {
  290.             if (XMLListReference.isListNode(newNode, refHandler)) {
  291.                 return;
  292.             }

  293.             final Element elem = document.createElement(newNode.getNodeName());
  294.             newElements.put(newNode, elem);
  295.             updateAttributes(newNode, elem);
  296.             if (newNode.getValue() != null) {
  297.                 final String txt = String.valueOf(listDelimiterHandler.escape(newNode.getValue(), ListDelimiterHandler.NOOP_TRANSFORMER));
  298.                 elem.appendChild(document.createTextNode(txt));
  299.             }
  300.             if (sibling2 == null) {
  301.                 getElement(parent, refHandler).appendChild(elem);
  302.             } else if (sibling1 != null) {
  303.                 getElement(parent, refHandler).insertBefore(elem, getElement(sibling1, refHandler).getNextSibling());
  304.             } else {
  305.                 getElement(parent, refHandler).insertBefore(elem, getElement(parent, refHandler).getFirstChild());
  306.             }
  307.         }

  308.         /**
  309.          * Processes the specified document, updates element values, and adds new nodes to the hierarchy.
  310.          *
  311.          * @param refHandler the {@code ReferenceNodeHandler}
  312.          */
  313.         public void processDocument(final ReferenceNodeHandler refHandler) {
  314.             updateAttributes(refHandler.getRootNode(), document.getDocumentElement());
  315.             NodeTreeWalker.INSTANCE.walkDFS(refHandler.getRootNode(), this, refHandler);
  316.         }

  317.         /**
  318.          * Updates the associated XML elements when a node is removed.
  319.          *
  320.          * @param element the element to be removed
  321.          */
  322.         private void removeReference(final Node element) {
  323.             final Node parentElem = element.getParentNode();
  324.             if (parentElem != null) {
  325.                 parentElem.removeChild(element);
  326.             }
  327.         }

  328.         /**
  329.          * {@inheritDoc} This implementation determines the XML element associated with the given node. Then this element's
  330.          * value and attributes are set accordingly.
  331.          */
  332.         @Override
  333.         protected void update(final ImmutableNode node, final Object reference, final ReferenceNodeHandler refHandler) {
  334.             if (XMLListReference.isListNode(node, refHandler)) {
  335.                 if (XMLListReference.isFirstListItem(node, refHandler)) {
  336.                     final String value = XMLListReference.listValue(node, refHandler, listDelimiterHandler);
  337.                     updateElement(node, refHandler, value);
  338.                 }
  339.             } else {
  340.                 final Object value = listDelimiterHandler.escape(refHandler.getValue(node), ListDelimiterHandler.NOOP_TRANSFORMER);
  341.                 updateElement(node, refHandler, value);
  342.             }
  343.         }

  344.         /**
  345.          * Updates the node's value if it represents an element node.
  346.          *
  347.          * @param element the element
  348.          * @param value the new value
  349.          */
  350.         private void updateElement(final Element element, final Object value) {
  351.             Text txtNode = findTextNodeForUpdate(element);
  352.             if (value == null) {
  353.                 // remove text
  354.                 if (txtNode != null) {
  355.                     element.removeChild(txtNode);
  356.                 }
  357.             } else {
  358.                 final String newValue = String.valueOf(value);
  359.                 if (txtNode == null) {
  360.                     txtNode = document.createTextNode(newValue);
  361.                     if (element.getFirstChild() != null) {
  362.                         element.insertBefore(txtNode, element.getFirstChild());
  363.                     } else {
  364.                         element.appendChild(txtNode);
  365.                     }
  366.                 } else {
  367.                     txtNode.setNodeValue(newValue);
  368.                 }
  369.             }
  370.         }

  371.         private void updateElement(final ImmutableNode node, final ReferenceNodeHandler refHandler, final Object value) {
  372.             final Element element = getElement(node, refHandler);
  373.             updateElement(element, value);
  374.             updateAttributes(node, element);
  375.         }
  376.     }

  377.     /** Constant for the default indent size. */
  378.     static final int DEFAULT_INDENT_SIZE = 2;

  379.     /** Constant for output property name used on a transformer to specify the indent amount. */
  380.     static final String INDENT_AMOUNT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount";

  381.     /** Constant for the default root element name. */
  382.     private static final String DEFAULT_ROOT_NAME = "configuration";

  383.     /** Constant for the name of the space attribute. */
  384.     private static final String ATTR_SPACE = "xml:space";

  385.     /** Constant for an internally used space attribute. */
  386.     private static final String ATTR_SPACE_INTERNAL = "config-xml:space";

  387.     /** Constant for the xml:space value for preserving whitespace. */
  388.     private static final String VALUE_PRESERVE = "preserve";

  389.     /** Schema Langauge key for the parser */
  390.     private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

  391.     /** Schema Language for the parser */
  392.     private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";

  393.     /**
  394.      * Determines the number of child elements of this given node with the specified node name.
  395.      *
  396.      * @param parent the parent node
  397.      * @param name the name in question
  398.      * @return the number of child elements with this name
  399.      */
  400.     private static int countChildElements(final Node parent, final String name) {
  401.         final NodeList childNodes = parent.getChildNodes();
  402.         int count = 0;
  403.         for (int i = 0; i < childNodes.getLength(); i++) {
  404.             final Node item = childNodes.item(i);
  405.             if (item instanceof Element && name.equals(((Element) item).getTagName())) {
  406.                 count++;
  407.             }
  408.         }
  409.         return count;
  410.     }

  411.     /**
  412.      * Determines the value of a configuration node. This method mainly checks whether the text value is to be trimmed or
  413.      * not. This is normally defined by the trim flag. However, if the node has children and its content is only whitespace,
  414.      * then it makes no sense to store any value; this would only scramble layout when the configuration is saved again.
  415.      *
  416.      * @param content the text content of this node
  417.      * @param hasChildren a flag whether the node has children
  418.      * @param trimFlag the trim flag
  419.      * @return the value to be stored for this node
  420.      */
  421.     private static String determineValue(final String content, final boolean hasChildren, final boolean trimFlag) {
  422.         final boolean shouldTrim = trimFlag || StringUtils.isBlank(content) && hasChildren;
  423.         return shouldTrim ? content.trim() : content;
  424.     }

  425.     /**
  426.      * Checks whether an element defines a complete list. If this is the case, extended list handling can be applied.
  427.      *
  428.      * @param element the element to be checked
  429.      * @return a flag whether this is the only element defining the list
  430.      */
  431.     private static boolean isSingleElementList(final Element element) {
  432.         final Node parentNode = element.getParentNode();
  433.         return countChildElements(parentNode, element.getTagName()) == 1;
  434.     }

  435.     /**
  436.      * Helper method for initializing the attributes of a configuration node from the given XML element.
  437.      *
  438.      * @param element the current XML element
  439.      * @return a map with all attribute values extracted for the current node
  440.      */
  441.     private static Map<String, String> processAttributes(final Element element) {
  442.         final NamedNodeMap attributes = element.getAttributes();
  443.         final Map<String, String> attrmap = new HashMap<>();

  444.         for (int i = 0; i < attributes.getLength(); ++i) {
  445.             final Node w3cNode = attributes.item(i);
  446.             if (w3cNode instanceof Attr) {
  447.                 final Attr attr = (Attr) w3cNode;
  448.                 attrmap.put(attr.getName(), attr.getValue());
  449.             }
  450.         }

  451.         return attrmap;
  452.     }

  453.     /**
  454.      * Checks whether the content of the current XML element should be trimmed. This method checks whether a
  455.      * {@code xml:space} attribute is present and evaluates its value. See
  456.      * <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
  457.      * details.
  458.      *
  459.      * @param element the current XML element
  460.      * @param currentTrim the current trim flag
  461.      * @return a flag whether the content of this element should be trimmed
  462.      */
  463.     private static boolean shouldTrim(final Element element, final boolean currentTrim) {
  464.         final Attr attr = element.getAttributeNode(ATTR_SPACE);

  465.         if (attr == null) {
  466.             return currentTrim;
  467.         }
  468.         return !VALUE_PRESERVE.equals(attr.getValue());
  469.     }

  470.     /** Stores the name of the root element. */
  471.     private String rootElementName;

  472.     /** Stores the public ID from the DOCTYPE. */
  473.     private String publicID;

  474.     /** Stores the system ID from the DOCTYPE. */
  475.     private String systemID;

  476.     /** Stores the document builder that should be used for loading. */
  477.     private DocumentBuilder documentBuilder;

  478.     /** Stores a flag whether DTD or Schema validation should be performed. */
  479.     private boolean validating;

  480.     /** Stores a flag whether DTD or Schema validation is used */
  481.     private boolean schemaValidation;

  482.     /** The EntityResolver to use */
  483.     private EntityResolver entityResolver = new DefaultEntityResolver();

  484.     /** The current file locator. */
  485.     private FileLocator locator;

  486.     /**
  487.      * Creates a new instance of {@code XMLConfiguration}.
  488.      */
  489.     public XMLConfiguration() {
  490.         initLogger(new ConfigurationLogger(XMLConfiguration.class));
  491.     }

  492.     /**
  493.      * Creates a new instance of {@code XMLConfiguration} and copies the content of the passed in configuration into this
  494.      * object. Note that only the data of the passed in configuration will be copied. If, for instance, the other
  495.      * configuration is a {@code XMLConfiguration}, too, things like comments or processing instructions will be lost.
  496.      *
  497.      * @param c the configuration to copy
  498.      * @since 1.4
  499.      */
  500.     public XMLConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
  501.         super(c);
  502.         rootElementName = c != null ? c.getRootElementName() : null;
  503.         initLogger(new ConfigurationLogger(XMLConfiguration.class));
  504.     }

  505.     /**
  506.      * Helper method for building the internal storage hierarchy. The XML elements are transformed into node objects.
  507.      *
  508.      * @param node a builder for the current node
  509.      * @param refValue stores the text value of the element
  510.      * @param element the current XML element
  511.      * @param elemRefs a map for assigning references objects to nodes; can be <strong>null</strong>, then reference objects are
  512.      *        irrelevant
  513.      * @param trim a flag whether the text content of elements should be trimmed; this controls the whitespace handling
  514.      * @param level the current level in the hierarchy
  515.      * @return a map with all attribute values extracted for the current node; this map also contains the value of the trim
  516.      *         flag for this node under the key {@value #ATTR_SPACE}
  517.      */
  518.     private Map<String, String> constructHierarchy(final ImmutableNode.Builder node, final MutableObject<String> refValue, final Element element,
  519.         final Map<ImmutableNode, Object> elemRefs, final boolean trim, final int level) {
  520.         final boolean trimFlag = shouldTrim(element, trim);
  521.         final Map<String, String> attributes = processAttributes(element);
  522.         attributes.put(ATTR_SPACE_INTERNAL, String.valueOf(trimFlag));
  523.         final StringBuilder buffer = new StringBuilder();
  524.         final NodeList list = element.getChildNodes();
  525.         boolean hasChildren = false;

  526.         for (int i = 0; i < list.getLength(); i++) {
  527.             final Node w3cNode = list.item(i);
  528.             if (w3cNode instanceof Element) {
  529.                 final Element child = (Element) w3cNode;
  530.                 final ImmutableNode.Builder childNode = new ImmutableNode.Builder();
  531.                 childNode.name(child.getTagName());
  532.                 final MutableObject<String> refChildValue = new MutableObject<>();
  533.                 final Map<String, String> attrmap = constructHierarchy(childNode, refChildValue, child, elemRefs, trimFlag, level + 1);
  534.                 final boolean childTrim = Boolean.parseBoolean(attrmap.remove(ATTR_SPACE_INTERNAL));
  535.                 childNode.addAttributes(attrmap);
  536.                 final ImmutableNode newChild = createChildNodeWithValue(node, childNode, child, refChildValue.getValue(), childTrim, attrmap, elemRefs);
  537.                 if (elemRefs != null && !elemRefs.containsKey(newChild)) {
  538.                     elemRefs.put(newChild, child);
  539.                 }
  540.                 hasChildren = true;
  541.             } else if (w3cNode instanceof Text) {
  542.                 final Text data = (Text) w3cNode;
  543.                 buffer.append(data.getData());
  544.             }
  545.         }

  546.         boolean childrenFlag = false;
  547.         if (hasChildren || trimFlag) {
  548.             childrenFlag = hasChildren || attributes.size() > 1;
  549.         }
  550.         final String text = determineValue(buffer.toString(), childrenFlag, trimFlag);
  551.         if (!text.isEmpty() || !childrenFlag && level != 0) {
  552.             refValue.setValue(text);
  553.         }
  554.         return attributes;
  555.     }

  556.     /**
  557.      * Creates a new child node, assigns its value, and adds it to its parent. This method also deals with elements whose
  558.      * value is a list. In this case multiple child elements must be added. The return value is the first child node which
  559.      * was added.
  560.      *
  561.      * @param parent the builder for the parent element
  562.      * @param child the builder for the child element
  563.      * @param elem the associated XML element
  564.      * @param value the value of the child element
  565.      * @param trim flag whether texts of elements should be trimmed
  566.      * @param attrmap a map with the attributes of the current node
  567.      * @param elemRefs a map for assigning references objects to nodes; can be <strong>null</strong>, then reference objects are
  568.      *        irrelevant
  569.      * @return the first child node added to the parent
  570.      */
  571.     private ImmutableNode createChildNodeWithValue(final ImmutableNode.Builder parent, final ImmutableNode.Builder child, final Element elem,
  572.         final String value, final boolean trim, final Map<String, String> attrmap, final Map<ImmutableNode, Object> elemRefs) {
  573.         final ImmutableNode addedChildNode;
  574.         final Collection<String> values;

  575.         if (value != null) {
  576.             values = getListDelimiterHandler().split(value, trim);
  577.         } else {
  578.             values = Collections.emptyList();
  579.         }

  580.         if (values.size() > 1) {
  581.             final Map<ImmutableNode, Object> refs = isSingleElementList(elem) ? elemRefs : null;
  582.             final Iterator<String> it = values.iterator();
  583.             // Create new node for the original child's first value
  584.             child.value(it.next());
  585.             addedChildNode = child.create();
  586.             parent.addChild(addedChildNode);
  587.             XMLListReference.assignListReference(refs, addedChildNode, elem);

  588.             // add multiple new children
  589.             while (it.hasNext()) {
  590.                 final ImmutableNode.Builder c = new ImmutableNode.Builder();
  591.                 c.name(addedChildNode.getNodeName());
  592.                 c.value(it.next());
  593.                 c.addAttributes(attrmap);
  594.                 final ImmutableNode newChild = c.create();
  595.                 parent.addChild(newChild);
  596.                 XMLListReference.assignListReference(refs, newChild, null);
  597.             }
  598.         } else {
  599.             if (values.size() == 1) {
  600.                 // we will have to replace the value because it might
  601.                 // contain escaped delimiters
  602.                 child.value(values.iterator().next());
  603.             }
  604.             addedChildNode = child.create();
  605.             parent.addChild(addedChildNode);
  606.         }

  607.         return addedChildNode;
  608.     }

  609.     /**
  610.      * Creates a DOM document from the internal tree of configuration nodes.
  611.      *
  612.      * @return the new document
  613.      * @throws ConfigurationException if an error occurs
  614.      */
  615.     private Document createDocument() throws ConfigurationException {
  616.         final ReferenceNodeHandler handler = getReferenceHandler();
  617.         final XMLDocumentHelper docHelper = (XMLDocumentHelper) handler.getReference(handler.getRootNode());
  618.         final XMLDocumentHelper newHelper = docHelper == null ? XMLDocumentHelper.forNewDocument(getRootElementName()) : docHelper.createCopy();

  619.         final XMLBuilderVisitor builder = new XMLBuilderVisitor(newHelper, getListDelimiterHandler());
  620.         builder.handleRemovedNodes(handler);
  621.         builder.processDocument(handler);
  622.         initRootElementText(newHelper.getDocument(), getModel().getNodeHandler().getRootNode().getValue());
  623.         return newHelper.getDocument();
  624.     }

  625.     /**
  626.      * Creates the {@code DocumentBuilder} to be used for loading files. This implementation checks whether a specific
  627.      * {@code DocumentBuilder} has been set. If this is the case, this one is used. Otherwise a default builder is created.
  628.      * Depending on the value of the validating flag this builder will be a validating or a non validating
  629.      * {@code DocumentBuilder}.
  630.      *
  631.      * @return the {@code DocumentBuilder} for loading configuration files
  632.      * @throws ParserConfigurationException if an error occurs
  633.      * @since 1.2
  634.      */
  635.     protected DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
  636.         if (getDocumentBuilder() != null) {
  637.             return getDocumentBuilder();
  638.         }
  639.         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  640.         if (isValidating()) {
  641.             factory.setValidating(true);
  642.             if (isSchemaValidation()) {
  643.                 factory.setNamespaceAware(true);
  644.                 factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
  645.             }
  646.         }

  647.         final DocumentBuilder result = factory.newDocumentBuilder();
  648.         result.setEntityResolver(this.entityResolver);

  649.         if (isValidating()) {
  650.             // register an error handler which detects validation errors
  651.             result.setErrorHandler(new DefaultHandler() {
  652.                 @Override
  653.                 public void error(final SAXParseException ex) throws SAXException {
  654.                     throw ex;
  655.                 }
  656.             });
  657.         }
  658.         return result;
  659.     }

  660.     /**
  661.      * Creates and initializes the transformer used for save operations. This base implementation initializes all of the
  662.      * default settings like indentation mode and the DOCTYPE. Derived classes may overload this method if they have
  663.      * specific needs.
  664.      *
  665.      * @return the transformer to use for a save operation
  666.      * @throws ConfigurationException if an error occurs
  667.      * @since 1.3
  668.      */
  669.     protected Transformer createTransformer() throws ConfigurationException {
  670.         final Transformer transformer = XMLDocumentHelper.createTransformer();

  671.         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
  672.         transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, Integer.toString(DEFAULT_INDENT_SIZE));
  673.         if (locator != null && locator.getEncoding() != null) {
  674.             transformer.setOutputProperty(OutputKeys.ENCODING, locator.getEncoding());
  675.         }
  676.         if (publicID != null) {
  677.             transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicID);
  678.         }
  679.         if (systemID != null) {
  680.             transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemID);
  681.         }

  682.         return transformer;
  683.     }

  684.     /**
  685.      * Gets the XML document this configuration was loaded from. The return value is <strong>null</strong> if this configuration
  686.      * was not loaded from a XML document.
  687.      *
  688.      * @return the XML document this configuration was loaded from
  689.      */
  690.     public Document getDocument() {
  691.         final XMLDocumentHelper docHelper = getDocumentHelper();
  692.         return docHelper != null ? docHelper.getDocument() : null;
  693.     }

  694.     /**
  695.      * Gets the {@code DocumentBuilder} object that is used for loading documents. If no specific builder has been set,
  696.      * this method returns <strong>null</strong>.
  697.      *
  698.      * @return the {@code DocumentBuilder} for loading new documents
  699.      * @since 1.2
  700.      */
  701.     public DocumentBuilder getDocumentBuilder() {
  702.         return documentBuilder;
  703.     }

  704.     /**
  705.      * Gets the helper object for managing the underlying document.
  706.      *
  707.      * @return the {@code XMLDocumentHelper}
  708.      */
  709.     private XMLDocumentHelper getDocumentHelper() {
  710.         final ReferenceNodeHandler handler = getReferenceHandler();
  711.         return (XMLDocumentHelper) handler.getReference(handler.getRootNode());
  712.     }

  713.     /**
  714.      * Gets the EntityResolver.
  715.      *
  716.      * @return The EntityResolver.
  717.      * @since 1.7
  718.      */
  719.     public EntityResolver getEntityResolver() {
  720.         return this.entityResolver;
  721.     }

  722.     /**
  723.      * Gets the public ID of the DOCTYPE declaration from the loaded XML document. This is <strong>null</strong> if no document has
  724.      * been loaded yet or if the document does not contain a DOCTYPE declaration with a public ID.
  725.      *
  726.      * @return the public ID
  727.      * @since 1.3
  728.      */
  729.     public String getPublicID() {
  730.         return syncReadValue(publicID, false);
  731.     }

  732.     /**
  733.      * Gets the extended node handler with support for references.
  734.      *
  735.      * @return the {@code ReferenceNodeHandler}
  736.      */
  737.     private ReferenceNodeHandler getReferenceHandler() {
  738.         return getSubConfigurationParentModel().getReferenceNodeHandler();
  739.     }

  740.     /**
  741.      * Gets the name of the root element. If this configuration was loaded from a XML document, the name of this
  742.      * document's root element is returned. Otherwise it is possible to set a name for the root element that will be used
  743.      * when this configuration is stored.
  744.      *
  745.      * @return the name of the root element
  746.      */
  747.     @Override
  748.     protected String getRootElementNameInternal() {
  749.         final Document doc = getDocument();
  750.         if (doc == null) {
  751.             return rootElementName == null ? DEFAULT_ROOT_NAME : rootElementName;
  752.         }
  753.         return doc.getDocumentElement().getNodeName();
  754.     }

  755.     /**
  756.      * Gets the system ID of the DOCTYPE declaration from the loaded XML document. This is <strong>null</strong> if no document has
  757.      * been loaded yet or if the document does not contain a DOCTYPE declaration with a system ID.
  758.      *
  759.      * @return the system ID
  760.      * @since 1.3
  761.      */
  762.     public String getSystemID() {
  763.         return syncReadValue(systemID, false);
  764.     }

  765.     /**
  766.      * {@inheritDoc} Stores the passed in locator for the upcoming IO operation.
  767.      */
  768.     @Override
  769.     public void initFileLocator(final FileLocator loc) {
  770.         locator = loc;
  771.     }

  772.     /**
  773.      * Initializes this configuration from an XML document.
  774.      *
  775.      * @param docHelper the helper object with the document to be parsed
  776.      * @param elemRefs a flag whether references to the XML elements should be set
  777.      */
  778.     private void initProperties(final XMLDocumentHelper docHelper, final boolean elemRefs) {
  779.         final Document document = docHelper.getDocument();
  780.         setPublicID(docHelper.getSourcePublicID());
  781.         setSystemID(docHelper.getSourceSystemID());

  782.         final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
  783.         final MutableObject<String> rootValue = new MutableObject<>();
  784.         final Map<ImmutableNode, Object> elemRefMap = elemRefs ? new HashMap<>() : null;
  785.         final Map<String, String> attributes = constructHierarchy(rootBuilder, rootValue, document.getDocumentElement(), elemRefMap, true, 0);
  786.         attributes.remove(ATTR_SPACE_INTERNAL);
  787.         final ImmutableNode top = rootBuilder.value(rootValue.getValue()).addAttributes(attributes).create();
  788.         getSubConfigurationParentModel().mergeRoot(top, document.getDocumentElement().getTagName(), elemRefMap, elemRefs ? docHelper : null, this);
  789.     }

  790.     /**
  791.      * Sets the text of the root element of a newly created XML Document.
  792.      *
  793.      * @param doc the document
  794.      * @param value the new text to be set
  795.      */
  796.     private void initRootElementText(final Document doc, final Object value) {
  797.         final Element elem = doc.getDocumentElement();
  798.         final NodeList children = elem.getChildNodes();

  799.         // Remove all existing text nodes
  800.         for (int i = 0; i < children.getLength(); i++) {
  801.             final Node nd = children.item(i);
  802.             if (nd.getNodeType() == Node.TEXT_NODE) {
  803.                 elem.removeChild(nd);
  804.             }
  805.         }

  806.         if (value != null) {
  807.             // Add a new text node
  808.             elem.appendChild(doc.createTextNode(String.valueOf(value)));
  809.         }
  810.     }

  811.     /**
  812.      * Returns the value of the schemaValidation flag.
  813.      *
  814.      * @return the schemaValidation flag
  815.      * @since 1.7
  816.      */
  817.     public boolean isSchemaValidation() {
  818.         return schemaValidation;
  819.     }

  820.     /**
  821.      * Returns the value of the validating flag.
  822.      *
  823.      * @return the validating flag
  824.      * @since 1.2
  825.      */
  826.     public boolean isValidating() {
  827.         return validating;
  828.     }

  829.     /**
  830.      * Loads a configuration file from the specified input source.
  831.      *
  832.      * @param source the input source
  833.      * @throws ConfigurationException if an error occurs
  834.      */
  835.     private void load(final InputSource source) throws ConfigurationException {
  836.         if (locator == null) {
  837.             throw new ConfigurationException(
  838.                 "Load operation not properly " + "initialized! Do not call read(InputStream) directly," + " but use a FileHandler to load a configuration.");
  839.         }

  840.         try {
  841.             final URL sourceURL = locator.getSourceURL();
  842.             if (sourceURL != null) {
  843.                 source.setSystemId(sourceURL.toString());
  844.             }

  845.             final DocumentBuilder builder = createDocumentBuilder();
  846.             final Document newDocument = builder.parse(source);
  847.             final Document oldDocument = getDocument();
  848.             initProperties(XMLDocumentHelper.forSourceDocument(newDocument), oldDocument == null);
  849.         } catch (final SAXParseException spe) {
  850.             throw new ConfigurationException("Error parsing " + source.getSystemId(), spe);
  851.         } catch (final Exception e) {
  852.             getLogger().debug("Unable to load the configuration: " + e);
  853.             throw new ConfigurationException("Unable to load the configuration", e);
  854.         }
  855.     }

  856.     /**
  857.      * Loads the configuration from the given input stream. This is analogous to {@link #read(Reader)}, but data is read
  858.      * from a stream. Note that this method will be called most time when reading an XML configuration source. By reading
  859.      * XML documents directly from an input stream, the file's encoding can be correctly dealt with.
  860.      *
  861.      * @param in the input stream
  862.      * @throws ConfigurationException if an error occurs
  863.      * @throws IOException if an IO error occurs
  864.      */
  865.     @Override
  866.     public void read(final InputStream in) throws ConfigurationException, IOException {
  867.         load(new InputSource(in));
  868.     }

  869.     /**
  870.      * Loads the configuration from the given reader. Note that the {@code clear()} method is not called, so the properties
  871.      * contained in the loaded file will be added to the current set of properties.
  872.      *
  873.      * @param in the reader
  874.      * @throws ConfigurationException if an error occurs
  875.      * @throws IOException if an IO error occurs
  876.      */
  877.     @Override
  878.     public void read(final Reader in) throws ConfigurationException, IOException {
  879.         load(new InputSource(in));
  880.     }

  881.     /**
  882.      * Sets the {@code DocumentBuilder} object to be used for loading documents. This method makes it possible to specify
  883.      * the exact document builder. So an application can create a builder, configure it for its special needs, and then pass
  884.      * it to this method.
  885.      *
  886.      * @param documentBuilder the document builder to be used; if undefined, a default builder will be used
  887.      * @since 1.2
  888.      */
  889.     public void setDocumentBuilder(final DocumentBuilder documentBuilder) {
  890.         this.documentBuilder = documentBuilder;
  891.     }

  892.     /**
  893.      * Sets a new EntityResolver. Setting this will cause RegisterEntityId to have no effect.
  894.      *
  895.      * @param resolver The EntityResolver to use.
  896.      * @since 1.7
  897.      */
  898.     public void setEntityResolver(final EntityResolver resolver) {
  899.         this.entityResolver = resolver;
  900.     }

  901.     /**
  902.      * Sets the public ID of the DOCTYPE declaration. When this configuration is saved, a DOCTYPE declaration will be
  903.      * constructed that contains this public ID.
  904.      *
  905.      * @param publicID the public ID
  906.      * @since 1.3
  907.      */
  908.     public void setPublicID(final String publicID) {
  909.         syncWrite(() -> this.publicID = publicID, false);
  910.     }

  911.     /**
  912.      * Sets the name of the root element. This name is used when this configuration object is stored in an XML file. Note
  913.      * that setting the name of the root element works only if this configuration has been newly created. If the
  914.      * configuration was loaded from an XML file, the name cannot be changed and an {@code UnsupportedOperationException}
  915.      * exception is thrown. Whether this configuration has been loaded from an XML document or not can be found out using
  916.      * the {@code getDocument()} method.
  917.      *
  918.      * @param name the name of the root element
  919.      */
  920.     public void setRootElementName(final String name) {
  921.         beginRead(true);
  922.         try {
  923.             if (getDocument() != null) {
  924.                 throw new UnsupportedOperationException("The name of the root element " + "cannot be changed when loaded from an XML document!");
  925.             }
  926.             rootElementName = name;
  927.         } finally {
  928.             endRead();
  929.         }
  930.     }

  931.     /**
  932.      * Sets the value of the schemaValidation flag. This flag determines whether DTD or Schema validation should be used.
  933.      * This flag is evaluated only if no custom {@code DocumentBuilder} was set. If set to true the XML document must
  934.      * contain a schemaLocation definition that provides resolvable hints to the required schemas.
  935.      *
  936.      * @param schemaValidation the validating flag
  937.      * @since 1.7
  938.      */
  939.     public void setSchemaValidation(final boolean schemaValidation) {
  940.         this.schemaValidation = schemaValidation;
  941.         if (schemaValidation) {
  942.             this.validating = true;
  943.         }
  944.     }

  945.     /**
  946.      * Sets the system ID of the DOCTYPE declaration. When this configuration is saved, a DOCTYPE declaration will be
  947.      * constructed that contains this system ID.
  948.      *
  949.      * @param systemID the system ID
  950.      * @since 1.3
  951.      */
  952.     public void setSystemID(final String systemID) {
  953.         syncWrite(() -> this.systemID = systemID, false);
  954.     }

  955.     /**
  956.      * Sets the value of the validating flag. This flag determines whether DTD/Schema validation should be performed when
  957.      * loading XML documents. This flag is evaluated only if no custom {@code DocumentBuilder} was set.
  958.      *
  959.      * @param validating the validating flag
  960.      * @since 1.2
  961.      */
  962.     public void setValidating(final boolean validating) {
  963.         if (!schemaValidation) {
  964.             this.validating = validating;
  965.         }
  966.     }

  967.     /**
  968.      * Validate the document against the Schema.
  969.      *
  970.      * @throws ConfigurationException if the validation fails.
  971.      */
  972.     public void validate() throws ConfigurationException {
  973.         syncWrite(() -> {
  974.             try {
  975.                 final StringWriter writer = new StringWriter();
  976.                 final Result result = new StreamResult(writer);
  977.                 XMLDocumentHelper.transform(createTransformer(), new DOMSource(createDocument()), result);
  978.                 final Reader reader = new StringReader(writer.getBuffer().toString());
  979.                 createDocumentBuilder().parse(new InputSource(reader));
  980.             } catch (final SAXException | IOException | ParserConfigurationException pce) {
  981.                 throw new ConfigurationException("Validation failed", pce);
  982.             }
  983.         }, false);
  984.     }

  985.     /**
  986.      * Saves the configuration to the specified writer.
  987.      *
  988.      * @param writer the writer used to save the configuration
  989.      * @throws ConfigurationException if an error occurs
  990.      * @throws IOException if an IO error occurs
  991.      */
  992.     @Override
  993.     public void write(final Writer writer) throws ConfigurationException, IOException {
  994.         write(writer, createTransformer());
  995.     }

  996.     /**
  997.      * Saves the configuration to the specified writer.
  998.      *
  999.      * @param writer the writer used to save the configuration.
  1000.      * @param transformer How to transform this configuration.
  1001.      * @throws ConfigurationException if an error occurs.
  1002.      * @since 2.7.0
  1003.      */
  1004.     public void write(final Writer writer, final Transformer transformer) throws ConfigurationException {
  1005.         final Source source = new DOMSource(createDocument());
  1006.         final Result result = new StreamResult(writer);
  1007.         XMLDocumentHelper.transform(transformer, source, result);
  1008.     }
  1009. }