All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.configuration2.XMLConfiguration Maven / Gradle / Ivy

Go to download

Tools to assist in the reading of configuration/preferences files in various formats

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

package org.apache.commons.configuration2;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.configuration2.convert.ListDelimiterHandler;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.ConfigurationLogger;
import org.apache.commons.configuration2.io.FileLocator;
import org.apache.commons.configuration2.io.FileLocatorAware;
import org.apache.commons.configuration2.io.InputStreamSupport;
import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.NodeTreeWalker;
import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableObject;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * 

* A specialized hierarchical configuration class that is able to parse XML documents. *

*

* The parsed document will be stored keeping its structure. The class also tries to preserve as much information from * the loaded XML document as possible, including comments and processing instructions. These will be contained in * documents created by the {@code save()} methods, too. *

*

* Like other file based configuration classes this class maintains the name and path to the loaded configuration file. * These properties can be altered using several setter methods, but they are not modified by {@code save()} and * {@code load()} methods. If XML documents contain relative paths to other documents (e.g. to a DTD), these references * are resolved based on the path set for this configuration. *

*

* By inheriting from {@link AbstractConfiguration} this class provides some extended functionality, e.g. interpolation * of property values. Like in {@link PropertiesConfiguration} property values can contain delimiter characters (the * comma ',' per default) and are then split into multiple values. This works for XML attributes and text content of * elements as well. The delimiter can be escaped by a backslash. As an example consider the following XML fragment: *

* *
 * <config>
 *   <array>10,20,30,40</array>
 *   <scalar>3\,1415</scalar>
 *   <cite text="To be or not to be\, this is the question!"/>
 * </config>
 * 
* *

* Here the content of the {@code array} element will be split at the commas, so the {@code array} key will be assigned * 4 values. In the {@code scalar} property and the {@code text} attribute of the {@code cite} element the comma is * escaped, so that no splitting is performed. *

*

* The configuration API allows setting multiple values for a single attribute, e.g. something like the following is * legal (assuming that the default expression engine is used): *

* *
 * XMLConfiguration config = new XMLConfiguration();
 * config.addProperty("test.dir[@name]", "C:\\Temp\\");
 * config.addProperty("test.dir[@name]", "D:\\Data\\");
 * 
* *

* However, in XML such a constellation is not supported; an attribute can appear only once for a single element. * Therefore, an attempt to save a configuration which violates this condition will throw an exception. *

*

* Like other {@code Configuration} implementations, {@code XMLConfiguration} uses a {@link ListDelimiterHandler} object * for controlling list split operations. Per default, a list delimiter handler object is set which disables this * feature. XML has a built-in support for complex structures including list properties; therefore, list splitting is * not that relevant for this configuration type. Nevertheless, by setting an alternative {@code ListDelimiterHandler} * implementation, this feature can be enabled. It works as for any other concrete {@code Configuration} implementation. *

*

* Whitespace in the content of XML documents is trimmed per default. In most cases this is desired. However, sometimes * whitespace is indeed important and should be treated as part of the value of a property as in the following example: *

* *
 *   <indent>    </indent>
 * 
* *

* Per default the spaces in the {@code indent} element will be trimmed resulting in an empty element. To tell * {@code XMLConfiguration} that spaces are relevant the {@code xml:space} attribute can be used, which is defined in * the XML specification. This will look as follows: *

* *
 *   <indent xml:space="preserve">    </indent>
 * 
* *

* The value of the {@code indent} property will now contain the spaces. *

*

* {@code XMLConfiguration} implements the {@link FileBasedConfiguration} interface and thus can be used together with a * file-based builder to load XML configuration files from various sources like files, URLs, or streams. *

*

* Like other {@code Configuration} implementations, this class uses a {@code Synchronizer} object to control concurrent * access. By choosing a suitable implementation of the {@code Synchronizer} interface, an instance can be made * thread-safe or not. Note that access to most of the properties typically set through a builder is not protected by * the {@code Synchronizer}. The intended usage is that these properties are set once at construction time through the * builder and after that remain constant. If you wish to change such properties during life time of an instance, you * have to use the {@code lock()} and {@code unlock()} methods manually to ensure that other threads see your changes. *

