Tutorial: consuming Google REST services with Spring

18 minuti di lettura
This is a simple tutorial on how to have your Spring-enabled application to interact with Google services. I'll focus on GoogleMaps, but you'll be able to use the same idea for every REST service. Also, I'll show you how to have an object oriented approach using Castor to unmarshal the response.

First, have a look at the GoogleMaps service here.

A typical XML response you can retrieve from the service, looks like that

[xml]
<?xml version="1.0" encoding="UTF-8"?>
<GeocodeResponse>
<status>OK</status>
<result>
<type>street_address</type>
<formatted_address>1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA</formatted_address>
<address_component>
<long_name>1600</long_name>
<short_name>1600</short_name>
<type>street_number</type>
</address_component>
<address_component>
<long_name>Amphitheatre Pkwy</long_name>
<short_name>Amphitheatre Pkwy</short_name>
<type>route</type>
</address_component>
<address_component>
<long_name>Mountain View</long_name>
<short_name>Mountain View</short_name>
<type>locality</type>
<type>political</type>
</address_component>
<address_component>
<long_name>San Jose</long_name>
<short_name>San Jose</short_name>
<type>administrative_area_level_3</type>
<type>political</type>
</address_component>
<address_component>
<long_name>Santa Clara</long_name>
<short_name>Santa Clara</short_name>
<type>administrative_area_level_2</type>
<type>political</type>
</address_component>
<address_component>
<long_name>California</long_name>
<short_name>CA</short_name>
<type>administrative_area_level_1</type>
<type>political</type>
</address_component>
<address_component>
<long_name>United States</long_name>
<short_name>US</short_name>
<type>country</type>
<type>political</type>
</address_component>
<address_component>
<long_name>94043</long_name>
<short_name>94043</short_name>
<type>postal_code</type>
</address_component>
<geometry>
<location>
<lat>37.4227820</lat>
<lng>-122.0850990</lng>
</location>
<location_type>ROOFTOP</location_type>
<viewport>
<southwest>
<lat>37.4196344</lat>
<lng>-122.0882466</lng>
</southwest>
<northeast>
<lat>37.4259296</lat>
<lng>-122.0819514</lng>
</northeast>
</viewport>
</geometry>
</result>
</GeocodeResponse>[/xml]

You can refere to the Google documentation at the link provided before for a complete description of this XML document.

Now the question is how to call this service from within your java code and how to handle the result.

Let's proceed with the first problem. SpringWS provides a very useful and ready-to-use class to interact with REST services, RestTemplate. You can refer to the online documentation at this page.

So to consume the service, you only need this

[java]public static void main(String[] args)
{
RestTemplate restTemplate = new RestTemplate();
Map<String, String> vars = new HashMap<String, String>();

vars.put("address", "1600 Amphitheatre Parkway, Mountain View, CA");
vars.put("sensor", "false");
String result = restTemplate
.getForObject(
"http://maps.googleapis.com/maps/api/geocode/xml?address={address}&sensor={sensor}",

String.class, vars);

System.out.println(result);

}[/java]

And this is done. Very simple.

Now, the second step. This is a little bit longer, but have no fear!

Look again at the XML response given by Google. It is possible to identify some entity:

  • Location: a point with lat/lon coordinates.

  • Area: a "piece" of the map defined by two Location objects: the south west corner and the north east corner (this is useful for viewport and bounds)

  • Geometry: composed by one Location, two Area and one string

  • AddressComponent: composed by three strings

  • Geocode: the description of a single result, composed by one string, a collection of strings, a collection of AddressComponent, a boolean and one Geometry

  • GecodeResponse: this describes the entire response and it is composed by a string value and a collection of Geocode objects


Given this, it's quite easy to write the code for this model

Location.java

