Integrating with Jaxen

Jaxen is a Java library providing generic navigation via xpath across various kinds of object/document models. Here’s how to integrate it with Scala, which gives you the ability to use xpath to navigate through (and filter) Scala objects.

package sjaxen;
 
// Bring in what we need from Jaxen
import org.jaxen.{DefaultNavigator,BaseXPath,Context,NamedAccessNavigator,JaxenConstants}
import JaxenConstants.EMPTY_ITERATOR
// Rename some stuff from Java so we can easily refer to it.
import java.util.{Iterator=>JIterator, Collection=>JCollection, List=>JList, Collections=>JCollections}
 
/** ProductNavigator implements the singleton navigator object for 
Jaxen-xpath navigation over Scala objects. Most methods here exist 
to be called by Jaxen. You can call parseXPath to create an evaluator
for an xpath expression against Scala objects. 
@author Ross Judson
*/
object ProductNavigator extends ScalaNavigator {
  /** Create an xpath evaluator for an expression. This compiles the expression
  into a form that can be executed against Scala objects. */
  def parseXPath(xpath: String) = new ProductXPath(xpath)
  def getTextStringValue(obj: Object) = obj match {
    case pe: ProductElement => pe.toString()
    case _ => obj.toString()
  }
  def isElement(obj: Object) = obj.isInstanceOf[ProductElement]
  def getElementName(obj: Object) = obj.asInstanceOf[ProductElement].name
  override def getParentAxisIterator(obj: Object) = obj match {
    case pe: ProductElement => singletonIterator(pe.parent)
    case _ => EMPTY_ITERATOR
  }
  override def getChildAxisIterator(obj: Object): JIterator = obj match {
    case xpc: XPathChildren => wrap(xpc.xpathChildren.elements)
    case _ => EMPTY_ITERATOR
  }
  def getChildAxisIterator(contextNode: Object, localName: String, prefix: String, namespace: String): JIterator = {
    val context = contextNode.asInstanceOf[ProductElement]
		val targ = context.product		    
		try {
      targ match {
        case xa: XPathAxes => new ProductElementIter(context, localName, xa.xpathAxis(localName).elements)
        case _ =>
          targ.getClass getMethod(localName, EMPTY_CLASS) invoke(targ, EMPTY) match {
            case null => EMPTY_ITERATOR
            case itr: Iterable[Object] => new ProductElementIter(context, localName, itr.elements)
            case coll: JCollection => new ProductElementIter(context, localName, coll.iterator)
            case arr: Object if arr.getClass.isArray => EMPTY_ITERATOR
            case o: Object => singletonIterator(ProductElement(context, localName, o)) 
          }
      }
    } catch {
      case _ => EMPTY_ITERATOR  
    }
  }
  
  // This differs from getChildAxisIterator in that it's retrieving
  // an attribute and returns the value in the field, rather than a 
  // list of field values that can be iterated through.
  def getAttributeAxisIterator(contextNode: Object, localName: String, prefix: String, namespace: String): JIterator = { 
    val context = contextNode.asInstanceOf[ProductElement]
    val targ = context.product        
     try {
       targ match {
         case xa: XPathAxes => new ProductElementIter(context, localName, xa.xpathAxis(localName).elements)
         case _ =>
           targ.getClass getMethod(localName, EMPTY_CLASS) invoke(targ, EMPTY) match {
             case null => EMPTY_ITERATOR
             case itr: Iterable[Object] => itr.elements
             case coll: JCollection => coll.iterator
             case arr: Object if arr.getClass.isArray => EMPTY_ITERATOR
             case o: Object => singletonIterator(o) 
           }
       }
     } catch {
       case _ => EMPTY_ITERATOR  
     }
  }
  