*

* More information about the basic functionality supported by {@code XMLConfiguration} can be found at the user's guide * at Basic * features and AbstractConfiguration. There is also a separate chapter dealing with * XML Configurations in * special. *

* * @since 1.0 */ public class XMLConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware, InputStreamSupport { /** * A concrete {@code BuilderVisitor} that can construct XML documents. */ static class XMLBuilderVisitor extends BuilderVisitor { /** * Removes all attributes of the given element. * * @param elem the element */ private static void clearAttributes(final Element elem) { final NamedNodeMap attributes = elem.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { elem.removeAttribute(attributes.item(i).getNodeName()); } } /** * Returns the only text node of an element for update. This method is called when the element's text changes. Then all * text nodes except for the first are removed. A reference to the first is returned or null if there is no text * node at all. * * @param elem the element * @return the first and only text node */ private static Text findTextNodeForUpdate(final Element elem) { Text result = null; // Find all Text nodes final NodeList children = elem.getChildNodes(); final Collection textNodes = new ArrayList<>(); for (int i = 0; i < children.getLength(); i++) { final Node nd = children.item(i); if (nd instanceof Text) { if (result == null) { result = (Text) nd; } else { textNodes.add(nd); } } } // We don't want CDATAs if (result instanceof CDATASection) { textNodes.add(result); result = null; } // Remove all but the first Text node textNodes.forEach(elem::removeChild); return result; } /** * Helper method for updating the values of all attributes of the specified node. * * @param node the affected node * @param elem the element that is associated with this node */ private static void updateAttributes(final ImmutableNode node, final Element elem) { if (node != null && elem != null) { clearAttributes(elem); node.getAttributes().forEach((k, v) -> { if (v != null) { elem.setAttribute(k, v.toString()); } }); } } /** Stores the document to be constructed. */ private final Document document; /** The element mapping. */ private final Map elementMapping; /** A mapping for the references for new nodes. */ private final Map newElements; /** Stores the list delimiter handler . */ private final ListDelimiterHandler listDelimiterHandler; /** * Creates a new instance of {@code XMLBuilderVisitor}. * * @param docHelper the document helper * @param handler the delimiter handler for properties with multiple values */ public XMLBuilderVisitor(final XMLDocumentHelper docHelper, final ListDelimiterHandler handler) { document = docHelper.getDocument(); elementMapping = docHelper.getElementMapping(); listDelimiterHandler = handler; newElements = new HashMap<>(); } /** * Helper method for accessing the element of the specified node. * * @param node the node * @param refHandler the {@code ReferenceNodeHandler} * @return the element of this node */ private Element getElement(final ImmutableNode node, final ReferenceNodeHandler refHandler) { final Element elementNew = newElements.get(node); if (elementNew != null) { return elementNew; } // special treatment for root node of the hierarchy final Object reference = refHandler.getReference(node); final Node element; if (reference instanceof XMLDocumentHelper) { element = ((XMLDocumentHelper) reference).getDocument().getDocumentElement(); } else if (reference instanceof XMLListReference) { element = ((XMLListReference) reference).getElement(); } else { element = (Node) reference; } return element != null ? (Element) elementMapping.get(element) : document.getDocumentElement(); } /** * Updates the current XML document regarding removed nodes. The elements associated with removed nodes are removed from * the document. * * @param refHandler the {@code ReferenceNodeHandler} */ public void handleRemovedNodes(final ReferenceNodeHandler refHandler) { refHandler.removedReferences().stream().filter(Node.class::isInstance).forEach(ref -> removeReference(elementMapping.get(ref))); } /** * {@inheritDoc} This implementation ensures that the correct XML element is created and inserted between the given * siblings. */ @Override protected void insert(final ImmutableNode newNode, final ImmutableNode parent, final ImmutableNode sibling1, final ImmutableNode sibling2, final ReferenceNodeHandler refHandler) { if (XMLListReference.isListNode(newNode, refHandler)) { return; } final Element elem = document.createElement(newNode.getNodeName()); newElements.put(newNode, elem); updateAttributes(newNode, elem); if (newNode.getValue() != null) { final String txt = String.valueOf(listDelimiterHandler.escape(newNode.getValue(), ListDelimiterHandler.NOOP_TRANSFORMER)); elem.appendChild(document.createTextNode(txt)); } if (sibling2 == null) { getElement(parent, refHandler).appendChild(elem); } else if (sibling1 != null) { getElement(parent, refHandler).insertBefore(elem, getElement(sibling1, refHandler).getNextSibling()); } else { getElement(parent, refHandler).insertBefore(elem, getElement(parent, refHandler).getFirstChild()); } } /** * Processes the specified document, updates element values, and adds new nodes to the hierarchy. * * @param refHandler the {@code ReferenceNodeHandler} */ public void processDocument(final ReferenceNodeHandler refHandler) { updateAttributes(refHandler.getRootNode(), document.getDocumentElement()); NodeTreeWalker.INSTANCE.walkDFS(refHandler.getRootNode(), this, refHandler); } /** * Updates the associated XML elements when a node is removed. * * @param element the element to be removed */ private void removeReference(final Node element) { final Node parentElem = element.getParentNode(); if (parentElem != null) { parentElem.removeChild(element); } } /** * {@inheritDoc} This implementation determines the XML element associated with the given node. Then this element's * value and attributes are set accordingly. */ @Override protected void update(final ImmutableNode node, final Object reference, final ReferenceNodeHandler refHandler) { if (XMLListReference.isListNode(node, refHandler)) { if (XMLListReference.isFirstListItem(node, refHandler)) { final String value = XMLListReference.listValue(node, refHandler, listDelimiterHandler); updateElement(node, refHandler, value); } } else { final Object value = listDelimiterHandler.escape(refHandler.getValue(node), ListDelimiterHandler.NOOP_TRANSFORMER); updateElement(node, refHandler, value); } } /** * Updates the node's value if it represents an element node. * * @param element the element * @param value the new value */ private void updateElement(final Element element, final Object value) { Text txtNode = findTextNodeForUpdate(element); if (value == null) { // remove text if (txtNode != null) { element.removeChild(txtNode); } } else { final String newValue = String.valueOf(value); if (txtNode == null) { txtNode = document.createTextNode(newValue); if (element.getFirstChild() != null) { element.insertBefore(txtNode, element.getFirstChild()); } else { element.appendChild(txtNode); } } else { txtNode.setNodeValue(newValue); } } } private void updateElement(final ImmutableNode node, final ReferenceNodeHandler refHandler, final Object value) { final Element element = getElement(node, refHandler); updateElement(element, value); updateAttributes(node, element); } } /** Constant for the default indent size. */ static final int DEFAULT_INDENT_SIZE = 2; /** Constant for output property name used on a transformer to specify the indent amount. */ static final String INDENT_AMOUNT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount"; /** Constant for the default root element name. */ private static final String DEFAULT_ROOT_NAME = "configuration"; /** Constant for the name of the space attribute. */ private static final String ATTR_SPACE = "xml:space"; /** Constant for an internally used space attribute. */ private static final String ATTR_SPACE_INTERNAL = "config-xml:space"; /** Constant for the xml:space value for preserving whitespace. */ private static final String VALUE_PRESERVE = "preserve"; /** Schema Langauge key for the parser */ private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; /** Schema Language for the parser */ private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; /** * Determines the number of child elements of this given node with the specified node name. * * @param parent the parent node * @param name the name in question * @return the number of child elements with this name */ private static int countChildElements(final Node parent, final String name) { final NodeList childNodes = parent.getChildNodes(); int count = 0; for (int i = 0; i < childNodes.getLength(); i++) { final Node item = childNodes.item(i); if (item instanceof Element && name.equals(((Element) item).getTagName())) { count++; } } return count; } /** * Determines the value of a configuration node. This method mainly checks whether the text value is to be trimmed or * not. This is normally defined by the trim flag. However, if the node has children and its content is only whitespace, * then it makes no sense to store any value; this would only scramble layout when the configuration is saved again. * * @param content the text content of this node * @param hasChildren a flag whether the node has children * @param trimFlag the trim flag * @return the value to be stored for this node */ private static String determineValue(final String content, final boolean hasChildren, final boolean trimFlag) { final boolean shouldTrim = trimFlag || StringUtils.isBlank(content) && hasChildren; return shouldTrim ? content.trim() : content; } /** * Checks whether an element defines a complete list. If this is the case, extended list handling can be applied. * * @param element the element to be checked * @return a flag whether this is the only element defining the list */ private static boolean isSingleElementList(final Element element) { final Node parentNode = element.getParentNode(); return countChildElements(parentNode, element.getTagName()) == 1; } /** * Helper method for initializing the attributes of a configuration node from the given XML element. * * @param element the current XML element * @return a map with all attribute values extracted for the current node */ private static Map processAttributes(final Element element) { final NamedNodeMap attributes = element.getAttributes(); final Map attrmap = new HashMap<>(); for (int i = 0; i < attributes.getLength(); ++i) { final Node w3cNode = attributes.item(i); if (w3cNode instanceof Attr) { final Attr attr = (Attr) w3cNode; attrmap.put(attr.getName(), attr.getValue()); } } return attrmap; } /** * Checks whether the content of the current XML element should be trimmed. This method checks whether a * {@code xml:space} attribute is present and evaluates its value. See * http://www.w3.org/TR/REC-xml/#sec-white-space for more * details. * * @param element the current XML element * @param currentTrim the current trim flag * @return a flag whether the content of this element should be trimmed */ private static boolean shouldTrim(final Element element, final boolean currentTrim) { final Attr attr = element.getAttributeNode(ATTR_SPACE); if (attr == null) { return currentTrim; } return !VALUE_PRESERVE.equals(attr.getValue()); } /** Stores the name of the root element. */ private String rootElementName; /** Stores the public ID from the DOCTYPE. */ private String publicID; /** Stores the system ID from the DOCTYPE. */ private String systemID; /** Stores the document builder that should be used for loading. */ private DocumentBuilder documentBuilder; /** Stores a flag whether DTD or Schema validation should be performed. */ private boolean validating; /** Stores a flag whether DTD or Schema validation is used */ private boolean schemaValidation; /** The EntityResolver to use */ private EntityResolver entityResolver = new DefaultEntityResolver(); /** The current file locator. */ private FileLocator locator; /** * Creates a new instance of {@code XMLConfiguration}. */ public XMLConfiguration() { initLogger(new ConfigurationLogger(XMLConfiguration.class)); } /** * Creates a new instance of {@code XMLConfiguration} and copies the content of the passed in configuration into this * object. Note that only the data of the passed in configuration will be copied. If, for instance, the other * configuration is a {@code XMLConfiguration}, too, things like comments or processing instructions will be lost. * * @param c the configuration to copy * @since 1.4 */ public XMLConfiguration(final HierarchicalConfiguration c) { super(c); rootElementName = c != null ? c.getRootElementName() : null; initLogger(new ConfigurationLogger(XMLConfiguration.class)); } /** * Helper method for building the internal storage hierarchy. The XML elements are transformed into node objects. * * @param node a builder for the current node * @param refValue stores the text value of the element * @param element the current XML element * @param elemRefs a map for assigning references objects to nodes; can be null, then reference objects are * irrelevant * @param trim a flag whether the text content of elements should be trimmed; this controls the whitespace handling * @param level the current level in the hierarchy * @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 {@value #ATTR_SPACE} */ private Map constructHierarchy(final ImmutableNode.Builder node, final MutableObject refValue, final Element element, final Map elemRefs, final boolean trim, final int level) { final boolean trimFlag = shouldTrim(element, trim); final Map attributes = processAttributes(element); attributes.put(ATTR_SPACE_INTERNAL, String.valueOf(trimFlag)); final StringBuilder buffer = new StringBuilder(); final NodeList list = element.getChildNodes(); boolean hasChildren = false; for (int i = 0; i < list.getLength(); i++) { final Node w3cNode = list.item(i); if (w3cNode instanceof Element) { final Element child = (Element) w3cNode; final ImmutableNode.Builder childNode = new ImmutableNode.Builder(); childNode.name(child.getTagName()); final MutableObject refChildValue = new MutableObject<>(); final Map attrmap = constructHierarchy(childNode, refChildValue, child, elemRefs, trimFlag, level + 1); final boolean childTrim = Boolean.parseBoolean(attrmap.remove(ATTR_SPACE_INTERNAL)); childNode.addAttributes(attrmap); final ImmutableNode newChild = createChildNodeWithValue(node, childNode, child, refChildValue.getValue(), childTrim, attrmap, elemRefs); if (elemRefs != null && !elemRefs.containsKey(newChild)) { elemRefs.put(newChild, child); } hasChildren = true; } else if (w3cNode instanceof Text) { final Text data = (Text) w3cNode; buffer.append(data.getData()); } } boolean childrenFlag = false; if (hasChildren || trimFlag) { childrenFlag = hasChildren || attributes.size() > 1; } final String text = determineValue(buffer.toString(), childrenFlag, trimFlag); if (!text.isEmpty() || !childrenFlag && level != 0) { refValue.setValue(text); } return attributes; } /** * 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 child elements must be added. The return value is the first child node which * was added. * * @param parent the builder for the parent element * @param child the builder for the child element * @param elem the associated XML element * @param value the value of the child element * @param trim flag whether texts of elements should be trimmed * @param attrmap a map with the attributes of the current node * @param elemRefs a map for assigning references objects to nodes; can be null, then reference objects are * irrelevant * @return the first child node added to the parent */ private ImmutableNode createChildNodeWithValue(final ImmutableNode.Builder parent, final ImmutableNode.Builder child, final Element elem, final String value, final boolean trim, final Map attrmap, final Map elemRefs) { final ImmutableNode addedChildNode; final Collection values; if (value != null) { values = getListDelimiterHandler().split(value, trim); } else { values = Collections.emptyList(); } if (values.size() > 1) { final Map refs = isSingleElementList(elem) ? elemRefs : null; final Iterator it = values.iterator(); // Create new node for the original child's first value child.value(it.next()); addedChildNode = child.create(); parent.addChild(addedChildNode); XMLListReference.assignListReference(refs, addedChildNode, elem); // add multiple new children while (it.hasNext()) { final ImmutableNode.Builder c = new ImmutableNode.Builder(); c.name(addedChildNode.getNodeName()); c.value(it.next()); c.addAttributes(attrmap); final ImmutableNode newChild = c.create(); parent.addChild(newChild); XMLListReference.assignListReference(refs, newChild, null); } } else { if (values.size() == 1) { // we will have to replace the value because it might // contain escaped delimiters child.value(values.iterator().next()); } addedChildNode = child.create(); parent.addChild(addedChildNode); } return addedChildNode; } /** * Creates a DOM document from the internal tree of configuration nodes. * * @return the new document * @throws ConfigurationException if an error occurs */ private Document createDocument() throws ConfigurationException { final ReferenceNodeHandler handler = getReferenceHandler(); final XMLDocumentHelper docHelper = (XMLDocumentHelper) handler.getReference(handler.getRootNode()); final XMLDocumentHelper newHelper = docHelper == null ? XMLDocumentHelper.forNewDocument(getRootElementName()) : docHelper.createCopy(); final XMLBuilderVisitor builder = new XMLBuilderVisitor(newHelper, getListDelimiterHandler()); builder.handleRemovedNodes(handler); builder.processDocument(handler); initRootElementText(newHelper.getDocument(), getModel().getNodeHandler().getRootNode().getValue()); return newHelper.getDocument(); } /** * Creates the {@code DocumentBuilder} to be used for loading files. This implementation checks whether a specific * {@code DocumentBuilder} has been set. If this is the case, this one is used. Otherwise a default builder is created. * Depending on the value of the validating flag this builder will be a validating or a non validating * {@code DocumentBuilder}. * * @return the {@code DocumentBuilder} for loading configuration files * @throws ParserConfigurationException if an error occurs * @since 1.2 */ protected DocumentBuilder createDocumentBuilder() throws ParserConfigurationException { if (getDocumentBuilder() != null) { return getDocumentBuilder(); } final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); if (isValidating()) { factory.setValidating(true); if (isSchemaValidation()) { factory.setNamespaceAware(true); factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA); } } final DocumentBuilder result = factory.newDocumentBuilder(); result.setEntityResolver(this.entityResolver); if (isValidating()) { // register an error handler which detects validation errors result.setErrorHandler(new DefaultHandler() { @Override public void error(final SAXParseException ex) throws SAXException { throw ex; } }); } return result; } /** * Creates and initializes the transformer used for save operations. This base implementation initializes all of the * default settings like indentation mode and the DOCTYPE. Derived classes may overload this method if they have * specific needs. * * @return the transformer to use for a save operation * @throws ConfigurationException if an error occurs * @since 1.3 */ protected Transformer createTransformer() throws ConfigurationException { final Transformer transformer = XMLDocumentHelper.createTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, Integer.toString(DEFAULT_INDENT_SIZE)); if (locator != null && locator.getEncoding() != null) { transformer.setOutputProperty(OutputKeys.ENCODING, locator.getEncoding()); } if (publicID != null) { transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicID); } if (systemID != null) { transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemID); } return transformer; } /** * Gets the XML document this configuration was loaded from. The return value is null if this configuration * was not loaded from a XML document. * * @return the XML document this configuration was loaded from */ public Document getDocument() { final XMLDocumentHelper docHelper = getDocumentHelper(); return docHelper != null ? docHelper.getDocument() : null; } /** * Gets the {@code DocumentBuilder} object that is used for loading documents. If no specific builder has been set, * this method returns null. * * @return the {@code DocumentBuilder} for loading new documents * @since 1.2 */ public DocumentBuilder getDocumentBuilder() { return documentBuilder; } /** * Gets the helper object for managing the underlying document. * * @return the {@code XMLDocumentHelper} */ private XMLDocumentHelper getDocumentHelper() { final ReferenceNodeHandler handler = getReferenceHandler(); return (XMLDocumentHelper) handler.getReference(handler.getRootNode()); } /** * Gets the EntityResolver. * * @return The EntityResolver. * @since 1.7 */ public EntityResolver getEntityResolver() { return this.entityResolver; } /** * Gets the public ID of the DOCTYPE declaration from the loaded XML document. This is null if no document has * been loaded yet or if the document does not contain a DOCTYPE declaration with a public ID. * * @return the public ID * @since 1.3 */ public String getPublicID() { beginRead(false); try { return publicID; } finally { endRead(); } } /** * Gets the extended node handler with support for references. * * @return the {@code ReferenceNodeHandler} */ private ReferenceNodeHandler getReferenceHandler() { return getSubConfigurationParentModel().getReferenceNodeHandler(); } /** * Gets the name of the root element. If this configuration was loaded from a XML document, the name of this * document's root element is returned. Otherwise it is possible to set a name for the root element that will be used * when this configuration is stored. * * @return the name of the root element */ @Override protected String getRootElementNameInternal() { final Document doc = getDocument(); if (doc == null) { return rootElementName == null ? DEFAULT_ROOT_NAME : rootElementName; } return doc.getDocumentElement().getNodeName(); } /** * Gets the system ID of the DOCTYPE declaration from the loaded XML document. This is null if no document has * been loaded yet or if the document does not contain a DOCTYPE declaration with a system ID. * * @return the system ID * @since 1.3 */ public String getSystemID() { beginRead(false); try { return systemID; } finally { endRead(); } } /** * {@inheritDoc} Stores the passed in locator for the upcoming IO operation. */ @Override public void initFileLocator(final FileLocator loc) { locator = loc; } /** * Initializes this configuration from an XML document. * * @param docHelper the helper object with the document to be parsed * @param elemRefs a flag whether references to the XML elements should be set */ private void initProperties(final XMLDocumentHelper docHelper, final boolean elemRefs) { final Document document = docHelper.getDocument(); setPublicID(docHelper.getSourcePublicID()); setSystemID(docHelper.getSourceSystemID()); final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(); final MutableObject rootValue = new MutableObject<>(); final Map elemRefMap = elemRefs ? new HashMap<>() : null; final Map attributes = constructHierarchy(rootBuilder, rootValue, document.getDocumentElement(), elemRefMap, true, 0); attributes.remove(ATTR_SPACE_INTERNAL); final ImmutableNode top = rootBuilder.value(rootValue.getValue()).addAttributes(attributes).create(); getSubConfigurationParentModel().mergeRoot(top, document.getDocumentElement().getTagName(), elemRefMap, elemRefs ? docHelper : null, this); } /** * Sets the text of the root element of a newly created XML Document. * * @param doc the document * @param value the new text to be set */ private void initRootElementText(final Document doc, final Object value) { final Element elem = doc.getDocumentElement(); final NodeList children = elem.getChildNodes(); // Remove all existing text nodes for (int i = 0; i < children.getLength(); i++) { final Node nd = children.item(i); if (nd.getNodeType() == Node.TEXT_NODE) { elem.removeChild(nd); } } if (value != null) { // Add a new text node elem.appendChild(doc.createTextNode(String.valueOf(value))); } } /** * Returns the value of the schemaValidation flag. * * @return the schemaValidation flag * @since 1.7 */ public boolean isSchemaValidation() { return schemaValidation; } /** * Returns the value of the validating flag. * * @return the validating flag * @since 1.2 */ public boolean isValidating() { return validating; } /** * Loads a configuration file from the specified input source. * * @param source the input source * @throws ConfigurationException if an error occurs */ private void load(final InputSource source) throws ConfigurationException { if (locator == null) { throw new ConfigurationException( "Load operation not properly " + "initialized! Do not call read(InputStream) directly," + " but use a FileHandler to load a configuration."); } try { final URL sourceURL = locator.getSourceURL(); if (sourceURL != null) { source.setSystemId(sourceURL.toString()); } final DocumentBuilder builder = createDocumentBuilder(); final Document newDocument = builder.parse(source); final Document oldDocument = getDocument(); initProperties(XMLDocumentHelper.forSourceDocument(newDocument), oldDocument == null); } catch (final SAXParseException spe) { throw new ConfigurationException("Error parsing " + source.getSystemId(), spe); } catch (final Exception e) { getLogger().debug("Unable to load the configuration: " + e); throw new ConfigurationException("Unable to load the configuration", e); } } /** * Loads the configuration from the given input stream. This is analogous to {@link #read(Reader)}, but data is read * from a stream. Note that this method will be called most time when reading an XML configuration source. By reading * XML documents directly from an input stream, the file's encoding can be correctly dealt with. * * @param in the input stream * @throws ConfigurationException if an error occurs * @throws IOException if an IO error occurs */ @Override public void read(final InputStream in) throws ConfigurationException, IOException { load(new InputSource(in)); } /** * Loads the configuration from the given reader. Note that the {@code clear()} method is not called, so the properties * contained in the loaded file will be added to the current set of properties. * * @param in the reader * @throws ConfigurationException if an error occurs * @throws IOException if an IO error occurs */ @Override public void read(final Reader in) throws ConfigurationException, IOException { load(new InputSource(in)); } /** * Sets the {@code DocumentBuilder} object to be used for loading documents. This method makes it possible to specify * the exact document builder. So an application can create a builder, configure it for its special needs, and then pass * it to this method. * * @param documentBuilder the document builder to be used; if undefined, a default builder will be used * @since 1.2 */ public void setDocumentBuilder(final DocumentBuilder documentBuilder) { this.documentBuilder = documentBuilder; } /** * Sets a new EntityResolver. Setting this will cause RegisterEntityId to have no effect. * * @param resolver The EntityResolver to use. * @since 1.7 */ public void setEntityResolver(final EntityResolver resolver) { this.entityResolver = resolver; } /** * Sets the public ID of the DOCTYPE declaration. When this configuration is saved, a DOCTYPE declaration will be * constructed that contains this public ID. * * @param publicID the public ID * @since 1.3 */ public void setPublicID(final String publicID) { beginWrite(false); try { this.publicID = publicID; } finally { endWrite(); } } /** * Sets the name of the root element. This name is used when this configuration object is stored in an XML file. Note * that setting the name of the root element works only if this configuration has been newly created. If the * configuration was loaded from an XML file, the name cannot be changed and an {@code UnsupportedOperationException} * exception is thrown. Whether this configuration has been loaded from an XML document or not can be found out using * the {@code getDocument()} method. * * @param name the name of the root element */ public void setRootElementName(final String name) { beginRead(true); try { if (getDocument() != null) { throw new UnsupportedOperationException("The name of the root element " + "cannot be changed when loaded from an XML document!"); } rootElementName = name; } finally { endRead(); } } /** * Sets the value of the schemaValidation flag. This flag determines whether DTD or Schema validation should be used. * This flag is evaluated only if no custom {@code DocumentBuilder} was set. If set to true the XML document must * contain a schemaLocation definition that provides resolvable hints to the required schemas. * * @param schemaValidation the validating flag * @since 1.7 */ public void setSchemaValidation(final boolean schemaValidation) { this.schemaValidation = schemaValidation; if (schemaValidation) { this.validating = true; } } /** * Sets the system ID of the DOCTYPE declaration. When this configuration is saved, a DOCTYPE declaration will be * constructed that contains this system ID. * * @param systemID the system ID * @since 1.3 */ public void setSystemID(final String systemID) { beginWrite(false); try { this.systemID = systemID; } finally { endWrite(); } } /** * Sets the value of the validating flag. This flag determines whether DTD/Schema validation should be performed when * loading XML documents. This flag is evaluated only if no custom {@code DocumentBuilder} was set. * * @param validating the validating flag * @since 1.2 */ public void setValidating(final boolean validating) { if (!schemaValidation) { this.validating = validating; } } /** * Validate the document against the Schema. * * @throws ConfigurationException if the validation fails. */ public void validate() throws ConfigurationException { beginWrite(false); try { final Transformer transformer = createTransformer(); final Source source = new DOMSource(createDocument()); final StringWriter writer = new StringWriter(); final Result result = new StreamResult(writer); XMLDocumentHelper.transform(transformer, source, result); final Reader reader = new StringReader(writer.getBuffer().toString()); final DocumentBuilder builder = createDocumentBuilder(); builder.parse(new InputSource(reader)); } catch (final SAXException | IOException | ParserConfigurationException pce) { throw new ConfigurationException("Validation failed", pce); } finally { endWrite(); } } /** * Saves the configuration to the specified writer. * * @param writer the writer used to save the configuration * @throws ConfigurationException if an error occurs * @throws IOException if an IO error occurs */ @Override public void write(final Writer writer) throws ConfigurationException, IOException { write(writer, createTransformer()); } /** * Saves the configuration to the specified writer. * * @param writer the writer used to save the configuration. * @param transformer How to transform this configuration. * @throws ConfigurationException if an error occurs. * @since 2.7.0 */ public void write(final Writer writer, final Transformer transformer) throws ConfigurationException { final Source source = new DOMSource(createDocument()); final Result result = new StreamResult(writer); XMLDocumentHelper.transform(transformer, source, result); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy