/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright (C) 2004 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), * a copy of which is included in this distribution. */ package org.jivesoftware.messenger; import org.jivesoftware.util.XPPWriter; import java.util.*; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.dom4j.*; /** * <p>Storage of generic meta-data.</p> * <p>Meta-data is expected to be stored as XML. We use a simple * naming convention of meta-data key names: data is stored * heirarchically separated by dots. The last name may contain * a colon ':' character that is read as name:attribute. * For example setting X.Y.Z to someValue, would map to an XML snippet of:</p> * <pre> * <X> * <Y> * <Z>someValue</Z> * </Y> * </X> * </pre> * And X.Y.Z:key to anotherValue as:</p> * <pre> * <X> * <Y> * <Z key="anotherValue" /> * </Y> * </X> * </pre> * <p>Some XML cannot be built or accessed using this naming * convention (e.g. a typical Roster reset packet). More complex XML * packet should be represented using the XMPPDOMFragment. The * MetaDataFragment class is designed to provide 80% of XML * manipulation capabilities with the simplest 20% of code and API size * making it convenient for meta-data, simple IQ packets, etc.</p> * * @author Iain Shigeoka * @see XMPPDOMFragment */ public class MetaDataFragment extends PayloadFragment { Element root = DocumentHelper.createElement("root"); /** * Parsing the XML DOM every time we need a property is slow. Therefore, * we use a Map to cache property values that are accessed more than once. */ private Map propertyCache = new HashMap(); public void setName(String name) { this.name = name; ((Element)root.elements().get(0)).setName(name); } public void setNamespace(String namespace) { this.namespace = namespace; QName qName = DocumentHelper.createQName(name, DocumentHelper.createNamespace("", namespace)); ((Element)root.elements().get(0)).setQName(qName); } public void send(XMLStreamWriter xmlSerializer, int version) throws XMLStreamException { Iterator fragIter = null; if (fragments != null && !fragments.isEmpty()) { fragIter = getFragments(); } XPPWriter.write((Element)root.elements().get(0), xmlSerializer, fragIter, version); } public XMPPFragment createDeepCopy() { MetaDataFragment meta = null; Iterator metaElements = root.elementIterator(); if (metaElements.hasNext()) { meta = new MetaDataFragment((Element)metaElements.next()); while (metaElements.hasNext()) { meta.root.add((Element)metaElements.next()); } } else { meta = new MetaDataFragment(namespace, name); } Iterator fragmentIter = getFragments(); while (fragmentIter.hasNext()) { meta.addFragment(((XMPPFragment)fragmentIter.next()).createDeepCopy()); } return meta; } /** * Create an empty meta-data chunk. */ public MetaDataFragment(String namespace, String name) { super(namespace, name); QName qName = DocumentHelper.createQName(name, DocumentHelper.createNamespace("", namespace)); root.add(DocumentHelper.createElement(qName)); } /** * Create a meta-data chunk based on a DOM tree. */ public MetaDataFragment(Element root) { super(root.getNamespaceURI(), root.getName()); this.root.add((Element)root.clone()); } /** * Returns the value of the specified property. A <tt>null</tt> answer does not necessarily mean * that the property does not exist. Use {@link #includesProperty(String)} to find out whether * the property exists or not. * * @param name the name of the property to get. * @return the value of the specified property. */ public synchronized String getProperty(String name) { String value = (String)propertyCache.get(name); if (value == null) { String[] propName = parsePropertyName(name); // Grab the attribute if there is one String lastName = propName[propName.length - 1]; String attName = null; int attributeIndex = lastName.indexOf(':'); if (attributeIndex >= 0) { propName[propName.length - 1] = lastName.substring(0, attributeIndex); attName = lastName.substring(attributeIndex + 1); } // Search for this property by traversing down the XML hierarchy. Element element = root; for (int i = 0; i < propName.length; i++) { element = element.element(propName[i]); if (element == null) { break; } } if (element != null) { if (attName == null) { value = element.getTextTrim(); } else { value = element.attributeValue(attName); } // At this point, we found a matching property, so return its value. // Empty strings are returned as null. if ("".equals(value)) { value = null; } else { // Add to cache so that getting property next time is fast. propertyCache.put(name, value); } } } return value; } /** * Returns true if the specified property is included in the XML hierarchy. A property could * have a value associated or not. If the property has an associated value then * {@link #getProperty(String)} will return a String otherwise <tt>null</tt> will be answered. * * @param name the name of the property to find out. * @return true if the specified property is included in the XML hierarchy. */ public synchronized boolean includesProperty(String name) { String value = (String)propertyCache.get(name); if (value == null) { String[] propName = parsePropertyName(name); // Grab the attribute if there is one String lastName = propName[propName.length - 1]; String attName = null; int attributeIndex = lastName.indexOf(':'); if (attributeIndex >= 0) { propName[propName.length - 1] = lastName.substring(0, attributeIndex); attName = lastName.substring(attributeIndex + 1); } // Search for this property by traversing down the XML hierarchy. Element element = root; for (int i = 0; i < propName.length; i++) { element = element.element(propName[i]); if (element == null) { break; } } if (element != null) { if (attName == null){ // The property exists so return true return true; } else { // The property exists if the attribute exists in the element return element.attribute(attName) != null; } } else { // The property does not exist so return false return false; } } return true; } /** * Return all values who's path matches the given property name as a String array, * or an empty array if the if there are no children. You MAY NOT use the atttribute * markup (using a ':' in the last element name) with this call. * <p/> * getProperties() allows you to retrieve several values with the same property name. * For example, consider the XML file entry: * <pre> * <foo> * <bar> * <prop>some value</prop> * <prop>other value</prop> * <prop>last value</prop> * </bar> * </foo> * </pre> * If you call getProperties("foo.bar.prop") will return a string array containing * {"some value", "other value", "last value"}. * * @param name the name of the property to retrieve * @return all child property values for the given node name. */ public String[] getProperties(String name) { String[] propName = parsePropertyName(name); // Search for this property by traversing down the XML heirarchy, stopping one short. Element element = root; for (int i = 0; i < propName.length - 1; i++) { element = element.element(propName[i]); if (element == null) { // This node doesn't match this part of the property name which // indicates this property doesn't exist so return empty array. return new String[]{}; } } // We found matching property, return names of children. Iterator iter = element.elementIterator(propName[propName.length - 1]); ArrayList props = new ArrayList(); while (iter.hasNext()) { Element e = (Element)iter.next(); props.add(e.getName()); } String[] childrenNames = new String[props.size()]; return (String[])props.toArray(childrenNames); } /** * Sets a property to an array of values. You MAY NOT use the atttribute * markup (using a ':' in the last element name) with this call. Multiple values matching the * same property is mapped to an XML file as multiple elements containing each value. * For example, using the name "foo.bar.prop", and the value string array containing * {"some value", "other value", "last value"} would produce the following XML: * <pre> * <foo> * <bar> * <prop>some value</prop> * <prop>other value</prop> * <prop>last value</prop> * </bar> * </foo> * </pre> * * @param name the name of the property. * @param values The array of values for the property (can be empty but not null) */ public void setProperties(String name, String[] values) { String[] propName = parsePropertyName(name); setProperty(name, values[0]); // Search for this property by traversing down the XML heirarchy, stopping one short. Element element = root; for (int i = 0; i < propName.length - 1; i++) { element = element.element(propName[i]); if (element == null) { // This node doesn't match this part of the property name which // indicates this property doesn't exist so return empty array. return; } } String childName = propName[propName.length - 1]; // We found matching property, clear all children. List toRemove = new ArrayList(); Iterator iter = element.elementIterator(childName); while (iter.hasNext()) { toRemove.add(iter.next()); } for (iter = toRemove.iterator(); iter.hasNext();) { element.remove((Element)iter.next()); } // Add the new children for (int i = 0; i < values.length; i++) { if (values[i] != null) { element.addElement(childName).setText(values[i]); } } } /** * Return all children property names of a parent property as a String array, * or an empty array if the if there are no children. You MAY NOT use the atttribute * markup (using a ':' in the last element name) with this call. * For example, given the properties <tt>X.Y.A</tt>, <tt>X.Y.B</tt>, and <tt>X.Y.C</tt>, then * the child properties of <tt>X.Y</tt> are <tt>A</tt>, <tt>B</tt>, and * <tt>C</tt>. * * @param parent the name of the parent property. * @return all child property values for the given parent. */ public String[] getChildrenProperties(String parent) { String[] propName = parsePropertyName(parent); // Search for this property by traversing down the XML heirarchy. Element element = root; for (int i = 0; i < propName.length; i++) { element = element.element(propName[i]); if (element == null) { // This node doesn't match this part of the property name which // indicates this property doesn't exist so return empty array. return new String[]{}; } } // We found matching property, return names of children. List children = element.elements(); int childCount = children.size(); String[] childrenNames = new String[childCount]; for (int i = 0; i < childCount; i++) { childrenNames[i] = ((Element)children.get(i)).getName(); } return childrenNames; } /** * Returns all recursive children of the given parent property or an empty string array * if no children exist. The list of children is depth-first so the array is optimized * for easy displaying. * * @param parent the parent property. * @return all recursive children of the given property in depth-first order or an empty * string array if no children exist. */ public String[] getRecursiveChildrenProperties(String parent) { String[] properties = getChildrenProperties(parent); if (properties.length == 0) { return properties; } else { List list = new ArrayList(15); for (int i = 0; i < properties.length; i++) { String propName = parent + "." + properties[i]; list.add(propName); list.addAll(Arrays.asList(getRecursiveChildrenProperties(propName))); } return (String[])list.toArray(new String[]{}); } } /** * Sets the value of the specified property. If the property doesn't * currently exist, it will be automatically created. * * @param name the name of the property to set. * @param value the new value for the property. */ public synchronized void setProperty(String name, String value) { if (name == null) return; if (value == null) value = ""; // Set cache correctly with prop name and value. propertyCache.put(name, value); String[] propName = parsePropertyName(name); // Search for this property by traversing down the XML heirarchy. Element element = root; for (int i = 0; i < propName.length - 1; i++) { // If we don't find this part of the property in the XML heirarchy // we add it as a new node if (element.element(propName[i]) == null) { element.addElement(propName[i]); } element = element.element(propName[i]); } String lastName = propName[propName.length - 1]; int attributeIndex = lastName.indexOf(':'); if (attributeIndex >= 0) { String eleName = lastName.substring(0, attributeIndex); String attName = lastName.substring(attributeIndex + 1); // If we don't find this part of the property in the XML heirarchy // we add it as a new node if (element.element(eleName) == null) { element.addElement(eleName); } element.element(eleName).addAttribute(attName, value); } else { // If we don't find this part of the property in the XML heirarchy // we add it as a new node if (element.element(lastName) == null) { element.addElement(lastName); } // Set the value of the property in this node. element.element(lastName).setText(value); } } /** * Deletes the specified property. You may use the atttribute markup (using a ':' in the last * element name) with this call. This method removes both the containing text, and the * element itself along with any attributes associated with that element.</p> * * @param name the property to delete. */ public synchronized void deleteProperty(String name) { // Remove property from cache. propertyCache.remove(name); String[] propName = parsePropertyName(name); // Search for this property by traversing down the XML heirarchy. Element element = root; for (int i = 0; i < propName.length - 1; i++) { element = element.element(propName[i]); // Can't find the property so return. if (element == null) { return; } } String lastName = propName[propName.length - 1]; int attributeIndex = lastName.indexOf(':'); if (attributeIndex >= 0) { String eleName = lastName.substring(0, attributeIndex); String attName = lastName.substring(attributeIndex + 1); Attribute attrib = element.element(eleName).attribute(attName); if (attrib != null) { element.element(eleName).remove(attrib); } } else { // Found the correct element to remove, so remove it... element.remove(element.element(lastName)); } } /** * Returns an array representation of the given Jive property. Jive * properties are always in the format "prop.name.is.this" which would be * represented as an array of four Strings. * * @param name the name of the Jive property. * @return an array representation of the given Jive property. */ private String[] parsePropertyName(String name) { List propName = new ArrayList(5); // Use a StringTokenizer to tokenize the property name. StringTokenizer tokenizer = new StringTokenizer(name, "."); while (tokenizer.hasMoreTokens()) { propName.add(tokenizer.nextToken()); } return (String[])propName.toArray(new String[propName.size()]); } /** * <p>Tries to convert any given fragment into a metadata fragment.</p> * * @param fragment The fragment to convert * @return The converted fragment or null if the fragment could not be converted */ public static MetaDataFragment convertToMetaData(XMPPFragment fragment) { MetaDataFragment meta = null; if (fragment instanceof MetaDataFragment) { meta = (MetaDataFragment)fragment; } else if (fragment instanceof XMPPDOMFragment) { XMPPDOMFragment dom = (XMPPDOMFragment)fragment; meta = new MetaDataFragment(dom.getRootElement()); Iterator frags = dom.getFragments(); while (frags.hasNext()) { meta.addFragment((XMPPFragment)frags.next()); } } else { // TODO: as a last resort, should read the fragment in using an empty serializer and rebuild the dom tree throw new IllegalArgumentException(); } return meta; } /** * <p>Tries to convert this fragment into a metadata fragment.</p> * * @return The converted fragment */ public XMPPDOMFragment convertToDOMFragment() { XMPPDOMFragment dom = new XMPPDOMFragment(root.createCopy()); return dom; } }