  implicit def wrap[T <: Object](iter: Iterator[T]): JIterator = new JIterator {
    def remove = throw new UnsupportedOperationException
    def hasNext = iter.hasNext
    def next: Object = iter.next
  }
  private def singletonIterator(obj: Object) = JCollections.singletonList(obj).iterator 
  private def ProductElement(parent: ProductElement, name: String, product: Object) = 
    new ProductElement(name, product).setParent(parent)
  
  private final val EMPTY = new Array[Object](0)
  private final val EMPTY_CLASS = new Array[Class](0)
 
  private class ProductElementIter(context: ProductElement, name: String, iter: JIterator) extends JIterator {
		def remove = throw new UnsupportedOperationException
    def hasNext = iter.hasNext
    def next = ProductElement(context, name, iter.next)
  }
}
 
private [sjaxen] case class ProductElement(name: String, product: Object) {
  var parent: ProductElement = _
  def setParent(p: ProductElement) = { parent = p; this }
}
 
class ProductXPath(xpath: String) extends BaseXPath(xpath, ProductNavigator) {
  override protected def getContext(node: Object) = node match {
    case c: Context => c
    case pe: ProductElement => super.getContext(node)
    case lst: JList => 
      super.getContext(lstMap(lst, o => ProductElement("root", o))) 
    case _ => super.getContext(ProductElement("root", node)) 
  }
  override def evaluate(node: Object) = super.evaluate(node) match {
    case ProductElement(_, obj) => obj
    case coll: JCollection => lstMap(coll, { 
      case ProductElement(_, obj) => obj
      case v: Object => v
    })
  }
  
  private def lstMap(lst: JCollection, f: Object => Object) = {
    val newList = new java.util.ArrayList(lst.size)
      val itr = lst.iterator()
      while (itr.hasNext) {
        newList.add(f(itr.next()))
      }
    newList
  }
}
 
/** Provides defaults for many of the Jaxen methods, so implementation of
concrete classes is easier. */
abstract class ScalaNavigator extends DefaultNavigator with NamedAccessNavigator {
  def getNamespacePrefix(obj: Object) = null
  def getCommentStringValue(obj: Object) = null
  def getNamespaceStringValue(obj: Object) = obj.toString()
  def getAttributeStringValue(obj: Object) = obj.toString()
  def getElementStringValue(obj: Object) = getTextStringValue(obj)
  def isProcessingInstruction(obj: Object) = false
  def isText(obj: Object) = obj.isInstanceOf[String]
  def isComment(obj: Object) = false                                              
  def isDocument(obj: Object) = false                                              
  def isNamespace(obj: Object) = false                                              
  def isAttribute(obj: Object) = false                                              
  def getAttributeQName(obj: Object) = ""
  def getAttributeName(obj: Object) = ""
  def getAttributeNamespaceUri(obj: Object) = ""
  def getElementQName(obj: Object) = ""
  def getElementNamespaceUri(obj: Object) = ""
}
 
/** To take direct control over axes, extend your class
with this trait, and you'll be able to return whatever you'd
like. */
trait XPathAxes {
  def xpathAxis(name: String): Iterable[AnyRef]
}
/** If there's a natural "child" axis for your object,
you can extend with this trait and get some default 
behavior. */
trait XPathChildren extends XPathAxes {
  def xpathChildren: Iterable[AnyRef]
  def xpathAxis(name: String) = name match {
    case "children" => xpathChildren
    case _ => Nil
  }
}
 
/*
object TestIt {
 
  def main(args: Array[String]) {
    val b = Both("Ross", new Zinger(List("Hello", "World")), new Zinger(List("Bottom", "Ladder")))
    val xp = ProductNavigator.parseXPath("left/lst|left/../right[@name='Ross']/lst")
    Console.println(xp evaluate b)
  }
  
  class Zinger(val lst: List[String]) {
    val name = "Ross"
  }
  case class Both(name: String, left: Zinger, right: Zinger);
  
}
*/
 
code/integrating-with-jaxen.txt · Last modified: 2010/02/11 09:10
 
Recent changes RSS feed Valid XHTML 1.0 Driven by DokuWiki