KJW
KJW

Reputation: 15251

Java: get xpath of element in org.w3c.dom document

I've written what I want to achieve. however, getElementIdx() function doesn't return proper count. There's an issue with getPreviousSibling() but I don't know why.

public static String getElementXpath(DOMElement elt){
        String path = ""; 

        try{
            for (; elt != null; elt = (DOMElement) elt.getParentNode()){
                int idx = getElementIdx(elt);
                String xname = elt.getTagName().toString();

                if (idx >= 1) xname += "[" + idx + "]";
                path = "/" + xname + path;  
            }
        }catch(Exception ee){
        }
        return path;                            
    }

    public static int getElementIdx(DOMElement elt) {
      int count = 1;
      try{

         for (DOMElement sib = (DOMElement) elt.getNextSibling(); sib != null; sib = (DOMElement) sib.getNextSibling())
            {
                if(sib.getTagName().equals(elt.getTagName())){
                    count++;
                }
            }
      }catch(Exception ee){      
      }
        return count;
    }

Upvotes: 7

Views: 8501

Answers (3)

Jon Skeet
Jon Skeet

Reputation: 1499770

Your title talks about getPreviousSibling(), but your code only uses getNextSibling() - why? I can't see why you'd want to use getNextSibling() at all... you want to find out how many elements of the same name come before the current one, not how many come after it.

The fact that you're catching and swallowing exceptions is also deeply suspicious... why would you want to do that? If you have an exception, shouldn't the method terminate with an exception?

You should also probably take account of the fact that getPreviousSibling may not return an element - it may return a text node, for example. You'll want to skip over those - currently you'd get an exception, which would terminate the loop and return the current count.

If these don't help, please post some sample XML, point out a node, and say what the code is currently returning (as well as posting your updated code). Just saying it doesn't return the proper count isn't nearly as useful as saying what it does return, and what you expected it to return.

EDIT: This is what I'd expect the code to look like:

