blob: d48ef7deab966d7f2590d396dbfa1d28cc56abf4 [file] [log] [blame]
/*
* Copyright 2004 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.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
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.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.apache.commons.lang.StringUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.CharacterData;
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.InputSource;
/**
* Reads a XML configuration file.
*
* To retrieve the value of an attribute of an element, use
* <code>X.Y.Z[@attribute]</code>. The '@' symbol was chosen for consistency
* with XPath.
*
* Setting property values will <b>NOT </b> automatically persist changes to
* disk, unless <code>autoSave=true</code>.
*
* @since commons-configuration 1.0
*
* @author Jörg Schaible
* @author <a href="mailto:kelvint@apache.org">Kelvin Tan </a>
* @author <a href="mailto:dlr@apache.org">Daniel Rall </a>
* @author Emmanuel Bourg
* @version $Revision: 1.17 $, $Date: 2004/10/04 19:35:45 $
*/
public class XMLConfiguration extends AbstractFileConfiguration
{
// For conformance with xpath
private static final String ATTRIBUTE_START = "[@";
private static final String ATTRIBUTE_END = "]";
/**
* For consistency with properties files. Access nodes via an "A.B.C"
* notation.
*/
private static final String NODE_DELIMITER = ".";
/**
* The XML document from our data source.
*/
private Document document;
/**
* If true, modifications are immediately persisted.
*/
private boolean autoSave = false;
/**
* Creates an empty XML configuration.
*/
public XMLConfiguration()
{
// build an empty document.
DocumentBuilder builder = null;
try
{
builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
}
catch (ParserConfigurationException e)
{
throw new ConfigurationRuntimeException(e.getMessage(), e);
}
document = builder.newDocument();
document.appendChild(document.createElement("configuration"));
}
/**
* Creates and loads the XML configuration from the specified resource.
*
* @param resource The name of the resource to load.
*
* @throws ConfigurationException Error while loading the XML file
*/
public XMLConfiguration(String resource) throws ConfigurationException
{
this.fileName = resource;
url = ConfigurationUtils.locate(resource);
load();
}
/**
* Creates and loads the XML configuration from the specified file.
*
* @param file The XML file to load.
* @throws ConfigurationException Error while loading the XML file
*/
public XMLConfiguration(File file) throws ConfigurationException
{
setFile(file);
load();
}
/**
* Creates and loads the XML configuration from the specified URL.
*
* @param url The location of the XML file to load.
* @throws ConfigurationException Error while loading the XML file
*/
public XMLConfiguration(URL url) throws ConfigurationException
{
setURL(url);
load();
}
/**
* {@inheritDoc}
*/
public void load(Reader in) throws ConfigurationException
{
try
{
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
document = builder.parse(new InputSource(in));
}
catch (Exception e)
{
throw new ConfigurationException(e.getMessage(), e);
}
initProperties(document.getDocumentElement(), new StringBuffer());
}
/**
* Loads and initializes from the XML file.
*
* @param element The element to start processing from. Callers should supply the root element of the document.
* @param hierarchy
*/
private void initProperties(Element element, StringBuffer hierarchy)
{
StringBuffer buffer = new StringBuffer();
NodeList list = element.getChildNodes();
for (int i = 0; i < list.getLength(); i++)
{
Node node = list.item(i);
if (node instanceof Element)
{
Element child = (Element) node;
StringBuffer subhierarchy = new StringBuffer(hierarchy.toString());
subhierarchy.append(child.getTagName());
processAttributes(subhierarchy.toString(), child);
initProperties(child, subhierarchy.append(NODE_DELIMITER));
}
else if (node instanceof CDATASection || node instanceof Text)
{
CharacterData data = (CharacterData) node;
buffer.append(data.getData());
}
}
String text = buffer.toString().trim();
if (text.length() > 0 && hierarchy.length() > 0)
{
super.addProperty(hierarchy.substring(0, hierarchy.length() - 1), text);
}
}
/**
* Helper method for constructing properties for the attributes of the given
* XML element.
*
* @param hierarchy the actual hierarchy
* @param element the actual XML element
*/
private void processAttributes(String hierarchy, Element element)
{
// Add attributes as x.y{ATTRIBUTE_START}att{ATTRIBUTE_END}
NamedNodeMap attributes = element.getAttributes();
for (int i = 0; i < attributes.getLength(); ++i)
{
Attr attr = (Attr) attributes.item(i);
String attrName = hierarchy + ATTRIBUTE_START + attr.getName() + ATTRIBUTE_END;
super.addProperty(attrName, attr.getValue());
}
}
/**
* Calls super method, and also ensures the underlying {@linkDocument} is
* modified so changes are persisted when saved.
*
* @param name
* @param value
*/
public void addProperty(String name, Object value)
{
super.addProperty(name, value);
addXmlProperty(name, value);
possiblySave();
}
Object getXmlProperty(String name)
{
// parse the key
String[] nodes = parseElementNames(name);
String attName = parseAttributeName(name);
// get all the matching elements
List children = findElementsForPropertyNodes(nodes);
List properties = new ArrayList();
if (attName == null)
{
// return text contents of elements
Iterator cIter = children.iterator();
while (cIter.hasNext())
{
Element child = (Element) cIter.next();
// add non-empty strings
String text = getChildText(child);
if (StringUtils.isNotEmpty(text))
{
properties.add(text);
}
}
}
else
{
// return text contents of attributes
Iterator cIter = children.iterator();
while (cIter.hasNext())
{
Element child = (Element) cIter.next();
if (child.hasAttribute(attName))
{
properties.add(child.getAttribute(attName));
}
}
}
switch (properties.size())
{
case 0:
return null;
case 1:
return properties.get(0);
default:
return properties;
}
}
/**
* TODO Add comment.
*
* @param nodes
* @return
*/
private List findElementsForPropertyNodes(String[] nodes)
{
List children = new ArrayList();
List elements = new ArrayList();
children.add(document.getDocumentElement());
for (int i = 0; i < nodes.length; i++)
{
elements.clear();
elements.addAll(children);
children.clear();
String eName = nodes[i];
Iterator eIter = elements.iterator();
while (eIter.hasNext())
{
Element element = (Element) eIter.next();
NodeList list = element.getChildNodes();
for (int j = 0; j < list.getLength(); j++)
{
Node node = list.item(j);
if (node instanceof Element)
{
Element child = (Element) node;
if (eName.equals(child.getTagName()))
{
children.add(child);
}
}
}
}
}
return children;
}
private static String getChildText(Node node)
{
// is there anything to do?
if (node == null)
{
return null;
}
// concatenate children text
StringBuffer str = new StringBuffer();
Node child = node.getFirstChild();
while (child != null)
{
short type = child.getNodeType();
if (type == Node.TEXT_NODE)
{
str.append(child.getNodeValue());
}
else if (type == Node.CDATA_SECTION_NODE)
{
str.append(child.getNodeValue());
}
child = child.getNextSibling();
}
// return text value
return StringUtils.trimToNull(str.toString());
}
private Element getChildElementWithName(String eName, Element element)
{
Element child = null;
NodeList list = element.getChildNodes();
for (int j = 0; j < list.getLength(); j++)
{
Node node = list.item(j);
if (node instanceof Element)
{
child = (Element) node;
if (eName.equals(child.getTagName()))
{
break;
}
child = null;
}
}
return child;
}
/**
* Adds the property value in our document tree.
*
* @param name The name of the element to set a value for.
* @param value The value to set.
*/
private void addXmlProperty(String name, Object value)
{
// parse the key
String[] nodes = parseElementNames(name);
String attName = parseAttributeName(name);
Element element = document.getDocumentElement();
Element parent = element;
for (int i = 0; i < nodes.length; i++)
{
if (element == null)
{
break;
}
parent = element;
String eName = nodes[i];
Element child = getChildElementWithName(eName, element);
element = child;
}
Element child = document.createElement(nodes[nodes.length - 1]);
parent.appendChild(child);
if (attName == null)
{
CharacterData data = document.createTextNode(String.valueOf(value));
child.appendChild(data);
}
else
{
child.setAttribute(attName, String.valueOf(value));
}
}
/**
* Calls super method, and also ensures the underlying {@link Document}is
* modified so changes are persisted when saved.
*
* @param name The name of the property to clear.
*/
public void clearProperty(String name)
{
super.clearProperty(name);
clearXmlProperty(name);
possiblySave();
}
private void clearXmlProperty(String name)
{
// parse the key
String[] nodes = parseElementNames(name);
String attName = parseAttributeName(name);
// get all the matching elements
List children = findElementsForPropertyNodes(nodes);
if (attName == null)
{
// remove children with no subelements
Iterator cIter = children.iterator();
while (cIter.hasNext())
{
Element child = (Element) cIter.next();
// determine if child has subelments
boolean hasSubelements = false;
Node subchild = child.getFirstChild();
while (subchild != null)
{
if (subchild.getNodeType() == Node.ELEMENT_NODE)
{
hasSubelements = true;
break;
}
subchild = subchild.getNextSibling();
}
if (!hasSubelements)
{
// safe to remove
if (!child.hasAttributes())
{
// remove entire node
Node parent = child.getParentNode();
parent.removeChild(child);
}
else
{
// only remove node contents
subchild = child.getLastChild();
while (subchild != null)
{
child.removeChild(subchild);
subchild = child.getLastChild();
}
}
}
}
}
else
{
// remove attributes from children
Iterator cIter = children.iterator();
while (cIter.hasNext())
{
Element child = (Element) cIter.next();
child.removeAttribute(attName);
}
}
}
/**
* Save the configuration if the automatic persistence is enabled and a file
* is specified.
*/
private void possiblySave()
{
if (autoSave && fileName != null)
{
try
{
save();
}
catch (ConfigurationException ce)
{
throw new ConfigurationRuntimeException("Failed to auto-save", ce);
}
}
}
/**
* If true, changes are automatically persisted.
*
* @param autoSave
*/
public void setAutoSave(boolean autoSave)
{
this.autoSave = autoSave;
}
/**
* {@inheritDoc}
*/
public void save(Writer writer) throws ConfigurationException
{
try
{
Transformer transformer = TransformerFactory.newInstance().newTransformer();
Source source = new DOMSource(document);
Result result = new StreamResult(writer);
transformer.setOutputProperty("indent", "yes");
transformer.transform(source, result);
}
catch (TransformerException e)
{
throw new ConfigurationException(e.getMessage(), e);
}
}
public String toString()
{
StringWriter writer = new StringWriter();
try
{
save(writer);
}
catch (ConfigurationException e)
{
e.printStackTrace();
}
return writer.toString();
}
/**
* Parse a property key and return an array of the element hierarchy it
* specifies. For example the key "x.y.z[@abc]" will result in [x, y, z].
*
* @param key the key to parse
*
* @return the elements in the key
*/
protected static String[] parseElementNames(String key)
{
if (key == null)
{
return new String[]{};
}
else
{
// find the beginning of the attribute name
int attStart = key.indexOf(ATTRIBUTE_START);
if (attStart > -1)
{
// remove the attribute part of the key
key = key.substring(0, attStart);
}
return StringUtils.split(key, NODE_DELIMITER);
}
}
/**
* Parse a property key and return the attribute name if it existst.
*
* @param key the key to parse
*
* @return the attribute name, or null if the key doesn't contain one
*/
protected static String parseAttributeName(String key)
{
String name = null;
if (key != null)
{
// find the beginning of the attribute name
int attStart = key.indexOf(ATTRIBUTE_START);
if (attStart > -1)
{
// find the end of the attribute name
int attEnd = key.indexOf(ATTRIBUTE_END);
attEnd = attEnd > -1 ? attEnd : key.length();
name = key.substring(attStart + ATTRIBUTE_START.length(), attEnd);
}
}
return name;
}
}