How to use Java XPath with KML files and namespaces on Android

I’m struggling with how to use XPath on KML files that contain the new gx:Track and gx:coord tags. The problem is with how to use XPath with namespaces under Android.

I’ve looked at a number of examples, including these

but I can’t seem to get even those examples to work.

The following code and output illustrates my problem:

public App() {
    super();
    try {
        test( testDoc1() );
        test( testDoc2() );
    } catch( Exception e ) {
        e.printStackTrace();
    } finally {
        Log.d( "TEST-FINISHED", "test is finished" );
    }
}

private String toXmlString( Document document ) throws TransformerException {
    DOMSource domSource = new DOMSource( document );
    StringWriter writer = new StringWriter();
    StreamResult result = new StreamResult( writer );
    TransformerFactory tf = TransformerFactory.newInstance();
    Transformer transformer = tf.newTransformer();
    transformer.transform( domSource, result );
    return writer.toString();
}

private Document testDoc1() throws ParserConfigurationException {
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    documentBuilderFactory.setNamespaceAware( true );
    Document mDocument = documentBuilderFactory.newDocumentBuilder().newDocument();

    String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
    Element mKmlElement = mDocument.createElement( "kml" );
    mKmlElement.setAttributeNS( XMLNS_NAMESPACE_URI, "xmlns", "http://www.opengis.net/kml/2.2" );
    mKmlElement.setAttributeNS( XMLNS_NAMESPACE_URI, "xmlns:gx", "http://www.google.com/kml/ext/2.2" );
    mDocument.appendChild( mKmlElement );

    Element mPlacemarkElement = mDocument.createElement( "Placemark" );
    mKmlElement.appendChild( mPlacemarkElement );

    Element gxTrackElement = mDocument.createElement( "gx:Track" );
    mPlacemarkElement.appendChild( gxTrackElement );

    Element gxCoordElement = mDocument.createElement( "gx:coord" );
    gxCoordElement.setTextContent( "-122.207881 37.371915 156.000000" );
    gxTrackElement.appendChild( gxCoordElement );

    return mDocument;
}

private Document testDoc2() throws ParserConfigurationException, IOException, SAXException {
    String kmlString = "<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Placemark><gx:Track><gx:coord>-122.207881 37.371915 156.000000</gx:coord></gx:Track></Placemark></kml>";

    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    documentBuilderFactory.setNamespaceAware( true );
    Document mDocument = documentBuilderFactory.newDocumentBuilder().parse( new InputSource( new StringReader( kmlString ) ) );

    return mDocument;
}

private void test( Document mDocument ) throws Exception {
    String xml = toXmlString( mDocument );
    Log.d( "TEST-XML", xml );

    XPath xPath = XPathFactory.newInstance().newXPath();
    xPath.setNamespaceContext( new NamespaceContext() {
        @Override
        public String getNamespaceURI( String prefix ) {
            switch( prefix ) {
                case XMLConstants.DEFAULT_NS_PREFIX:
                    return "http://www.opengis.net/kml/2.2";
                case "gx":
                    return "http://www.google.com/kml/ext/2.2";
            }
            return XMLConstants.NULL_NS_URI;
        }

        @Override
        public String getPrefix( String namespaceURI ) {
            return null;
        }

        @Override
        public Iterator getPrefixes( String namespaceURI ) {
            return null;
        }
    } );
    NodeList result1 = (NodeList) xPath.evaluate( "/kml", mDocument, XPathConstants.NODESET );
    Log.d( "TEST-RESULT1", String.valueOf( result1.getLength() ) );
    NodeList result2 = (NodeList) xPath.evaluate( "/kml/Placemark", mDocument, XPathConstants.NODESET );
    Log.d( "TEST-RESULT2", String.valueOf( result2.getLength() ) );
    NodeList result3 = (NodeList) xPath.evaluate( "/kml/Placemark/gx:Track", mDocument, XPathConstants.NODESET );
    Log.d( "TEST-RESULT3", String.valueOf( result3.getLength() ) );
}

The test() method executes 3 XPath statements/patterns and is called once for each of two test documents. The 2 documents are constructed using different methods but the contents should be identical. However, the results I get from the 3 XPath statements are different.

These are the results with document 1:

2018-11-17 17:51:28.289 22837-22837/ca.csdesigninc.offroadtracker D/TEST-XML: <?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Placemark><gx:Track><gx:coord>-122.207881 37.371915 156.000000</gx:coord></gx:Track></Placemark></kml>
2018-11-17 17:51:28.324 22837-22837/ca.csdesigninc.offroadtracker D/TEST-RESULT1: 1
2018-11-17 17:51:28.334 22837-22837/ca.csdesigninc.offroadtracker D/TEST-RESULT2: 1
2018-11-17 17:51:28.343 22837-22837/ca.csdesigninc.offroadtracker D/TEST-RESULT3: 0

