/*
* Copyright(c) 2005 Center for E-Commerce Infrastructure Development, The
* University of Hong Kong (HKU). All Rights Reserved.
*
* This software is licensed under the GNU GENERAL PUBLIC LICENSE Version 2.0 [1]
*
* [1] http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
*/
package hk.hku.cecid.piazza.commons.util;
import hk.hku.cecid.piazza.commons.module.ComponentException;
import hk.hku.cecid.piazza.commons.module.PersistentComponent;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.DocumentSource;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
/**
* PropertyTree is an implementation of a PropertySheet.
* It represents a property sheet with a tree structure and
* is actually backed by a Document object.
*
* @see org.dom4j.Document
*
* @author Hugo Y. K. Lam
*
*/
public class PropertyTree extends PersistentComponent implements PropertySheet {
private Document dom;
/**
* Creates a new instance of PropertyTree.
*/
public PropertyTree() {
dom = DocumentHelper.createDocument();
}
/**
* Creates a new instance of PropertyTree.
*
* @param url the url of the properties source.
* @throws ComponentException if the properties could not be loaded from the specified url.
*/
public PropertyTree(URL url) throws ComponentException {
super(url);
}
/**
* Creates a new instance of PropertyTree.
*
* @param node the root node of the properties source.
* @throws ComponentException if the properties could not be constructed from the specified node.
*/
public PropertyTree(org.w3c.dom.Node node) throws ComponentException {
try {
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
transformer.transform(new DOMSource(node), new StreamResult(baos));
dom = new SAXReader().read(new ByteArrayInputStream(baos.toByteArray()));
}
catch (Exception e) {
throw new ComponentException("Unable to construct from the given node", e);
}
}
/**
* Creates a new instance of PropertyTree.
*
* @param ins the input stream of the properties source.
* @throws ComponentException if the properties could not be loaded from the specified input stream.
*/
public PropertyTree(InputStream ins) throws ComponentException {
try {
dom = new SAXReader().read(ins);
} catch (Exception e) {
throw new ComponentException("Unable to read from input stream", e);
}
}
/**
* Checks if the specified xpath exists in this property tree.
*
* @param xpath the property xpath.
* @return true if the specified xpath exists in this property tree.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#containsKey(java.lang.String)
*/
public boolean containsKey(String xpath) {
return getProperty(xpath) != null;
}
/**
* Gets a property with the specified xpath.
* If the xpath refers to more than one properpty, the first one will be returned.
*
* @param xpath the property xpath.
* @return the property with the specified xpath.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#getProperty(java.lang.String)
*/
public String getProperty(String xpath) {
Node node = getPropertyNode(xpath);
return node == null ? null : StringUtilities.propertyValue(node.getStringValue());
}
/**
* Gets a property with the specified xpath.
* If the xpath refers to more than one properpty, the first one will be returned.
*
* @param xpath the property xpath.
* @param def the default value.
* @return the property with the specified xpath.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#getProperty(java.lang.String,
* java.lang.String)
*/
public String getProperty(String xpath, String def) {
Node node = getPropertyNode(xpath);
return node == null ? def : StringUtilities.propertyValue(node.getStringValue());
}
/**
* Gets a list of properties with the specified xpath.
*
* @param xpath the properties xpath.
* @return the properties with the specified xpath.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#getProperties(java.lang.String)
*/
public String[] getProperties(String xpath) {
List nodes = getPropertyNodes(xpath);
String[] nodeValues = new String[nodes.size()];
for (int i = 0; i < nodes.size(); i++) {
nodeValues[i] = StringUtilities.propertyValue(((Node) nodes.get(i)).getStringValue());
}
return nodeValues;
}
/**
* Gets a two-dimensional list of properties with the specified xpaths.
* The first xpath will define the first dimension of the list while
* the second xpath will define the second dimension. E.g.
* <p>
* <pre>
* <!- Properties content -->
* <application>
* <listener>
* <id>MyListener</id>
* <name>My Listener</name>
* </listener>
* <listener>
* <id>MyListener2</id>
* <name>My Listener 2</name>
* </listener>
* </application>
*
* First xpath: /application/listener
* Second xpath: ./id|./name
*
* Returned array:
* {{"MyListener","My Listener"},{"MyListener2","My Listener 2"}}
* </pre>
* </p>
*
* @param xpath the first xpath.
* @param xpath2 the second xpath.
* @return a two-dimensional list of properties with the specified xpaths.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#getProperties(java.lang.String,
* java.lang.String)
*/
public String[][] getProperties(String xpath, String xpath2) {
List nodes = getPropertyNodes(xpath);
String[][] nodeValues = new String[nodes.size()][];
for (int i = 0; i < nodes.size(); i++) {
List nodes2 = ((Node) nodes.get(i)).selectNodes(xpath2);
nodeValues[i] = new String[nodes2.size()];
for (int j = 0; j < nodes2.size(); j++) {
nodeValues[i][j] = StringUtilities.propertyValue(((Node) nodes2.get(j)).getStringValue());
}
}
return nodeValues;
}
/**
* Creates a Properties object which stores the properties retrieved by the specified xpath.
*
* @param xpath the properties xpath.
* @return a Properties object which stores the retrieved properties.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#createProperties(java.lang.String)
*/
public Properties createProperties(String xpath) {
Properties newProps = new Properties();
List nodes = getPropertyNodes(xpath);
for (int i = 0; i < nodes.size(); i++) {
Node node = (Node) nodes.get(i);
String key = node.getName();
int prefixIndex = key.indexOf(':');
if (prefixIndex != -1) {
key = key.substring(prefixIndex + 1);
}
String tmpkey = ((Element)node).attributeValue("name");
String tmpvalue = ((Element)node).attributeValue("value");
String tmptype = ((Element)node).attributeValue("type");
String value = node.getStringValue();
if (tmpkey != null) {
key = tmpkey.trim();
}
if (tmpvalue!=null) {
value = tmpvalue;
}
if (tmptype != null) {
String type = tmptype.trim();
if (!"".equals(type)) {
key = type + ":" + key;
}
}
if (value != null) {
newProps.setProperty(key, value);
}
}
return newProps;
}
/**
* Counts the number of properties with the specified xpath.
*
* @param xpath the properties xpath.
* @return the number of properties with the specified xpath.
*/
public int countProperties(String xpath) {
return getPropertyNodes(xpath).size();
}
/**
* Sets a property value with the specified key.
*
* @param xpath the property xpath.
* @param value the property value.
* @return true if the operation is successful. false otherwise.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#setProperty(java.lang.String,
* java.lang.String)
*/
public boolean setProperty(String xpath, String value) {
Node node = getPropertyNode(xpath);
boolean result;
if (node == null) {
result = addProperty(xpath, value);
}
else {
try {
if (value == null) {
node.detach();
}
else {
node.setText(value);
}
result = true;
}
catch (Exception e) {
result = false;
}
}
return result;
}
/**
* Removes a property with the specified xpath.
* If the xpath refers to more than one properpty, the first one will be removed.
*
* @param xpath the property xpath.
* @return true if the operation is successful. false otherwise.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#removeProperty(java.lang.String)
*/
public boolean removeProperty(String xpath) {
return setProperty(xpath, null);
}
/**
* Gets all the existing property xpaths.
*
* @return all the existing property xpaths.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#propertyNames()
*/
public Enumeration propertyNames() {
Iterator nodes = getPropertyNodes("//*[count(./*)=0]").iterator();
Vector propNames = new Vector();
while (nodes.hasNext()) {
Node node = (Node) nodes.next();
propNames.addElement(node.getUniquePath());
}
return propNames.elements();
}
/**
* Loads the properties from the specified url location.
*
* @param url the url of the properties source.
* @throws Exception if the operation is unsuccessful.
* @see hk.hku.cecid.piazza.commons.module.PersistentComponent#loading(java.net.URL)
*/
protected void loading(URL url) throws Exception {
SAXReader reader = new SAXReader();
dom = reader.read(url);
}
/**
* Stores the properties to the specified url location.
*
* @param url the url of the properties source.
* @throws Exception if the operation is unsuccessful.
* @see hk.hku.cecid.piazza.commons.module.PersistentComponent#storing(java.net.URL)
*/
protected void storing(URL url) throws Exception {
XMLWriter writer = new XMLWriter(new FileOutputStream(Convertor.toFile(url)),
OutputFormat.createPrettyPrint());
writer.write(dom);
writer.close();
}
/**
* Gets a property node with the specified xpath.
* If the xpath refers to more than one properpty node, the first one will be returned.
*
* @param xpath the property xpath.
* @return the property node with the specified xpath.
*/
protected Node getPropertyNode(String xpath) {
try {
return dom.selectSingleNode(xpath);
}
catch (Exception e) {
return null;
}
}
/**
* Gets a list of property nodes with the specified xpath.
*
* @param xpath the properties xpath.
* @return the property nodes with the specified xpath.
*/
protected List getPropertyNodes(String xpath) {
try {
return dom.selectNodes(xpath);
}
catch (Exception e) {
return Collections.EMPTY_LIST;
}
}
/**
* Adds a property to this property tree.
*
* @param xpath the property xpath.
* @param value the property value.
* @return true if the operation is successful. false otherwise.
*/
protected boolean addProperty(String xpath, String value) {
try {
if (xpath != null) {
// retrieve the root element in the document
Element curElement = dom.getRootElement();
String rootElementName = curElement == null ? "" : curElement
.getName();
// basic check for the path validity
xpath = xpath.trim();
if (xpath.startsWith("//") || xpath.startsWith("../")) {
return false;
}
else if (xpath.startsWith("./")) {
xpath = xpath.substring(2);
}
else if (xpath.startsWith("/" + rootElementName + "/")) {
xpath = xpath.substring(2 + rootElementName.length());
}
else if (xpath.equals("/" + rootElementName)) {
xpath = "";
}
else if (xpath.startsWith("/") && curElement != null) {
return false;
}
// set the value to the root directly if there is no
// sub-elements specified
if ("".equals(xpath) && curElement != null) {
curElement.setText(value);
return true;
}
StringTokenizer pathElements = new StringTokenizer(xpath, "/");
// loop the elements specified in the path
while (pathElements.hasMoreElements()) {
// retrieve the element name
String elementName = pathElements.nextToken().trim();
if (!"".equals(elementName)) {
// parse the element name
StringTokenizer elementNameCombo = new StringTokenizer(
elementName, "[]");
boolean isIndexing = elementNameCombo.countTokens() > 1;
// get all elements with the specified name
elementName = elementNameCombo.nextToken();
List nextElements = curElement == null ? Collections.EMPTY_LIST
: curElement.elements(elementName);
// assume the element referring to the first child
int elementPosition = 0;
// get the element position specified in the path, if
// any
if (isIndexing) {
try {
elementPosition = Integer
.parseInt(elementNameCombo.nextToken()) - 1;
}
catch (Exception e) {
// assume the element referring to a new child
// being appended at the end
elementPosition = nextElements.size();
}
}
// adjust the element position if it is out of range
if (elementPosition < 0) {
elementPosition = 0;
}
else if (elementPosition > nextElements.size()) {
elementPosition = nextElements.size();
}
Element nextElement;
if (elementPosition < nextElements.size()) {
nextElement = (Element) nextElements
.get(elementPosition);
}
else {
if (curElement == null) {
nextElement = DocumentHelper
.createElement(elementName);
dom.setRootElement(nextElement);
}
else {
nextElement = curElement
.addElement(elementName);
}
}
/*
// DEBUG (Print out the status in this turn of loop)
Debugger.print("curElement: " + curElement
+ ", targetElement: " + elementName
+ ", targetElementPosition: " + elementPosition
+ ", targetExists: "
+ (elementPosition < nextElements.size())
+ ", targetElementSizes: "
+ nextElements.size() + ", nextElement: "
+ nextElement);
// DEBUG (Dump all child elements of the current
// element)
if (curElement != null) {
Debugger.print(curElement.elements());
}
// END OF DEBUG
*/
// move to the next element
curElement = nextElement;
// set the value if it is the last element
if (!pathElements.hasMoreElements()) {
curElement.setText(value);
return true;
}
}
}
}
return false;
}
catch (Exception e) {
/*Sys.main.log.error("Error adding property '" + xpath + "' with value '"
+ value + "'", e);*/
return false;
}
}
/**
* Appends a property sheet to this property tree.
* The specified property sheet can only be appended if it is of the PropertyTree type.
*
* @param p the property sheet to be appended.
* @return true if the operation is successful. false otherwise.
* @see hk.hku.cecid.piazza.commons.util.PropertySheet#append(hk.hku.cecid.piazza.commons.util.PropertySheet)
*/
public boolean append(PropertySheet p) {
Document dom2;
if (p instanceof PropertyTree) {
dom2 = ((PropertyTree) p).getDOM();
}
else {
return false;
}
Element rootElement = dom.getRootElement();
Element rootElement2 = dom2.getRootElement();
if (rootElement2 == null) {
return true;
}
if (rootElement == null) {
rootElement2.detach();
dom.setRootElement(rootElement2);
return true;
}
else {
String rootElementName = rootElement.getName();
String rootElementName2 = rootElement2.getName();
if (rootElementName.equals(rootElementName2)) {
Iterator elements = rootElement2.elements().iterator();
while (elements.hasNext()) {
Element element = (Element) elements.next();
element.detach();
rootElement.add(element);
}
return true;
}
else {
return false;
}
}
}
/**
* Creates a sub-tree from this property tree.
*
* @param xpath the xpath for locating the subtree.
* @return a new property tree.
*/
public PropertyTree subtree(String xpath) {
Node node = getPropertyNode(xpath);
if (node == null) {
return new PropertyTree();
}
try{
return new PropertyTree(new ByteArrayInputStream(node.asXML().getBytes("UTF-8")));
}
catch (Exception e) {
return new PropertyTree();
}
}
/**
* Gets the docment source.
*
* @return the document source.
*/
public Source getSource() {
return new DocumentSource(dom);
}
/**
* Gets the Document object which backs this property tree.
*
* @return the Document object.
*/
private Document getDOM() {
return dom;
}
/**
* Returns a W3C document representation of this property tree.
*
* @return a new W3C document.
*/
public org.w3c.dom.Document toDocument() {
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(dom.asXML().getBytes("UTF-8")));
}
catch (Exception e) {
throw new RuntimeException("Unable to convert document", e);
}
}
/**
* Returns a string representation of this property tree, which is the XML text.
*
* @return a string representation of this property tree.
* @see java.lang.Object#toString()
*/
public String toString() {
return dom.asXML();
}
}