/*
* Copyright 2004-2005 The Apache Software Foundation.
*
* Licensed 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.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
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.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
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.InputSource;
import org.apache.commons.configuration.reloading.ReloadingStrategy;
/**
* A specialized hierarchical configuration class that is able to parse XML
* documents.
*
* <p>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()</code> methods, too.
*
* @since commons-configuration 1.0
*
* @author Jörg Schaible
* @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger </a>
* @version $Revision: 156237 $, $Date: 2005-03-05 11:26:22 +0100 (Sa, 05 Mrz 2005) $
*/
public class XMLConfiguration extends HierarchicalConfiguration implements FileConfiguration
{
/** Constant for the default root element name. */
private static final String DEFAULT_ROOT_NAME = "configuration";
/** Delimiter character for attributes. */
private static char ATTR_DELIMITER = ',';
private FileConfigurationDelegate delegate = new FileConfigurationDelegate();
/** The document from this configuration's data source. */
private Document document;
/** Stores the name of the root element. */
private String rootElementName;
/**
* Creates a new instance of <code>XMLConfiguration</code>.
*/
public XMLConfiguration()
{
super();
}
/**
* Creates a new instance of <code>XMLConfiguration</code>.
* 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
{
this();
setFileName(fileName);
load();
}
/**
* Creates a new instance of <code>XMLConfiguration</code>.
* 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
{
this();
setFile(file);
if (file.exists())
{
load();
}
}
/**
* Creates a new instance of <code>XMLConfiguration</code>.
* 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
{
this();
setURL(url);
load();
}
/**
* 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 <code>UnsupportedOperationException</code>
* exception is thrown. Whether this configuration has been loaded from an
* XML document or not can be found out using the <code>getDocument()</code>
* 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;
}
/**
* Returns the XML document this configuration was loaded from. The return
* value is <b>null</b> if this configuration was not loaded from a XML
* document.
*
* @return the XML document this configuration was loaded from
*/
public Document getDocument()
{
return document;
}
/**
* @inheritDoc
*/
protected void addPropertyDirect(String key, Object obj)
{
super.addPropertyDirect(key, obj);
delegate.possiblySave();
}
/**
* @inheritDoc
*/
public void clearProperty(String key)
{
super.clearProperty(key);
delegate.possiblySave();
}
/**
* @inheritDoc
*/
public void clearTree(String key)
{
super.clearTree(key);
delegate.possiblySave();
}
/**
* @inheritDoc
*/
public void setProperty(String key, Object value)
{
super.setProperty(key, value);
delegate.possiblySave();
}
/**
* 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)
{
constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs);
}
/**
* 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
*/
private void constructHierarchy(Node node, Element element, boolean elemRefs)
{
processAttributes(node, element);
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);
node.addChild(childNode);
}
else if (w3cNode instanceof Text)
{
Text data = (Text) w3cNode;
buffer.append(data.getData());
}
}
String text = buffer.toString().trim();
if (text.length() > 0)
{
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
*/
private void processAttributes(Node node, Element element)
{
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;
for (Iterator it = PropertyConverter.split(attr.getValue(), ATTR_DELIMITER).iterator(); it.hasNext();)
{
Node child = new XMLNode(ConfigurationKey.constructAttributeKey(attr.getName()), element);
child.setValue(it.next());
node.addChild(child);
}
}
}
}
/**
* 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);
builder.processDocument(getRoot());
return document;
} /* try */
catch (DOMException domEx)
{
throw new ConfigurationException(domEx);
}
catch (ParserConfigurationException pex)
{
throw new ConfigurationException(pex);
}
}
/**
* Creates a new node object. This implementation returns an instance of the
* <code>XMLNode</code> class.
*
* @param name the node's name
* @return the new node
*/
protected Node createNode(String name)
{
return new XMLNode(name, null);
}
public void load() throws ConfigurationException
{
delegate.load();
}
public void load(String fileName) throws ConfigurationException
{
delegate.load(fileName);
}
public void load(File file) throws ConfigurationException
{
delegate.load(file);
}
public void load(URL url) throws ConfigurationException
{
delegate.load(url);
}
public void load(InputStream in) throws ConfigurationException
{
delegate.load(in);
}
public void load(InputStream in, String encoding) throws ConfigurationException
{
delegate.load(in, encoding);
}
/**
* Load the properties from the given reader.
* Note that the <code>clear()</code> 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
*/
public void load(Reader in) throws ConfigurationException
{
try
{
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document newDocument = builder.parse(new InputSource(in));
Document oldDocument = document;
document = null;
initProperties(newDocument, oldDocument == null);
document = (oldDocument == null) ? newDocument : oldDocument;
}
catch (Exception e)
{
throw new ConfigurationException(e.getMessage(), e);
}
}
public void save() throws ConfigurationException
{
delegate.save();
}
public void save(String fileName) throws ConfigurationException
{
delegate.save(fileName);
}
public void save(File file) throws ConfigurationException
{
delegate.save(file);
}
public void save(URL url) throws ConfigurationException
{
delegate.save(url);
}
public void save(OutputStream out) throws ConfigurationException
{
delegate.save(out);
}
public void save(OutputStream out, String encoding) throws ConfigurationException
{
delegate.save(out, encoding);
}
/**
* 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 = TransformerFactory.newInstance().newTransformer();
Source source = new DOMSource(createDocument());
Result result = new StreamResult(writer);
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(source, result);
}
catch (TransformerException e)
{
throw new ConfigurationException(e.getMessage(), e);
}
}
public String getFileName()
{
return delegate.getFileName();
}
public void setFileName(String fileName)
{
delegate.setFileName(fileName);
}
public String getBasePath()
{
return delegate.getBasePath();
}
public void setBasePath(String basePath)
{
delegate.setBasePath(basePath);
}
public File getFile()
{
return delegate.getFile();
}
public void setFile(File file)
{
delegate.setFile(file);
}
public URL getURL()
{
return delegate.getURL();
}
public void setURL(URL url)
{
delegate.setURL(url);
}
public void setAutoSave(boolean autoSave)
{
delegate.setAutoSave(autoSave);
}
public boolean isAutoSave()
{
return delegate.isAutoSave();
}
public ReloadingStrategy getReloadingStrategy()
{
return delegate.getReloadingStrategy();
}
public void setReloadingStrategy(ReloadingStrategy strategy)
{
delegate.setReloadingStrategy(strategy);
}
public void reload()
{
delegate.reload();
}
public String getEncoding()
{
return delegate.getEncoding();
}
public void setEncoding(String encoding)
{
delegate.setEncoding(encoding);
}
/**
* A specialized <code>Node</code> class that is connected with an XML
* element. Changes on a node are also performed on the associated element.
*/
class XMLNode extends Node
{
/**
* Creates a new instance of <code>XMLNode</code> and initializes it
* with the corresponding XML element.
*
* @param elem the XML element
*/
public XMLNode(Element elem)
{
super();
setReference(elem);
}
/**
* Creates a new instance of <code>XMLNode</code> 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 (ConfigurationKey.isAttributeKey(getName()))
{
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 (ConfigurationKey.isAttributeKey(getName()))
{
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(value.toString());
if (((Element) getReference()).getFirstChild() != null)
{
((Element) getReference()).insertBefore(txtNode, ((Element) getReference()).getFirstChild());
}
else
{
((Element) getReference()).appendChild(txtNode);
}
}
else
{
txtNode.setNodeValue(value.toString());
}
}
}
/**
* Updates the node's value if it represents an attribute.
*
*/
private void updateAttribute()
{
XMLBuilderVisitor.updateAttribute(getParent(), getName());
}
/**
* 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
* <b>null </b> 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 <code>BuilderVisitor</code> that can construct XML
* documents.
*/
static class XMLBuilderVisitor extends BuilderVisitor
{
/** Stores the document to be constructed. */
private Document document;
/**
* Creates a new instance of <code>XMLBuilderVisitor</code>
*
* @param doc the document to be created
*/
public XMLBuilderVisitor(Document doc)
{
document = doc;
}
/**
* 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);
}
/**
* @inheritDoc
*/
protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
{
if (ConfigurationKey.isAttributeKey(newNode.getName()))
{
updateAttribute(parent, getElement(parent), newNode.getName());
return null;
}
else
{
Element elem = document.createElement(newNode.getName());
if (newNode.getValue() != null)
{
elem.appendChild(document.createTextNode(newNode.getValue().toString()));
}
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
*/
private static void updateAttribute(Node node, Element elem, String name)
{
if (node != null && elem != null)
{
List attrs = node.getChildren(name);
StringBuffer buf = new StringBuffer();
for (Iterator it = attrs.iterator(); it.hasNext();)
{
Node attr = (Node) it.next();
if (attr.getValue() != null)
{
if (buf.length() > 0)
{
buf.append(ATTR_DELIMITER);
}
buf.append(attr.getValue());
}
attr.setReference(elem);
}
if (buf.length() < 1)
{
elem.removeAttribute(ConfigurationKey.removeAttributeMarkers(name));
}
else
{
elem.setAttribute(ConfigurationKey.removeAttributeMarkers(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
*/
static void updateAttribute(Node node, String name)
{
if (node != null)
{
updateAttribute(node, (Element) node.getReference(), name);
}
}
/**
* 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 treatement for root node of the hierarchy
return (node.getName() != null) ? (Element) node.getReference() : document.getDocumentElement();
}
}
private class FileConfigurationDelegate extends AbstractFileConfiguration
{
public void load(Reader in) throws ConfigurationException
{
XMLConfiguration.this.load(in);
}
public void save(Writer out) throws ConfigurationException
{
XMLConfiguration.this.save(out);
}
}
}