public static int getElementIndex(Element original) {
  int count = 1;

  for (Node node = original.getPreviousSibling(); node != null;
       node = node.getPreviousSibling()) {
    if (node instanceof Element) {
      Element element = (Element) node;
      if (element.getTagName().equals(original.getTagName()) {
        count++;
      }
    }
  }

  return count;
}

You could also use if (node.getNodeType() == Node.ELEMENT_NODE) instead of the instanceof test.

Upvotes: 6

Lumi
Lumi

Reputation: 15264

I played around with the XOM library, which has a good API. Doing this on foot is more difficult than in XSLT. The following will get you started. Note that the sibling position stuff is missing.

An interface:

package milu.calcxpath;
import nu.xom.Node;
import nu.xom.ParentNode;

public interface Calculator
{
    public void buildXPath( Node node, StringBuilder sb );
    public void buildXPath( ParentNode node, StringBuilder sb );
}

Implementing class:

package milu.calcxpath;
import nu.xom.Attribute;
import nu.xom.Comment;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Node;
import nu.xom.ParentNode;
import nu.xom.ProcessingInstruction;
import nu.xom.Text;

public class SimpleCalculator implements Calculator
{
    @Override
    public void buildXPath( Node node, StringBuilder sb )
    {
        if ( null == node )
            return;
        if ( this.findShortCut(node, sb) )
            return;

        ParentNode parent = node.getParent();
        boolean doParents = true;
        if ( parent instanceof Element )
            if ( this.findShortCut((Element) parent, sb) )
                doParents = false;
        if ( doParents )
            this.buildXPath(parent, sb);

        if ( node instanceof Element ) {
            String name = ( (Element) node ).getLocalName();
            sb.append("/" + name);
        } else if ( node instanceof Attribute ) {
            sb.append("/@" + ( (Attribute) node ).getLocalName());
        } else if ( node instanceof Text ) {
            sb.append("/text()");
        } else if ( node instanceof Comment ) {
            sb.append("/comment()");
        } else if ( node instanceof ProcessingInstruction ) {
            sb.append("/processing-instruction()");
        }
    }

    protected boolean findShortCut( Node node, StringBuilder sb )
    {
        return false;
    }

    @Override
    public void buildXPath( ParentNode node, StringBuilder sb )
    {
        if ( null == node )
            return;
        ParentNode parent = node.getParent();
        if ( null == parent )
            return;
        else if ( parent instanceof Document ) {
            ;
        } else { // element
            if ( ! this.findShortCut((Element) parent, sb) )
                this.buildXPath(parent, sb);
        }
        sb.append("/");
        sb.append(( (Element) node ).getLocalName());
    }

    protected boolean findShortCut( Element elm, StringBuilder sb )
    {
        return false;
    }
}

Another one, extending it. This does the @id stuff.

package milu.calcxpath;
import nu.xom.Attribute;
import nu.xom.Element;
import nu.xom.Node;

public class IdShortCutCalculator extends SimpleCalculator
{
    final private static String ID = "id";

    @Override
    protected boolean findShortCut( Node node, StringBuilder sb )
    {
        if ( ! ( node instanceof Attribute ) )
            return false;
        Attribute attr = (Attribute) node;
        if ( ! attr.getLocalName().equals(ID) )
            return false;
        sb.append("//@id='");
        sb.append(attr.getValue());
        sb.append("'");
        return true;
    }

    @Override
    protected boolean findShortCut( Element elm, StringBuilder sb )
    {
        String val = elm.getAttributeValue(ID);
        if ( null == val )
            return false;
        sb.append("//*[@id='");
        sb.append(val);
        sb.append("']");
        return true;
    }
}

Another class as a frontend:

package milu.calcxpath;

import nu.xom.Node;

public class XPathCalculator
{
    private Calculator calculator;

    public XPathCalculator(Calculator calc) {
        this.calculator = calc;
    }

    public String calculateXPath( Node node )
    {
        StringBuilder sb = new StringBuilder();
        this.calculator.buildXPath(node, sb);
        return sb.toString();
    }
}

And a test script:

package milu.calcxpath;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Nodes;

public class Test
{
    public static void main( String[] args ) throws Exception
    {
        Builder builder = new Builder();
        Document doc = builder.build(Test.class.getResourceAsStream("/milu/calcxpath/eins.xml"));
        Calculator calc;
        // calc = new SimpleCalculator();
        calc = new IdShortCutCalculator();
        XPathCalculator xpc = new XPathCalculator(calc);
        show(xpc, doc, "//*");
        show(xpc, doc, "//@*");
        show(xpc, doc, "//node()");
        show(xpc, doc, "//processing-instruction()");
        show(xpc, doc, "//*//processing-instruction()");
    }

    private static void show( XPathCalculator xpc, Document doc, String xpath )
    {
        System.out.println("==========================");
        System.out.println("    " + xpath);
        Nodes nodes = doc.query(xpath);
        int size = nodes.size();
        for ( int i = 0; i < size; i++ )
            System.out.println(xpc.calculateXPath(nodes.get(i)));
    }
}

The doc I used for testing:

<Urmel>
  <!-- spukt im Schloss -->
  <Monster xmlns="urn:X-Monster">
    <Gurke>
      <?Garten eins="zwei" drei="vier"?>
      <Heini Hecht="toll">
        <eins>eins</eins>
        <zwei id="ich-bin-die-zwei">zwei</zwei>
        <drei letzt="1">drei</drei>
      </Heini>
      <!-- Es kann nur einen geben :-) -->
    </Gurke>
    <Tomate id="pomodoro">
      <eene/>
      <meene/>
      <miste>Auweia!</miste>
      <aa>
        <bb>
          <cc>dd</cc>
        </bb>
      </aa>
    </Tomate>
  </Monster>
</Urmel>

Far from perfect, but I hope this helps! :-)

Upvotes: 1

crowne
crowne

Reputation: 8534

Dom4j xpath support is really good, you can access any element by providing the xpath expression.
However I'm not sure whether the reverse is true, i.e. whether given an element you can derive the xpath expression.

See the api at http://www.docjar.com/projects/dom4j-1.6.1-code.html

Note avoid www.dom4j.org, it appears to have been hi-jacked by some kind of spammy link farm.

Upvotes: 4

Related Questions