Code Added: Facade to read & WRITE using XPath

From: bdinmstig (bdinmstig_at_hotmail.com)
Date: 05/27/04


Date: 27 May 2004 10:18:30 -0700

I refined my attempt a little further, and the following code does
seem to work, however it has 2 major problems:

1. Very limited support for XPath features
Basic paths are supported for elements, attributes, ".", and "..",
plus also the "[expr='value']" predicate format is supported -
however, only one predicate per path step is supported, and expr must
be a relative path.

2. Poor performance
My knowledge of the full API for XML/XPath in Java is limited - so if
anyone out there knows more about it maybe you can give me some tips
to make this code work faster and use less memory.

If these don't bother you, feel free to use the following code. Enjoy!

//---------- SNIP ----------//
import java.io.*;
import java.util.*;
import org.apache.xerces.parsers.*;
import org.apache.xml.serialize.*;
import org.apache.xpath.*;
import org.w3c.dom.*;
import org.w3c.dom.traversal.*;
import org.xml.sax.*;

/**
 * A Facade that encapsulates the complexities of the XML DOM API,
 * providing a simple get/set interface for persisting information in
 * XML using XPath.
 *
 * @author Will Hains
 * @version 1.19
 */
public class XmlFacade
{
  //// XmlFacade setup ////
  
  /**
   * The parsed XML tree.
   */
  protected final Document _doc;
  