and these are the results with document 2:

2018-11-17 17:51:28.348 22837-22837/ca.csdesigninc.offroadtracker D/TEST-XML: <?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Placemark><gx:Track><gx:coord>-122.207881 37.371915 156.000000</gx:coord></gx:Track></Placemark></kml>
2018-11-17 17:51:28.358 22837-22837/ca.csdesigninc.offroadtracker D/TEST-RESULT1: 0
2018-11-17 17:51:28.363 22837-22837/ca.csdesigninc.offroadtracker D/TEST-RESULT2: 0
2018-11-17 17:51:28.372 22837-22837/ca.csdesigninc.offroadtracker D/TEST-RESULT3: 0

There are at least 2 problems:

  1. since the 2 documents are identical (I think), why are the results of the tests different? (i.e., the first 2 XPath statements succeed with document 1 but neither succeeds with document 2.)

  2. and why does the 3rd XPath statement fail to find the gx:Track element in both document 1 and document 2?

    UPDATE: This problem seems to have something to do with having

    xmlns="http://www.opengis.net/kml/2.2"
    

    included in document 2. If I remove it, the results of the first 2 XPath tests are the correct (for both documents) – and in fact XPath test 3 now works on document 2. Unfortunately, I still don’t have a handle on this behavior.

I’m probably missing something obvious and would appreciate any help.

Answer

The differences are due to namespaces. Both in how the XML is being produced, and when you are selecting content in the XPath.

Unfortunately, it is difficult to see the difference because the XML that happens to be serialized by the toXmlString() for testDoc1() doesn’t exactly match the state of the in-memory document.

When you construct the kml element, using createElement() it creates an element that is bound to the “no namespace”. Then, you added namespace attributes, which happen to come out when serializing with toXmlString() and make the kml element appear to be in the http://www.opengis.net/kml/2.2 namespace.

If you were to marshal that XML back to a new Document object, the kml element would be bound to that namespace. However, the current in-memory object for that element is not.

You can observe this by adding some additional diagnostics println messages:

NodeList result1 = (NodeList) xPath.evaluate("/kml", mDocument, XPathConstants.NODESET);
System.out.println(String.valueOf(result1.getLength()));
System.out.println("Namespace URI: " + result1.item(0).getNamespaceURI());
System.out.println("Prefix: " + result1.item(0).getPrefix());

You can round-trip your XML and observe that it behaves different when you marshall the serialized XML:

private void test(Document mDocument) throws Exception {
  String xml = toXmlString(mDocument);
  System.out.println( xml);
  DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
  documentBuilderFactory.setNamespaceAware(true);
  mDocument = documentBuilderFactory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));

However, that’s cheating. What you really want to do is ensure that the elements are created properly in the first place. When you create an element that you want to be bound to a namespace, use the createElementNS() method, as indicated in the JavaDoc comments for createElement():

To create an element with a qualified name and namespace URI, use the createElementNS method.

So, to create an element that is bound to the http://www.opengis.net/kml/2.2 namespace, you would want to use:

Element mKmlElement = mDocument.createElementNS("http://www.opengis.net/kml/2.2", "kml"); 

and:

Element mKmlElement = mDocument.createElementNS("http://www.opengis.net/kml/2.2", "Placemark");

and the same goes for the gx:Track element:

Element gxTrackElement = mDocument.createElementNS("http://www.google.com/kml/ext/2.2","gx:Track");

Once you get your Document objects truly equal and correct, you then need to adjust your XPath.

With XPath, if you don’t apply a namespace prefix, it will select elements bound to the “no namespace”. So, /kml will only select kml elements that are not bound to a namespace. But since your kml elements are bound to the http://www.opengis.net/kml/2.2 namespace, it won’t select them.

In your override of the getNamespaceURI() function, you could reserve gx for the Google KML Extension namespace, and then default any other namespace-prefix to resolve to http://www.opengis.net/kml/2.2:

@Override
public String getNamespaceURI(String prefix) {
  return "gx".equals(prefix) ? "http://www.google.com/kml/ext/2.2" : "http://www.opengis.net/kml/2.2";
}

Then, adjust your XPath statements to use a prefix for those KML elements. If you use the above code, it doesn’t matter what prefix you use. Anything other than gx will return the http://www.opengis.net/kml/2.2 namespace.

NodeList result1 = (NodeList) xPath.evaluate("/k:kml", mDocument, XPathConstants.NODESET);
System.out.println(String.valueOf(result1.getLength()));
System.out.println("Namespace URI: " + result1.item(0).getNamespaceURI());
System.out.println("Prefix: " + result1.item(0).getPrefix());

