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