  /**
   * Loads the specified XML as the new document tree,
   * discarding the previous tree.
   */
  protected XmlFacade(String xmlText)
  {
    try
    {
      // Parse the XML document
      DOMParser parser = new DOMParser();
      parser.parse(new InputSource(new StringReader(xmlText)));
      _doc = parser.getDocument();
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error loading XML: \n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  //// Read operations ////
  
  /**
   * @return the value of the specified Node.
   */
  protected String getValue(Node nd) throws Exception
  {
    if(nd == null) return null;
    
    // Element
    if(nd.getNodeType() == Node.ELEMENT_NODE)
    {
      String flattenedValue = "";
      nd.normalize();
      NodeIterator i = XPathAPI.selectNodeIterator
      (
        nd,
        "descendant::text()"
      );
      for(Node t = i.nextNode(); t != null; t = i.nextNode())
      {
        flattenedValue += t.getNodeValue();
      }
      return flattenedValue;
    }
    
    // Non-element
    else return nd.getNodeValue();
  }
  
  /**
   * @return the first value found at the specified path,
   * or null if the path was not found.
   */
  public String get(String xpath)
  {
    try
    {
      return getValue(XPathAPI.selectSingleNode(_doc, xpath));
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error retrieving from XPath: " + xpath + "\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  /**
   * @return a List of all values found at the specified path.
   */
  public List getList(String xpath)
  {
    try
    {
      List list = new Vector();
      NodeIterator i = XPathAPI.selectNodeIterator(_doc, xpath);
      for(Node nd = i.nextNode(); nd != null; nd = i.nextNode())
      {
        list.add(getValue(nd));
      }
      return list;
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error retrieving from XPath: " + xpath + "\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  /**
   * @return an ordered Map of keys to values rooted at the
   * specified path, and defined by the specified relative key and
   * value paths.
   */
  public Map getMap(String xpath, String keyPath, String valuePath)
  {
    Map map = new LinkedHashMap();
    Object[] keys = getList(xpath + "/" + keyPath).toArray();
    Object[] values = getList(xpath + "/" + valuePath).toArray();
    for(int i = 0, length = keys.length; i < length; i++)
    {
      String key = (String)keys[i];
      if(key != null) map.put(key, values[i]);
    }
    return map;
  }
  
  //// Delete operations ////
  
  /**
   * Deletes the specified node.
   */
  protected void delete(Node nd) throws Exception
  {
    if(nd != null)
    {
      if(nd.getNodeType() == Node.ELEMENT_NODE)
      {
        nd.getParentNode().removeChild(nd);
      }
      else
      {
        Attr a = (Attr)nd;
        a.getOwnerElement().removeAttributeNode(a);
      }
    }
  }
  
  /**
   * Deletes the value at the specified path.
   */
  public void delete(String xpath)
  {
    try
    {
      delete(XPathAPI.selectSingleNode(_doc, xpath));
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error deleting from XPath: " + xpath + "\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  //// Write operations ////
  
  /**
   * Lazily creates nodes based on the specified path.
   * The implementation of this method determines the extent that
   * this class can simulate read-write XPath support.
   *
   * @return a new node inserted into the doc at the specified path,
   * or the node that already existed there.
   */
  protected Node getNode(String xpath)
  {
    try
    {
      // Find the closest existing ancestor
      Node nd = null;
      String validPath = xpath;
      for
      (
        int endOfValidPath = xpath.length();
        endOfValidPath > 0;
        endOfValidPath = validPath.lastIndexOf('/')
      )
      {
        validPath = validPath.substring(0, endOfValidPath);
        nd = XPathAPI.selectSingleNode(_doc, validPath);
        if(nd != null) break;
      }
      
      // Create the remainder of the path
      for
      (
        StringTokenizer tokens = new StringTokenizer
        (
          xpath.substring(validPath.length()),
          "/"
        );
        tokens.hasMoreTokens();
      )
      {
        // References to self can be ignored
        String ndName = tokens.nextToken();
        if(ndName.equals(".")) continue;
        
        // Find predicate (supports only single condition)
        String predicate = null;
        int predicatePos = ndName.indexOf('[');
        StringTokenizer predTokens = null;
        if(predicatePos > 0)
        {
          predicate = ndName.substring
          (
            predicatePos + 1,
            ndName.indexOf(']')
          );
          predTokens = new StringTokenizer
          (
            predicate,
            " \t\n\r\f=\'\""
          );
          ndName = ndName.substring(0, predicatePos);
        }
        
        // Attribute
        Node newNode;
        if(ndName.indexOf('@') == 0)
        {
          String attrName = ndName.substring(1);
          newNode = _doc.createAttribute(attrName);
          ((Element)nd).setAttributeNode((Attr)newNode);
        }
        
        // Element
        else
        {
          newNode = _doc.createElement(ndName);
          nd.appendChild(newNode);
        }
        
        // Update path to existing ancestor
        validPath += "/" + ndName;
        nd = newNode;
        
        // Process predicates
        if(predTokens != null)
        {
          // Find the predicate name and value
          String predNdName = "";
          String predNdValue = "";
          try
          {
            while(predNdName.length() < 1)
            {
              predNdName = predTokens.nextToken();
            }
            while(predNdValue.length() < 1)
            {
              predNdValue = predTokens.nextToken();
            }
          }
          catch(NoSuchElementException e)
          {
            throw new IllegalArgumentException
            (
              "This implementation does not support " +
              "the predicate format: " +
              predicate
            );
          }
          
          // Element
          if(predNdName.indexOf('@') < 0)
          {
            Element predicateElement = _doc.createElement(predNdName);
            Node predicateTextNode = _doc.createTextNode(predNdValue);
            predicateElement.appendChild(predicateTextNode);
            nd.appendChild(predicateElement);
          }
          
          // Attribute
          else ((Element)nd).setAttribute
          (
            predNdName.substring(1),
            predNdValue
           );
        }
      }
      
      return nd;
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error retrieving/creating from XPath: " + xpath + "\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  /**
   * Sets the value of the specified node.
   */
  protected void setValue(Node nd, String value) throws Exception
  {
    // Node is an element - replace its contents
    if(nd.getNodeType() == Node.ELEMENT_NODE)
    {
      for(int i = 0; i < nd.getChildNodes().getLength(); i++)
      {
        nd.removeChild(nd.getChildNodes().item(i));
      }
      nd.appendChild(_doc.createTextNode(value));
    }
    
    // Node is a non-element - set its value directly
    else nd.setNodeValue(value);
  }
  
  /**
   * Sets the value at the specified path.
   */
  public void set(String xpath, boolean value)
  {
    set(xpath, Boolean.toString(value));
  }
  
  /**
   * Sets the value at the specified path.
   */
  public void set(String xpath, int value)
  {
    set(xpath, Integer.toString(value));
  }
  
  /**
   * Sets the value at the specified path.
   */
  public void set(String xpath, java.lang.Object value)
  {
    if(value == null) delete(xpath);
    else set(xpath, value.toString());
  }
  
  /**
   * Sets the value at the specified path.
   */
  public void set(String xpath, String value)
  {
    try
    {
      setValue(getNode(xpath), value);
      fireXmlChanged();
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error writing to XPath: " + xpath + "\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  /**
   * Sets the values matching the specified path.
   */
  public void setList(String xpath, Collection c)
  {
    try
    {
      // Make sure there is at least one matching node
      if(c.size() > 0) getNode(xpath);
      
      // Find existing nodes matching XPath query
      Node prevNode = null;
      NodeIterator i = XPathAPI.selectNodeIterator(_doc, xpath);
      for(Iterator j = c.iterator(); j.hasNext();)
      {
        // Get the next value to be set
        Object v = j.next();
        String value = v != null ? v.toString() : null;
        
        // A node exists in the current list position - change it
        Node nd = i.nextNode();
        if(nd != null) setValue(nd, value);
        
        // The list is longer than there are existing nodes
        else
        {
          // Element
          if(prevNode.getNodeType() == Node.ELEMENT_NODE)
          {
            Element el = _doc.createElement(prevNode.getNodeName());
            nd = prevNode.getParentNode().appendChild(el);
          }
          
          // Attribute
          else
          {
            // Copy the previous node's owner element / parent node
            Element owner = ((Attr)prevNode).getOwnerElement();
            Element copy = _doc.createElement(owner.getNodeName());
            owner = (Element)owner.getParentNode().appendChild(copy);
            
            // Add a copy of the previous node and set its value
            nd = _doc.createAttribute(((Attr)prevNode).getName());
            owner.setAttributeNode((Attr)nd);
          }
          
          // Set the value of the new node
          setValue(nd, value);
        }
        
        // Keep the current node for copying later
        prevNode = nd;
      }
      
      // Remove redundant nodes when existing list is longer
      List redundantNodes = new Vector();
      for(Node nd = i.nextNode(); nd != null; nd = i.nextNode())
      {
        redundantNodes.add(nd);
      }
      for(Iterator r = redundantNodes.iterator(); r.hasNext();)
      {
        delete((Node)r.next());
      }
      
      // Clean up parent nodes that contain no other information
      String voidParentsPath =
        xpath.substring(0, xpath.lastIndexOf('/')) +
        "[not(@*|node())]";
      NodeIterator k = XPathAPI.selectNodeIterator
      (
        _doc,
        voidParentsPath
       );
      for(Node n = k.nextNode(); n != null; n = k.nextNode())
      {
        delete(n);
      }
      fireXmlChanged();
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error writing to XPath: " + xpath + "\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
       );
    }
  }
  
  /**
   * Sets a key-value pair list matching the specified paths to the
   * contents of the specified Map.
   * If you want to maintain the order of the keys, use an ordered Map
   * implementation, such as TreeMap or LinkedHashMap.
   *
   * @param xpath the absolute path to the element or elements
   * that contain the key-value pairs.
   * @param keyPath the relative path from the element(s) specified by
   * xpath to the element/attribute containing each key.
   * @param valuePath the relative path from the element(s) specified
   * by xpath to the element/attribute containing each value.
   */
  public void setMap
  (
    String xpath,
    String keyPath,
    String valuePath,
    Map map
  )
  {
    try
    {
      // Delete existing nodes
      List redundantNodes = new Vector();
      NodeIterator n = XPathAPI.selectNodeIterator(_doc, xpath);
      for(Node nd = n.nextNode(); nd != null; nd = n.nextNode())
      {
        redundantNodes.add(nd);
      }
      for(Iterator r = redundantNodes.iterator(); r.hasNext();)
      {
        delete((Node)r.next());
      }
      
      // Set map values
      if(valuePath == null || valuePath.equals("")) valuePath = ".";
      for(Iterator i = map.keySet().iterator(); i.hasNext();)
      {
        String key = (String)i.next();
        set
        (
          xpath + "[" + keyPath + " = '" + key + "']/" + valuePath,
          map.get(key)
         );
        fireXmlChanged();
      }
    }
    catch(Exception e)
    {
      throw new IllegalArgumentException
      (
        "Error writing to XPath: " + xpath +
        "{ " + keyPath + " => " + valuePath + " }\n" +
        e.getClass().getName() + ": " +
        e.getMessage()
      );
    }
  }
  
  public String toString()
  {
    try
    {
      OutputFormat format = new OutputFormat(_doc);
      format.setStandalone(false);
      format.setIndent(4);
      format.setLineWidth(0);
      StringWriter stringOut = new StringWriter();
      XMLSerializer serial = new XMLSerializer(stringOut, format);
      serial.serialize(_doc.getDocumentElement());
      return stringOut.toString();
    }
    catch(IOException e)
    {
      return null;
    }
  }
  
  //// Events & Listeners ////
  
  private final Set _listeners = new HashSet();
  
  public void addXmlFacadeListener(XmlFacadeListener l)
  {
    _listeners.add(l);
  }
  
  public void removeXmlFacadeListener(XmlFacadeListener l)
  {
    _listeners.remove(l);
  }
  
  protected void fireXmlChanged()
  {
    for(Iterator i = _listeners.iterator(); i.hasNext();)
    {
      ((XmlFacadeListener)i.next()).dataChanged();
    }
  }
}

/**
 * Unit test class.
 * (Just testing the set methods for now.)
 */
class TestXmlFacade
{
  public static void main(String[] args)
  {
    XmlFacade xml = new XmlFacade("<XF/>");
    xml.set("/XF/Test[@no='1']", true);
    xml.set("/XF/Test[@no='2']", 14);
    xml.set("/XF/Test[@no='3']", "Hello World");
    System.out.println(xml);
    
    List list = new Vector();
    list.add("Daria");
    list.add("Trent");
    list.add("Quinn");
    list.add("Helen");
    list.add("Jake");
    list.add("Jane");
    list.add("Tiffany");
    xml.setList("/XF/Test[@no='4']/Morgendorffer/@name", list);
    list.remove("Helen");
    xml.setList("/XF/Test[@no='4']/Morgendorffer/@name", list);
    list.add("Sandi");
    xml.setList("/XF/Test[@no='4']/Morgendorffer/@name", list);
    list.add(3, "Stacey");
    xml.setList("/XF/Test[@no='4']/Morgendorffer/@name", list);
    System.out.println(xml);
    
    Map map = new LinkedHashMap();
    map.put("Daria","F");
    map.put("Quinn","F");
    map.put("Helen","F");
    map.put("Jane","F");
    map.put("Tiffany","F");
    map.put("Trent","M");
    map.put("Jake","M");
    xml.setMap("/XF/Test[@no='5']/Morgendorffer", "@name", null, map);
    map.remove("Tiffany");
    xml.setMap("/XF/Test[@no='5']/Morgendorffer", "@name", null, map);
    map.put("Stacey","F");
    xml.setMap("/XF/Test[@no='5']/Morgendorffer", "@name", null, map);
    System.out.println(xml);
  }
}
//---------- SNIP ----------//
public interface XmlFacadeListener
{
  public void dataChanged();
}
//---------- SNIP ----------//



Relevant Pages

  • Facade to read & WRITE using XPath
    ... I have taught my developers the basics of XPath, ... to have to review XMLDOM code all over the place, so this Facade ... public void set(String xpath, String value); ...
    (comp.lang.java)
  • Re: tdom xpath search
    ... what you intend; XPath 1.0 doesn't ... know string matching wild-cards. ... always the first seq-name child below sequence, ...
    (comp.lang.tcl)
  • Re: HTML-Seite parsen in Java??
    ... Ich habe genau sowas vor einiger Zeit gemacht und dabei gute Erfahrungen mit der Kombination TagSoup und XPath gemacht. ... Map<String, String> map; ... public Iterator getPrefixes(String namespaceURI) { ... } catch (XPathExpressionException e) { ...
    (de.comp.lang.java)
  • Re: .NET equivalent to XSLT value-of select
    ... There seem to be a number of classes that can use XPath, ... > XPathNavigator and its method Evaluate where you would need to explictly ... > call the XPath string function on your expression e.g. ... > public static void Main { ...
    (microsoft.public.dotnet.xml)