September 13, 2010

XPath Based Mapping - Geocode Example

In a previous post I introduced MOXy's XPath based mapping JAXB extension.  In this post I'll use Google Maps Geocoding API V2 to demonstrate how powerful this extension is.


Input XML Document

The following is the sample document used on the Google Maps Geocoding API V2 page.


<?xml version="1.0" encoding="UTF-8" ?>
<kml xmlns="http://earth.google.com/kml/2.0">
    <Response>
        <name>1600 Amphitheatre Parkway, Mountain View, CA</name>
        <Status>
            <code>200</code>
            <request>geocode</request>
        </Status>
        <Placemark id="p1">
            <address>1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA
            </address>
            <AddressDetails Accuracy="8" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0">
                <Country>
                    <CountryNameCode>US</CountryNameCode>
                    <CountryName>USA</CountryName>
                    <AdministrativeArea>
                        <AdministrativeAreaName>CA</AdministrativeAreaName>
                        <SubAdministrativeArea>
                            <SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName>
                            <Locality>
                                <LocalityName>Mountain View</LocalityName>
                                <Thoroughfare>
                                    <ThoroughfareName>1600 Amphitheatre Pkwy</ThoroughfareName>
                                </Thoroughfare>
                                <PostalCode>
                                    <PostalCodeNumber>94043</PostalCodeNumber>
                                </PostalCode>
                            </Locality>
                        </SubAdministrativeArea>
                    </AdministrativeArea>
                </Country>
            </AddressDetails>
            <ExtendedData>
                <LatLonBox north="37.4251799" south="37.4188847" east="-122.0813633" west="-122.0876585" />
            </ExtendedData>
            <Point>
                <coordinates>-122.0829964,37.4217080,0</coordinates>
            </Point>
        </Placemark>
    </Response>
</kml>

Java Model - Address

In this example we will map a portion of the geocode data to a typical address object.  Here we will make use of MOXy's @XmlPath annotation.  The get/set methods have been omitted to save space. 

The namespace qualification of the XPath corresponds to the configuration in the @javax.xml.bind.annotation.XmlSchema annotation on the package-info class.  XPath fragments with out a prefix correspond to the default namespace ("http://earth.google.com/kml/2.0"), and fragments prefixed with "ns" correspond to the namespace "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0".

package blog.geocode;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import org.eclipse.persistence.oxm.annotations.XmlPath;

@XmlRootElement(name="kml")
@XmlType(propOrder={"country", "state", "city", "street", "postalCode"})
public class Address {

    @XmlPath("Response/Placemark/ns:AddressDetails/ns:Country/ns:AdministrativeArea/ns:SubAdministrativeArea/ns:Locality/ns:Thoroughfare/ns:ThoroughfareName/text()")
    private String street;

    @XmlPath("Response/Placemark/ns:AddressDetails/ns:Country/ns:AdministrativeArea/ns:SubAdministrativeArea/ns:Locality/ns:LocalityName/text()")
    private String city;

    @XmlPath("Response/Placemark/ns:AddressDetails/ns:Country/ns:AdministrativeArea/ns:AdministrativeAreaName/text()")
    private String state;

    @XmlPath("Response/Placemark/ns:AddressDetails/ns:Country/ns:CountryNameCode/text()")
    private String country;

    @XmlPath("Response/Placemark/ns:AddressDetails/ns:Country/ns:AdministrativeArea/ns:SubAdministrativeArea/ns:Locality/ns:PostalCode/ns:PostalCodeNumber/text()")
    private String postalCode;

}

Java Model - package-info

We will supply a package-info class to specify the namespace qualication, and field access.

@XmlSchema(
        namespace="http://earth.google.com/kml/2.0",
        xmlns={
                @XmlNs(prefix="ns", namespaceURI="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0")
        },
        elementFormDefault=XmlNsForm.QUALIFIED)
@XmlAccessorType(XmlAccessType.FIELD)
package blog.geocode;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

Java Model - jaxb.properties

In order to specify that we are using the MOXy JAXB implementation we need to put a file called jaxb.properties in with our Address class with the following entry: 

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Demo Code

The following code can be used to demonstrate our mapping.  In the URL creation be sure to replace the string "YOUR_KEY_HERE" with your own API key.  If you do not have one, you can create one here.

