org.apache.commons.configuration.XMLConfiguration Maven / Gradle / Ivy
/*
* 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.configuration;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
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.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.configuration.tree.ConfigurationNode;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
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 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 save()
* and 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 array
element will be split at
* the commas, so the array
key will be assigned 4 values. In the
* scalar
property and the text
attribute of the
* 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\\");
*
*
* Because in XML such a constellation is not directly supported (an attribute
* can appear only once for a single element), the values are concatenated to a
* single value. If delimiter parsing is enabled (refer to the
* {@link #setDelimiterParsingDisabled(boolean)}
method), the
* current list delimiter character will be used as separator. Otherwise the
* pipe symbol ("|") will be used for this purpose. No matter which character is
* used as delimiter, it can always be escaped with a backslash. A backslash
* itself can also be escaped with another backslash. Consider the following
* example fragment from a configuration file:
*
* <directories names="C:\Temp\\|D:\Data\"/>
*
* Here the backslash after Temp is escaped. This is necessary because it
* would escape the list delimiter (the pipe symbol assuming that list delimiter
* parsing is disabled) otherwise. So this attribute would have two values.
*
* Note: You should ensure that the delimiter parsing disabled
* property is always consistent when you load and save a configuration file.
* Otherwise the values of properties can become corrupted.
*
* 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 indent
element will be trimmed
* resulting in an empty element. To tell XMLConfiguration
that
* spaces are relevant the 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 indent
property will now contain the spaces.
*
* XMLConfiguration
implements the {@link FileConfiguration}
* interface and thus provides full support for loading XML documents from
* different sources like files, URLs, or streams. A full description of these
* features can be found in the documentation of
* {@link AbstractFileConfiguration}
.
*
* Note:Configuration objects of this type can be read concurrently
* by multiple threads. However if one of these threads modifies the object,
* synchronization has to be performed manually.
*
* @since commons-configuration 1.0
*
* @author Jörg Schaible
* @author Oliver Heger
* @version $Revision: 721895 $, $Date: 2008-11-30 22:08:42 +0100 (So, 30 Nov 2008) $
*/
public class XMLConfiguration extends AbstractHierarchicalFileConfiguration
implements EntityResolver
{
/**
* The serial version UID.
*/
private static final long serialVersionUID = 2453781111653383552L;
/** 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 the xml:space value for preserving whitespace.*/
private static final String VALUE_PRESERVE = "preserve";
/** Constant for the delimiter for multiple attribute values.*/
private static final char ATTR_VALUE_DELIMITER = '|';
/** The document from this configuration's data source. */
private Document document;
/** Stores a map with the registered public IDs.*/
private Map registeredEntities = new HashMap();
/** 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 validation should be performed.*/
private boolean validating;
/** A flag whether attribute splitting is disabled.*/
private boolean attributeSplittingDisabled;
/**
* Creates a new instance of XMLConfiguration
.
*/
public XMLConfiguration()
{
super();
}
/**
* Creates a new instance of 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 XMLConfiguration
, too,
* things like comments or processing instructions will be lost.
*
* @param c the configuration to copy
* @since 1.4
*/
public XMLConfiguration(HierarchicalConfiguration c)
{
super(c);
clearReferences(getRootNode());
setRootElementName(getRootNode().getName());
}
/**
* Creates a new instance of XMLConfiguration
. The
* configuration is loaded from the specified file
*
* @param fileName the name of the file to load
* @throws ConfigurationException if the file cannot be loaded
*/
public XMLConfiguration(String fileName) throws ConfigurationException
{
super(fileName);
}
/**
* Creates a new instance of XMLConfiguration
.
* The configuration is loaded from the specified file.
*
* @param file the file
* @throws ConfigurationException if an error occurs while loading the file
*/
public XMLConfiguration(File file) throws ConfigurationException
{
super(file);
}
/**
* Creates a new instance of XMLConfiguration
.
* The configuration is loaded from the specified URL.
*
* @param url the URL
* @throws ConfigurationException if loading causes an error
*/
public XMLConfiguration(URL url) throws ConfigurationException
{
super(url);
}
/**
* Returns 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
*/
public String getRootElementName()
{
if (getDocument() == null)
{
return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
}
else
{
return getDocument().getDocumentElement().getNodeName();
}
}
/**
* 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 UnsupportedOperationException
* exception is thrown. Whether this configuration has been loaded from an
* XML document or not can be found out using the getDocument()
* method.
*
* @param name the name of the root element
*/
public void setRootElementName(String name)
{
if (getDocument() != null)
{
throw new UnsupportedOperationException("The name of the root element "
+ "cannot be changed when loaded from an XML document!");
}
rootElementName = name;
getRootNode().setName(name);
}
/**
* Returns the DocumentBuilder
object that is used for
* loading documents. If no specific builder has been set, this method
* returns null.
*
* @return the DocumentBuilder
for loading new documents
* @since 1.2
*/
public DocumentBuilder getDocumentBuilder()
{
return documentBuilder;
}
/**
* Sets the 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(DocumentBuilder documentBuilder)
{
this.documentBuilder = documentBuilder;
}
/**
* Returns 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()
{
return publicID;
}
/**
* 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(String publicID)
{
this.publicID = publicID;
}
/**
* Returns 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()
{
return systemID;
}
/**
* 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(String systemID)
{
this.systemID = systemID;
}
/**
* Returns the value of the validating flag.
*
* @return the validating flag
* @since 1.2
*/
public boolean isValidating()
{
return validating;
}
/**
* Sets the value of the validating flag. This flag determines whether
* DTD validation should be performed when loading XML documents. This
* flag is evaluated only if no custom DocumentBuilder
was set.
*
* @param validating the validating flag
* @since 1.2
*/
public void setValidating(boolean validating)
{
this.validating = validating;
}
/**
* Returns the flag whether attribute splitting is disabled.
*
* @return the flag whether attribute splitting is disabled
* @see #setAttributeSplittingDisabled(boolean)
* @since 1.6
*/
public boolean isAttributeSplittingDisabled()
{
return attributeSplittingDisabled;
}
/**
*
* Sets a flag whether attribute splitting is disabled.
*
*
* The Configuration API allows adding multiple values to an attribute. This
* is problematic when storing the configuration because in XML an attribute
* can appear only once with a single value. To solve this problem, per
* default multiple attribute values are concatenated using a special
* separator character and split again when the configuration is loaded. The
* separator character is either the list delimiter character (see
* {@link #setListDelimiter(char)}) or the pipe symbol ("|") if
* list delimiter parsing is disabled.
*
*
* In some constellations the splitting of attribute values can have
* undesired effects, especially if list delimiter parsing is disabled and
* attributes may contain the "|" character. In these cases it is
* possible to disable the attribute splitting mechanism by calling this
* method with a boolean value set to false. If attribute splitting
* is disabled, the values of attributes will not be processed, but stored
* as configuration properties exactly as they are returned by the XML
* parser.
*
*
* Note that in this mode multiple attribute values cannot be handled
* correctly. It is possible to create a XMLConfiguration
* object, add multiple values to an attribute and save it. When the
* configuration is loaded again and attribute splitting is disabled, the
* attribute will only have a single value, which is the concatenation of
* all values set before. So it lies in the responsibility of the
* application to carefully set the values of attributes.
*
*
* As is true for the {@link #setDelimiterParsingDisabled(boolean)} method,
* this method must be called before the configuration is loaded. So it
* can't be used together with one of the constructors expecting the
* specification of the file to load. Instead the default constructor has to
* be used, then setAttributeSplittingDisabled(false)
has to be
* called, and finally the configuration can be loaded using one of its
* load()
methods.
*
*
* @param attributeSplittingDisabled true for disabling attribute
* splitting, false for enabling it
* @see #setDelimiterParsingDisabled(boolean)
* @since 1.6
*/
public void setAttributeSplittingDisabled(boolean attributeSplittingDisabled)
{
this.attributeSplittingDisabled = attributeSplittingDisabled;
}
/**
* Returns 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()
{
return document;
}
/**
* Removes all properties from this configuration. If this configuration
* was loaded from a file, the associated DOM document is also cleared.
*/
public void clear()
{
super.clear();
document = null;
}
/**
* Initializes this configuration from an XML document.
*
* @param document the document to be parsed
* @param elemRefs a flag whether references to the XML elements should be set
*/
public void initProperties(Document document, boolean elemRefs)
{
if (document.getDoctype() != null)
{
setPublicID(document.getDoctype().getPublicId());
setSystemID(document.getDoctype().getSystemId());
}
constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs, true);
getRootNode().setName(document.getDocumentElement().getNodeName());
if (elemRefs)
{
getRoot().setReference(document.getDocumentElement());
}
}
/**
* Helper method for building the internal storage hierarchy. The XML
* elements are transformed into node objects.
*
* @param node the actual node
* @param element the actual XML element
* @param elemRefs a flag whether references to the XML elements should be set
* @param trim a flag whether the text content of elements should be trimmed;
* this controls the whitespace handling
*/
private void constructHierarchy(Node node, Element element, boolean elemRefs, boolean trim)
{
boolean trimFlag = shouldTrim(element, trim);
processAttributes(node, element, elemRefs);
StringBuffer buffer = new StringBuffer();
NodeList list = element.getChildNodes();
for (int i = 0; i < list.getLength(); i++)
{
org.w3c.dom.Node w3cNode = list.item(i);
if (w3cNode instanceof Element)
{
Element child = (Element) w3cNode;
Node childNode = new XMLNode(child.getTagName(),
elemRefs ? child : null);
constructHierarchy(childNode, child, elemRefs, trimFlag);
node.addChild(childNode);
handleDelimiters(node, childNode, trimFlag);
}
else if (w3cNode instanceof Text)
{
Text data = (Text) w3cNode;
buffer.append(data.getData());
}
}
String text = buffer.toString();
if (trimFlag)
{
text = text.trim();
}
if (text.length() > 0 || !node.hasChildren())
{
node.setValue(text);
}
}
/**
* Helper method for constructing node objects for the attributes of the
* given XML element.
*
* @param node the actual node
* @param element the actual XML element
* @param elemRefs a flag whether references to the XML elements should be set
*/
private void processAttributes(Node node, Element element, boolean elemRefs)
{
NamedNodeMap attributes = element.getAttributes();
for (int i = 0; i < attributes.getLength(); ++i)
{
org.w3c.dom.Node w3cNode = attributes.item(i);
if (w3cNode instanceof Attr)
{
Attr attr = (Attr) w3cNode;
List values;
if (isAttributeSplittingDisabled())
{
values = Collections.singletonList(attr.getValue());
}
else
{
values = PropertyConverter.split(attr.getValue(),
isDelimiterParsingDisabled() ? ATTR_VALUE_DELIMITER
: getListDelimiter());
}
for (Iterator it = values.iterator(); it.hasNext();)
{
Node child = new XMLNode(attr.getName(), elemRefs ? element
: null);
child.setValue(it.next());
node.addAttribute(child);
}
}
}
}
/**
* Deals with elements whose value is a list. In this case multiple child
* elements must be added.
*
* @param parent the parent element
* @param child the child element
* @param trim flag whether texts of elements should be trimmed
*/
private void handleDelimiters(Node parent, Node child, boolean trim)
{
if (child.getValue() != null)
{
List values;
if (isDelimiterParsingDisabled())
{
values = new ArrayList();
values.add(child.getValue().toString());
}
else
{
values = PropertyConverter.split(child.getValue().toString(),
getListDelimiter(), trim);
}
if (values.size() > 1)
{
Iterator it = values.iterator();
// Create new node for the original child's first value
Node c = createNode(child.getName());
c.setValue(it.next());
// Copy original attributes to the new node
for (Iterator itAttrs = child.getAttributes().iterator(); itAttrs
.hasNext();)
{
Node ndAttr = (Node) itAttrs.next();
ndAttr.setReference(null);
c.addAttribute(ndAttr);
}
parent.remove(child);
parent.addChild(c);
// add multiple new children
while (it.hasNext())
{
c = new XMLNode(child.getName(), null);
c.setValue(it.next());
parent.addChild(c);
}
}
else if (values.size() == 1)
{
// we will have to replace the value because it might
// contain escaped delimiters
child.setValue(values.get(0));
}
}
}
/**
* Checks whether the content of the current XML element should be trimmed.
* This method checks whether a 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 boolean shouldTrim(Element element, boolean currentTrim)
{
Attr attr = element.getAttributeNode(ATTR_SPACE);
if (attr == null)
{
return currentTrim;
}
else
{
return !VALUE_PRESERVE.equals(attr.getValue());
}
}
/**
* Creates the DocumentBuilder
to be used for loading files.
* This implementation checks whether a specific
* 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 DocumentBuilder
.
*
* @return the DocumentBuilder
for loading configuration
* files
* @throws ParserConfigurationException if an error occurs
* @since 1.2
*/
protected DocumentBuilder createDocumentBuilder()
throws ParserConfigurationException
{
if (getDocumentBuilder() != null)
{
return getDocumentBuilder();
}
else
{
DocumentBuilderFactory factory = DocumentBuilderFactory
.newInstance();
factory.setValidating(isValidating());
DocumentBuilder result = factory.newDocumentBuilder();
result.setEntityResolver(this);
if (isValidating())
{
// register an error handler which detects validation errors
result.setErrorHandler(new DefaultHandler()
{
public void error(SAXParseException ex) throws SAXException
{
throw ex;
}
});
}
return result;
}
}
/**
* Creates a DOM document from the internal tree of configuration nodes.
*
* @return the new document
* @throws ConfigurationException if an error occurs
*/
protected Document createDocument() throws ConfigurationException
{
try
{
if (document == null)
{
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document newDocument = builder.newDocument();
Element rootElem = newDocument.createElement(getRootElementName());
newDocument.appendChild(rootElem);
document = newDocument;
}
XMLBuilderVisitor builder = new XMLBuilderVisitor(document,
isDelimiterParsingDisabled() ? (char) 0 : getListDelimiter());
builder.processDocument(getRoot());
initRootElementText(document, getRootNode().getValue());
return document;
}
catch (DOMException domEx)
{
throw new ConfigurationException(domEx);
}
catch (ParserConfigurationException pex)
{
throw new ConfigurationException(pex);
}
}
/**
* 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(Document doc, Object value)
{
Element elem = doc.getDocumentElement();
NodeList children = elem.getChildNodes();
// Remove all existing text nodes
for (int i = 0; i < children.getLength(); i++)
{
org.w3c.dom.Node nd = children.item(i);
if (nd.getNodeType() == org.w3c.dom.Node.TEXT_NODE)
{
elem.removeChild(nd);
}
}
if (value != null)
{
// Add a new text node
elem.appendChild(doc.createTextNode(String.valueOf(value)));
}
}
/**
* Creates a new node object. This implementation returns an instance of the
* XMLNode
class.
*
* @param name the node's name
* @return the new node
*/
protected Node createNode(String name)
{
return new XMLNode(name, null);
}
/**
* Loads the configuration from the given input stream.
*
* @param in the input stream
* @throws ConfigurationException if an error occurs
*/
public void load(InputStream in) throws ConfigurationException
{
load(new InputSource(in));
}
/**
* Load the configuration from the given reader.
* Note that the clear()
method is not called, so
* the properties contained in the loaded file will be added to the
* actual set of properties.
*
* @param in An InputStream.
*
* @throws ConfigurationException if an error occurs
*/
public void load(Reader in) throws ConfigurationException
{
load(new InputSource(in));
}
/**
* Loads a configuration file from the specified input source.
* @param source the input source
* @throws ConfigurationException if an error occurs
*/
private void load(InputSource source) throws ConfigurationException
{
try
{
URL sourceURL = getDelegate().getURL();
if (sourceURL != null)
{
source.setSystemId(sourceURL.toString());
}
DocumentBuilder builder = createDocumentBuilder();
Document newDocument = builder.parse(source);
Document oldDocument = document;
document = null;
initProperties(newDocument, oldDocument == null);
document = (oldDocument == null) ? newDocument : oldDocument;
}
catch (Exception e)
{
throw new ConfigurationException("Unable to load the configuration", e);
}
}
/**
* Saves the configuration to the specified writer.
*
* @param writer the writer used to save the configuration
* @throws ConfigurationException if an error occurs
*/
public void save(Writer writer) throws ConfigurationException
{
try
{
Transformer transformer = createTransformer();
Source source = new DOMSource(createDocument());
Result result = new StreamResult(writer);
transformer.transform(source, result);
}
catch (TransformerException e)
{
throw new ConfigurationException("Unable to save the configuration", e);
}
catch (TransformerFactoryConfigurationError e)
{
throw new ConfigurationException("Unable to save the configuration", e);
}
}
/**
* Creates and initializes the transformer used for save operations. This
* base implementation initializes all of the default settings like
* indention 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 TransformerException if an error occurs
* @since 1.3
*/
protected Transformer createTransformer() throws TransformerException
{
Transformer transformer = TransformerFactory.newInstance()
.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
if (getEncoding() != null)
{
transformer.setOutputProperty(OutputKeys.ENCODING, getEncoding());
}
if (getPublicID() != null)
{
transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
getPublicID());
}
if (getSystemID() != null)
{
transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
getSystemID());
}
return transformer;
}
/**
* Creates a copy of this object. The new configuration object will contain
* the same properties as the original, but it will lose any connection to a
* source document (if one exists). This is to avoid race conditions if both
* the original and the copy are modified and then saved.
*
* @return the copy
*/
public Object clone()
{
XMLConfiguration copy = (XMLConfiguration) super.clone();
// clear document related properties
copy.document = null;
copy.setDelegate(copy.createDelegate());
// clear all references in the nodes, too
clearReferences(copy.getRootNode());
return copy;
}
/**
* Creates the file configuration delegate for this object. This implementation
* will return an instance of a class derived from FileConfigurationDelegate
* that deals with some specialities of XMLConfiguration
.
* @return the delegate for this object
*/
protected FileConfigurationDelegate createDelegate()
{
return new XMLFileConfigurationDelegate();
}
/**
* Adds a collection of nodes directly to this configuration. This
* implementation ensures that the nodes to be added are of the correct node
* type (they have to be converted to XMLNode
if necessary).
*
* @param key the key where the nodes are to be added
* @param nodes the collection with the new nodes
* @since 1.5
*/
public void addNodes(String key, Collection nodes)
{
Collection xmlNodes;
if (nodes != null && !nodes.isEmpty())
{
xmlNodes = new ArrayList(nodes.size());
for (Iterator it = nodes.iterator(); it.hasNext();)
{
xmlNodes.add(convertToXMLNode((ConfigurationNode) it.next()));
}
}
else
{
xmlNodes = nodes;
}
super.addNodes(key, xmlNodes);
}
/**
* Converts the specified node into a XMLNode
if necessary.
* This is required for nodes that are directly added, e.g. by
* addNodes()
. If the passed in node is already an instance
* of XMLNode
, it is directly returned, and conversion
* stops. Otherwise a new XMLNode
is created, and the
* children are also converted.
*
* @param node the node to be converted
* @return the converted node
*/
private XMLNode convertToXMLNode(ConfigurationNode node)
{
if (node instanceof XMLNode)
{
return (XMLNode) node;
}
XMLNode nd = (XMLNode) createNode(node.getName());
nd.setValue(node.getValue());
nd.setAttribute(node.isAttribute());
for (Iterator it = node.getChildren().iterator(); it.hasNext();)
{
nd.addChild(convertToXMLNode((ConfigurationNode) it.next()));
}
for (Iterator it = node.getAttributes().iterator(); it.hasNext();)
{
nd.addAttribute(convertToXMLNode((ConfigurationNode) it.next()));
}
return nd;
}
/**
*
* Registers the specified DTD URL for the specified public identifier.
*
*
* XMLConfiguration
contains an internal
* EntityResolver
implementation. This maps
* PUBLICID
's to URLs (from which the resource will be
* loaded). A common use case for this method is to register local URLs
* (possibly computed at runtime by a class loader) for DTDs. This allows
* the performance advantage of using a local version without having to
* ensure every SYSTEM
URI on every processed XML document is
* local. This implementation provides only basic functionality. If more
* sophisticated features are required, using
* {@link #setDocumentBuilder(DocumentBuilder)} to set a custom
* DocumentBuilder
(which also can be initialized with a
* custom EntityResolver
) is recommended.
*
*
* Note: This method will have no effect when a custom
* DocumentBuilder
has been set. (Setting a custom
* DocumentBuilder
overrides the internal implementation.)
*
*
* Note: This method must be called before the
* configuration is loaded. So the default constructor of
* XMLConfiguration
should be used, the location of the
* configuration file set, registerEntityId()
called, and
* finally the load()
method can be invoked.
*
*
* @param publicId Public identifier of the DTD to be resolved
* @param entityURL The URL to use for reading this DTD
* @throws IllegalArgumentException if the public ID is undefined
* @since 1.5
*/
public void registerEntityId(String publicId, URL entityURL)
{
if (publicId == null)
{
throw new IllegalArgumentException("Public ID must not be null!");
}
getRegisteredEntities().put(publicId, entityURL);
}
/**
* Resolves the requested external entity. This is the default
* implementation of the EntityResolver
interface. It checks
* the passed in public ID against the registered entity IDs and uses a
* local URL if possible.
*
* @param publicId the public identifier of the entity being referenced
* @param systemId the system identifier of the entity being referenced
* @return an input source for the specified entity
* @throws SAXException if a parsing exception occurs
* @since 1.5
*/
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException
{
// Has this system identifier been registered?
URL entityURL = null;
if (publicId != null)
{
entityURL = (URL) getRegisteredEntities().get(publicId);
}
if (entityURL != null)
{
// Obtain an InputSource for this URL. This code is based on the
// createInputSourceFromURL() method of Commons Digester.
try
{
URLConnection connection = entityURL.openConnection();
connection.setUseCaches(false);
InputStream stream = connection.getInputStream();
InputSource source = new InputSource(stream);
source.setSystemId(entityURL.toExternalForm());
return source;
}
catch (IOException e)
{
throw new SAXException(e);
}
}
else
{
// default processing behavior
return null;
}
}
/**
* Returns a map with the entity IDs that have been registered using the
* registerEntityId()
method.
*
* @return a map with the registered entity IDs
*/
Map getRegisteredEntities()
{
return registeredEntities;
}
/**
* A specialized Node
class that is connected with an XML
* element. Changes on a node are also performed on the associated element.
*/
class XMLNode extends Node
{
/**
* The serial version UID.
*/
private static final long serialVersionUID = -4133988932174596562L;
/**
* Creates a new instance of XMLNode
and initializes it
* with a name and the corresponding XML element.
*
* @param name the node's name
* @param elem the XML element
*/
public XMLNode(String name, Element elem)
{
super(name);
setReference(elem);
}
/**
* Sets the value of this node. If this node is associated with an XML
* element, this element will be updated, too.
*
* @param value the node's new value
*/
public void setValue(Object value)
{
super.setValue(value);
if (getReference() != null && document != null)
{
if (isAttribute())
{
updateAttribute();
}
else
{
updateElement(value);
}
}
}
/**
* Updates the associated XML elements when a node is removed.
*/
protected void removeReference()
{
if (getReference() != null)
{
Element element = (Element) getReference();
if (isAttribute())
{
updateAttribute();
}
else
{
org.w3c.dom.Node parentElem = element.getParentNode();
if (parentElem != null)
{
parentElem.removeChild(element);
}
}
}
}
/**
* Updates the node's value if it represents an element node.
*
* @param value the new value
*/
private void updateElement(Object value)
{
Text txtNode = findTextNodeForUpdate();
if (value == null)
{
// remove text
if (txtNode != null)
{
((Element) getReference()).removeChild(txtNode);
}
}
else
{
if (txtNode == null)
{
txtNode = document
.createTextNode(PropertyConverter.escapeDelimiters(
value.toString(), getListDelimiter()));
if (((Element) getReference()).getFirstChild() != null)
{
((Element) getReference()).insertBefore(txtNode,
((Element) getReference()).getFirstChild());
}
else
{
((Element) getReference()).appendChild(txtNode);
}
}
else
{
txtNode.setNodeValue(PropertyConverter.escapeDelimiters(
value.toString(), getListDelimiter()));
}
}
}
/**
* Updates the node's value if it represents an attribute.
*
*/
private void updateAttribute()
{
XMLBuilderVisitor.updateAttribute(getParent(), getName(), getListDelimiter());
}
/**
* Returns the only text node of this 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.
*
* @return the first and only text node
*/
private Text findTextNodeForUpdate()
{
Text result = null;
Element elem = (Element) getReference();
// Find all Text nodes
NodeList children = elem.getChildNodes();
Collection textNodes = new ArrayList();
for (int i = 0; i < children.getLength(); i++)
{
org.w3c.dom.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
for (Iterator it = textNodes.iterator(); it.hasNext();)
{
elem.removeChild((org.w3c.dom.Node) it.next());
}
return result;
}
}
/**
* A concrete BuilderVisitor
that can construct XML
* documents.
*/
static class XMLBuilderVisitor extends BuilderVisitor
{
/** Stores the document to be constructed. */
private Document document;
/** Stores the list delimiter.*/
private char listDelimiter = AbstractConfiguration.
getDefaultListDelimiter();
/**
* Creates a new instance of XMLBuilderVisitor
*
* @param doc the document to be created
* @param listDelimiter the delimiter for attribute properties with multiple values
*/
public XMLBuilderVisitor(Document doc, char listDelimiter)
{
document = doc;
this.listDelimiter = listDelimiter;
}
/**
* Processes the node hierarchy and adds new nodes to the document.
*
* @param rootNode the root node
*/
public void processDocument(Node rootNode)
{
rootNode.visit(this, null);
}
/**
* Inserts a new node. This implementation ensures that the correct
* XML element is created and inserted between the given siblings.
*
* @param newNode the node to insert
* @param parent the parent node
* @param sibling1 the first sibling
* @param sibling2 the second sibling
* @return the new node
*/
protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
{
if (newNode.isAttribute())
{
updateAttribute(parent, getElement(parent), newNode.getName(), listDelimiter);
return null;
}
else
{
Element elem = document.createElement(newNode.getName());
if (newNode.getValue() != null)
{
String txt = newNode.getValue().toString();
if (listDelimiter != 0)
{
txt = PropertyConverter.escapeDelimiters(txt, listDelimiter);
}
elem.appendChild(document.createTextNode(txt));
}
if (sibling2 == null)
{
getElement(parent).appendChild(elem);
}
else if (sibling1 != null)
{
getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
}
else
{
getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
}
return elem;
}
}
/**
* Helper method for updating the value of the specified node's
* attribute with the given name.
*
* @param node the affected node
* @param elem the element that is associated with this node
* @param name the name of the affected attribute
* @param listDelimiter the delimiter for attributes with multiple values
*/
private static void updateAttribute(Node node, Element elem, String name, char listDelimiter)
{
if (node != null && elem != null)
{
List attrs = node.getAttributes(name);
StringBuffer buf = new StringBuffer();
char delimiter = (listDelimiter != 0) ? listDelimiter : ATTR_VALUE_DELIMITER;
for (Iterator it = attrs.iterator(); it.hasNext();)
{
Node attr = (Node) it.next();
if (attr.getValue() != null)
{
if (buf.length() > 0)
{
buf.append(delimiter);
}
buf.append(PropertyConverter.escapeDelimiters(attr
.getValue().toString(), delimiter));
}
attr.setReference(elem);
}
if (buf.length() < 1)
{
elem.removeAttribute(name);
}
else
{
elem.setAttribute(name, buf.toString());
}
}
}
/**
* Updates the value of the specified attribute of the given node.
* Because there can be multiple child nodes representing this attribute
* the new value is determined by iterating over all those child nodes.
*
* @param node the affected node
* @param name the name of the attribute
* @param listDelimiter the delimiter for attributes with multiple values
*/
static void updateAttribute(Node node, String name, char listDelimiter)
{
if (node != null)
{
updateAttribute(node, (Element) node.getReference(), name, listDelimiter);
}
}
/**
* Helper method for accessing the element of the specified node.
*
* @param node the node
* @return the element of this node
*/
private Element getElement(Node node)
{
// special treatment for root node of the hierarchy
return (node.getName() != null && node.getReference() != null) ? (Element) node
.getReference()
: document.getDocumentElement();
}
}
/**
* A special implementation of the FileConfiguration
interface that is
* used internally to implement the FileConfiguration
methods
* for XMLConfiguration
, too.
*/
private class XMLFileConfigurationDelegate extends FileConfigurationDelegate
{
public void load(InputStream in) throws ConfigurationException
{
XMLConfiguration.this.load(in);
}
}
}