[java]
public class Location
{
private double latitude;
private double longitude;

/**
* @return the latitude
*/
public double getLatitude()
{
return latitude;
}

/**
* @param latitude
*            the latitude to set
*/
public void setLatitude(double latitude)
{
this.latitude = latitude;
}

/**
* @return the longitude
*/
public double getLongitude()
{
return longitude;
}

/**
* @param longitude
*            the longitude to set
*/
public void setLongitude(double longitude)
{
this.longitude = longitude;
}

private String latitudeString(){
double lat = getLatitude();
String dir = "N";

if(lat<0){
dir = "S";
lat *= -1;
}

return "" + lat + "°" + dir;
}

private String longitudeString(){
double lng = getLongitude();
String dir = "E";

if(lng<0){
dir = "W";
lng *= -1;
}

return "" + lng + "°" + dir;
}

/**
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return latitudeString() + ", " + longitudeString();
}
}
[/java]

Area.java

[java]
public class Area
{
private Location southWest;
private Location northEast;

/**
* @return the southWest
*/
public Location getSouthWest()
{
return southWest;
}

/**
* @param southWest
*            the southWest to set
*/
public void setSouthWest(Location southWest)
{
this.southWest = southWest;
}

/**
* @return the northEast
*/
public Location getNorthEast()
{
return northEast;
}

/**
* @param northEast
*            the northEast to set
*/
public void setNorthEast(Location northEast)
{
this.northEast = northEast;
}

/**
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
String s = "";
s += "SW:" + getSouthWest().toString() +
" NE:" + getNorthEast().toString();
return s;
}
}
[/java]

Geometry.java

[java]
public class Geometry
{
private Location location;
private String locationType;
private Area viewport;
private Area bounds;

/**
* @return the location
*/
public Location getLocation()
{
return location;
}

/**
* @param location
*            the location to set
*/
public void setLocation(Location location)
{
this.location = location;
}

/**
* @return the locationType
*/
public String getLocationType()
{
return locationType;
}

/**
* @param locationType
*            the locationType to set
*/
public void setLocationType(String locationType)
{
this.locationType = locationType;
}

/**
* @return the viewport
*/
public Area getViewport()
{
return viewport;
}

/**
* @param viewport
*            the viewport to set
*/
public void setViewport(Area viewport)
{
this.viewport = viewport;
}

/**
* @return the bounds
*/
public Area getBounds()
{
return bounds;
}

/**
* @param bounds
*            the bounds to set
*/
public void setBounds(Area bounds)
{
this.bounds = bounds;
}

/**
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
String s = "";
s += "LOC:" + getLocation().toString() + " [" + getLocationType()
+ "]\n";

if (getViewport() != null)
s += "VIEWPORT:" + getViewport().toString() + "\n";

if (getBounds() != null)
s += "BOUNDS:" + getBounds().toString() + "\n";

return s;
}
}

[/java]

AddressComponent.java

[java]
public class AddressComponent
{
private String longName;
private String shortName;
private Collection<String> types = new ArrayList<String>();

/**
* @return the longName
*/
public String getLongName()
{
return longName;
}

/**
* @param longName
*            the longName to set
*/
public void setLongName(String longName)
{
this.longName = longName;
}

/**
* @return the shortName
*/
public String getShortName()
{
return shortName;
}

/**
* @param shortName
*            the shortName to set
*/
public void setShortName(String shortName)
{
this.shortName = shortName;
}

/**
* @return the types
*/
public Collection<String> getTypes()
{
return types;
}

/**
* @param types
*            the types to set
*/
public void setTypes(Collection<String> types)
{
this.types = types;
}

/**
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
String s = "";

s+="Short name: " + getShortName() + "\n";
s+="Long name: " + getLongName() + "\n";
s+="Types: ";
int i = 0;
for (String t : getTypes())
{
if(i>0)
s+=", ";
s+=t;
i++;
}
s+="\n";

return s;
}
}[/java]

Geocode.java

[java]
public class Geocode
{
private Collection<String> types = new ArrayList<String>();
private String formattedAddress;
private Collection<AddressComponent> addressComponents = new ArrayList<AddressComponent>();
private Geometry geometry;
private boolean partialMatch;

/**
* @return the types
*/
public Collection<String> getTypes()
{
return types;
}

/**
* @param types
*            the types to set
*/
public void setTypes(Collection<String> types)
{
this.types = types;
}

/**
* @return the formattedAddress
*/
public String getFormattedAddress()
{
return formattedAddress;
}

/**
* @param formattedAddress
*            the formattedAddress to set
*/
public void setFormattedAddress(String formattedAddress)
{
this.formattedAddress = formattedAddress;
}

/**
* @return the addressComponents
*/
public Collection<AddressComponent> getAddressComponents()
{
return addressComponents;
}

/**
* @param addressComponents
*            the addressComponents to set
*/
public void setAddressComponents(
Collection<AddressComponent> addressComponents)
{
this.addressComponents = addressComponents;
}

/**
* @return the geometry
*/
public Geometry getGeometry()
{
return geometry;
}

/**
* @param geometry
*            the geometry to set
*/
public void setGeometry(Geometry geometry)
{
this.geometry = geometry;
}

/**
* @return the partialMatch
*/
public boolean isPartialMatch()
{
return partialMatch;
}

/**
* @param partialMatch
*            the partialMatch to set
*/
public void setPartialMatch(boolean partialMatch)
{
this.partialMatch = partialMatch;
}

/**
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
String s = "";

s += getFormattedAddress() + "\n";

s+="Types: ";
int i = 0;
for (String t : getTypes())
{
if(i>0)
s+=", ";
s+=t;
i++;
}

s+="\n\n";

i=0;
for (AddressComponent component : getAddressComponents())
{
if(i>0)
s+="\n";
s+=component.toString();
i++;
}

s+="\n";

s+=getGeometry().toString();

s+="PARTIAL: " + ( isPartialMatch() ? "true" : "false" );

s+="\n";
return s;
}
}[/java]

GeocodeResponse.java

[java]
public class GeocodeResponse
{
private String status;
private Collection<Geocode> geocodes = new ArrayList<Geocode>();

/**
* @return the status
*/
public String getStatus()
{
return status;
}

/**
* @param status
*            the status to set
*/
public void setStatus(String status)
{
this.status = status;
}

/**
* @return the geocodes
*/
public Collection<Geocode> getGeocodes()
{
return geocodes;
}

/**
* @param geocodes the geocodes to set
*/
public void setGeocodes(Collection<Geocode> geocodes)
{
this.geocodes = geocodes;
}
}[/java]