NodeList result2 = (NodeList) xPath.evaluate("/k:kml/k:Placemark", mDocument, XPathConstants.NODESET);
System.out.println( String.valueOf(result2.getLength()));
NodeList result3 = (NodeList) xPath.evaluate("/k:kml/k:Placemark/gx:Track", mDocument, XPathConstants.NODESET);
System.out.println(String.valueOf(result3.getLength()));

Putting it all together:

public App() {
  super();
  try {
    test( testDoc1() );
    test( testDoc2() );
  } catch( Exception e ) {
    e.printStackTrace();
  } finally {
    Log.d( "TEST-FINISHED", "test is finished" );
  }
}
private String toXmlString(Document document) throws TransformerException {
  DOMSource domSource = new DOMSource(document);
  StringWriter writer = new StringWriter();
  StreamResult result = new StreamResult(writer);
  TransformerFactory tf = TransformerFactory.newInstance();
  Transformer transformer = tf.newTransformer();
  transformer.transform(domSource, result);
  return writer.toString();
}

private Document testDoc1() throws ParserConfigurationException {
  DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
  documentBuilderFactory.setNamespaceAware(true);
  Document mDocument = documentBuilderFactory.newDocumentBuilder().newDocument();

  String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
  //Element mKmlElement = mDocument.createElement("kml");
  Element mKmlElement = mDocument.createElementNS("http://www.opengis.net/kml/2.2", "kml");
  //mKmlElement.setAttributeNS(XMLNS_NAMESPACE_URI, "xmlns", "http://www.opengis.net/kml/2.2");
  mKmlElement.setAttributeNS(XMLNS_NAMESPACE_URI, "xmlns:gx", "http://www.google.com/kml/ext/2.2");
  mDocument.appendChild(mKmlElement);


  //Element mPlacemarkElement = mDocument.createElement("Placemark");
  Element mPlacemarkElement = mDocument.createElementNS("http://www.opengis.net/kml/2.2", "Placemark");
  //mPlacemarkElement.setAttributeNS(XMLNS_NAMESPACE_URI, "xmlns", "http://www.opengis.net/kml/2.2");
  mKmlElement.appendChild(mPlacemarkElement);

  //Element gxTrackElement = mDocument.createElement("gx:Track");
  Element gxTrackElement = mDocument.createElementNS("http://www.google.com/kml/ext/2.2","gx:Track");
  mPlacemarkElement.appendChild(gxTrackElement);

  //Element gxCoordElement = mDocument.createElement("gx:coord");
  Element gxCoordElement = mDocument.createElementNS("http://www.google.com/kml/ext/2.2", "gx:coord");
  gxCoordElement.setTextContent("-122.207881 37.371915 156.000000");
  gxTrackElement.appendChild(gxCoordElement);

  return mDocument;
}

private Document testDoc2() throws ParserConfigurationException, IOException, SAXException {
  String kmlString = "<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Placemark><gx:Track><gx:coord>-122.207881 37.371915 156.000000</gx:coord></gx:Track></Placemark></kml>";

  DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
  documentBuilderFactory.setNamespaceAware(true);
  Document mDocument = documentBuilderFactory.newDocumentBuilder().parse(new 
  InputSource(new StringReader(kmlString)));

  return mDocument;
}

private void test(Document mDocument) throws Exception {
  String xml = toXmlString(mDocument);
  System.out.println( xml);

  XPath xPath = XPathFactory.newInstance().newXPath();

  xPath.setNamespaceContext(new NamespaceContext() {
    @Override
    public String getNamespaceURI(String prefix) {
      return "gx".equals(prefix) ? "http://www.google.com/kml/ext/2.2" : "http://www.opengis.net/kml/2.2";
    }

    @Override
    public String getPrefix(String namespaceURI) {
      if ("http://www.google.com/kml/ext/2.2".equals(namespaceURI)) {
        return "gx";
      }
      return null;
    }

    @Override
    public Iterator getPrefixes(String namespaceURI) {
      List<String> ns = new ArrayList<>();
      ns.add("gx");
      return ns.iterator();
    }
  });

  NodeList result1 = (NodeList) xPath.evaluate("/k:kml", mDocument, XPathConstants.NODESET);
  System.out.println(String.valueOf(result1.getLength()));
  System.out.println("Namespace URI: " + result1.item(0).getNamespaceURI());
  System.out.println("Prefix: " + result1.item(0).getPrefix());

  NodeList result2 = (NodeList) xPath.evaluate("/k:kml/k:Placemark", mDocument, XPathConstants.NODESET);
  System.out.println( String.valueOf(result2.getLength()));
  NodeList result3 = (NodeList) xPath.evaluate("/k:kml/k:Placemark/gx:Track", mDocument, XPathConstants.NODESET);
  System.out.println(String.valueOf(result3.getLength()));

}

Leave a Reply

Your email address will not be published. Required fields are marked *