package blog.geocode;

import java.io.InputStream;
import java.net.URL;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Address.class);
        
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        URL xmlURL = new URL("http://maps.google.com/maps/geo?q=1600+Amphitheatre+Parkway,+Mountain+View,+CA&output=xml&sensor=false&key=YOUR_KEY_HERE");
        InputStream xml = xmlURL.openStream();
        Address address = (Address) unmarshaller.unmarshal(xml);
        xml.close();

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(address, System.out);
    }

}

Output

The following is the output from running our Demo class.  Note that only the portion of the document that we mapped will be output to XML.

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.0" xmlns:ns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0">
    <Response>
        <Placemark>
            <ns:AddressDetails>
                <ns:Country>
                    <ns:CountryNameCode>US</ns:CountryNameCode>
                    <ns:AdministrativeArea>
                        <ns:AdministrativeAreaName>CA</ns:AdministrativeAreaName>
                        <ns:SubAdministrativeArea>
                            <ns:Locality>
                                <ns:LocalityName>Mountain View</ns:LocalityName>
                                <ns:Thoroughfare>
                                    <ns:ThoroughfareName>1600 Amphitheatre Pkwy</ns:ThoroughfareName>
                                </ns:Thoroughfare>
                                <ns:PostalCode>
                                    <ns:PostalCodeNumber>94043</ns:PostalCodeNumber>
                                </ns:PostalCode>
                            </ns:Locality>
                        </ns:SubAdministrativeArea>
                    </ns:AdministrativeArea>
                </ns:Country>
            </ns:AddressDetails>
        </Placemark>
    </Response>
</kml> 

Further Reading:

If you enjoyed this post you may also be interested in:

11 comments:

  1. Blaise,
    Is it possible to annotate the fields (assume default namesapce)

    @XmlPath("//Point/coordinates") instead of
    @XmlPath("Response/Placemark/Point/coordinates")

    Using the "any-level descendant" syntax doesn't work as in I get values as null. Thanks for your help and excellent tutorial on moxy impl.

    ReplyDelete
  2. Hi JT,

    The reason we don't allow the "any-level" concept frpm XPath is that we require that information when marshalling to XML.

    Are you interested in this type of functionality for unmarshal only use cases?

    -Blaise

    ReplyDelete
  3. Hi,

    I'd be definitely interested in unmarshal-only use-cases.

    Zdenek

    ReplyDelete
  4. Hi Zdenek,

    Sorry for the delay in responding. I haven't had a chanve to post an example yet, but if you want to try somethings out yourself just include your XPath in the @XmlPath annotation and make the following magic call after you create your JAXBContext:
      JAXBHelper.getJAXBContext(jc).getXMLContext().getSession(0).getDatasourceLogin().setDatasourcePlatform(new DOMPlatform());

    -Blaise

    ReplyDelete
  5. This is really exciting.
    Unfourtnately I get exception:
    javax.servlet.ServletException: java.lang.NoSuchMethodError: org.eclipse.persistence.oxm.NamespaceResolver.hasPrefixesToNamespaces()Z

    ReplyDelete
    Replies
    1. Which environment are you running in? It appears as though you may be using the EclipseLink OSGi bundles, and using a new MOXy bundle and an older core bundle.

      -Blaise

      Delete
  6. This tutorial is great, However when I try to unmarshal a json string to java beans using the code mentioned above I get the following error:

    Exception in thread "main" javax.xml.bind.PropertyException: name: eclipselink.media-type value: application/json
    at javax.xml.bind.helpers.AbstractMarshallerImpl.setProperty(Unknown Source)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.setProperty(MarshallerImpl.java:533)
    at sample.TestJSON.main(TestJSON.java:19)

    I have downloaded latest eclipse link jars and have created a jaxb.properties file as well.Can you please help me resolve this problem,

    ReplyDelete
    Replies
    1. Hi Anusha,

      I have another post covering the JSON aspect of the Geocode API that you may be interested in:
      - Binding to JSON & XML - Geocode Example

      For that post I have the complete source code available on GitHub:
      - https://github.com/bdoughan/blog20110819

      -Blaise

      Delete
    2. Is this propertyException specific to geocoding? I get it too

      Delete
  7. Excellent Post..Can you please help me configure EclipseLink MoxY in Spring?

    ReplyDelete