Now we have our object model, but still we need something to translate XML to an object graph. Castor is very helpful for this kind of purpose. To set Castor up I'll use spring-beans.

In order to do so, what we need first is the XML mapping for our objects. We need it to instruct Castor on how to deal with them. let's create the file geocode.xml in the same package as our App.java file. This file will look like this

[xml]<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "http://castor.org/mapping.dtd">

<mapping>

<class name="it.innove.google.Location" auto-complete="false">
<map-to xml="Location" />
<field name="latitude" type="double">
<bind-xml name="lat" node="element" />
</field>
<field name="longitude" type="double">
<bind-xml name="lng" node="element" />
</field>
</class>

<class name="it.innove.google.Area" auto-complete="false">
<map-to xml="Area" />
<field name="southWest" type="it.innove.google.Location">
<bind-xml name="southwest" node="element" />
</field>
<field name="northEast" type="it.innove.google.Location">
<bind-xml name="northeast" node="element" />
</field>
</class>

<class name="it.innove.google.Geometry" auto-complete="false">
<map-to xml="Geometry" />
<field name="locationType" type="string">
<bind-xml name="location_type" node="element" />
</field>
<field name="location" type="it.innove.google.Location">
<bind-xml name="location" node="element" />
</field>
<field name="viewport" type="it.innove.google.Area">
<bind-xml name="viewport" node="element" />
</field>
<field name="bounds" type="it.innove.google.Area">
<bind-xml name="bounds" node="element" />
</field>
</class>

<class name="it.innove.google.AddressComponent" auto-complete="false">
<map-to xml="AddressComponent" />
<field name="longName" type="string">
<bind-xml name="long_name" node="element" />
</field>
<field name="shortName" type="string">
<bind-xml name="short_name" node="element" />
</field>
<field name="types" type="string" collection="arraylist">
<bind-xml name="type" node="element" />
</field>
</class>

<class name="it.innove.google.Geocode" auto-complete="false">
<map-to xml="Geocode" />
<field name="formattedAddress" type="string">
<bind-xml name="formatted_address" node="element" />
</field>
<field name="types" type="string" collection="arraylist">
<bind-xml name="type" node="element" />
</field>
<field name="addressComponents" type="it.innove.google.AddressComponent"
collection="arraylist">
<bind-xml name="address_component" node="element" />
</field>
<field name="geometry" type="it.innove.google.Geometry">
<bind-xml name="geometry" node="element" />
</field>
<field name="partialMatch" type="boolean">
<bind-xml name="partial_match" node="element" />
</field>
</class>

<class name="it.innove.google.GeocodeResponse" auto-complete="false">
<map-to xml="GeocodeResponse" />
<field name="status" type="string">
<bind-xml name="status" node="element" />
</field>
<field name="geocodes" type="it.innove.google.Geocode" collection="arraylist">
<bind-xml name="result" node="element" />
</field>
</class>

</mapping>[/xml]

As you can see, in this file we are merely copying the structure of our objects describing it in XML tags.

Then, we need to setup our application context creating an XML file in our package (the same package as our App.java). Our application-context.xml will look like this

[xml]<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<bean id="marshaller"
scope="singleton">
<property name="mappingLocations">
<list>
<value>classpath:it/innove/google/geocode.xml
</value>
</list>
</property>
</bean>
</beans>[/xml]

Now we only need to modify our main method like this:

[java]public static void main(String[] args) throws XmlMappingException ,
IOException
{
RestTemplate restTemplate = new RestTemplate();
Map<String, String> vars = new HashMap<String, String>();

vars.put("address", "1600 Amphitheatre Parkway, Mountain View, CA");
vars.put("sensor", "false");
String result = restTemplate
.getForObject(
"http://maps.googleapis.com/maps/api/geocode/xml?address={address}&sensor={sensor}",
String.class, vars);

System.out.println(result);

ApplicationContext ctx = new ClassPathXmlApplicationContext(
"classpath:it/innove/google/application-context.xml");

CastorMarshaller marshaller = ctx.getBean("marshaller",
CastorMarshaller.class);

StreamSource s = new StreamSource(new StringReader(result));
GeocodeResponse response = (GeocodeResponse) marshaller.unmarshal(s);

for (Geocode code : response.getGeocodes())
{
System.out.println("----------------------------------------");
System.out.println(code);
System.out.println("----------------------------------------");
}
}

[/java]

That's all!

As you can see, it is quite simple and straight-forward, even though to have an object-oriented approach you will need to study the XML responses and try to model them in a java class library.

Also, Castor in not the only choice to unmarshal responses, but I'm used to it much more than other libraries.

I hope you'll find useful

Bye,

Ste