/* * 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(); } }