RESTEasy Client Framework (RESTful Web Services Client) – A Tutorial

Around two years back I had posted a blog post on RESTful Web Services using JBoss RESTEasy framework. That blog post was focused on the server side of RESTful Web Services. It explained how to expose your business functionality as RESTful Web Services to the consumer community, i.e. act as a Service Provider. This post intends to look at the other side of the RESTful Web Service contract, the client side. How does a service consumer utilize a RESTful Web Service interface? What facilities does the JBoss RESTEasy framework provide to support service consumers? I found the RESTEasy Client Framework documentation a little lacking so decided put a small tutorial together. The examples below are implemented using RESTEasy 2.2.2 GA version.

First, I need to setup a service provider which will serve me consumable RESTful Web Services. I selected RESTful Web Services API provided by geonames.org. The site provides detailed documentation(http://www.geonames.org/export/web-services.html) along with sample URLs for consumption. The sample URLs are:

http://api.geonames.org/postalCodeSearch?postalcode=24501&maxRows=10&username=demo
http://api.geonames.org/postalCodeSearchJSON?postalcode=9011&maxRows=10&username=demo

I have used the demo user. This user has a daily user limit. So I suggest you create your own user. GeoNames offers free services for a defined daily limit. It also provides it’s own client libraries; for the purpose of this tutorial I have chosen to ignore these libraries. The Geonames API supports message payload delivery in XML and JSON format. We will have a look at both types of payload. First let’s look at XML payload.

Here’s the sample URL for XML payload. I am searching for postal code 24501 with a limit of retrieving max. 10 results.

http://api.geonames.org/postalCodeSearch?postalcode=24501&maxRows=10&username=demo

The XML payload is as below:

<geonames>
	<totalResultsCount>4</totalResultsCount>
	<code>
		<postalcode>24501</postalcode>
		<name>Lynchburg</name>
		<countryCode>US</countryCode>
		<lat>37.38311</lat>
		<lng>-79.17833</lng>
		<adminCode1>VA</adminCode1>
		<adminName1>Virginia</adminName1>
		<adminCode2>680</adminCode2>
		<adminName2>Lynchburg (city)</adminName2>
		<adminCode3/><adminName3/>
	</code>
	<code>
		<postalcode>24501</postalcode>
		<name>Rasoolabad</name>
		<countryCode>PK</countryCode>
		<lat>34.18583</lat>
		<lng>71.76333</lng>
		<adminCode1/>
		<adminName1>NWFP Peshawar</adminName1>
		<adminCode2/><adminName2/>
		<adminCode3/><adminName3/>
	</code>
	<code>
		<postalcode>24501</postalcode>
		<name>Chitral Karoona</name>
		<countryCode>PK</countryCode>
		<lat>34.18583</lat>
		<lng>71.76333</lng>
		<adminCode1/>
		<adminName1>NWFP Peshawar</adminName1>
		<adminCode2/><adminName2/>
		<adminCode3/><adminName3/>
	</code>
	<code>
		<postalcode>24501</postalcode>
		<name>Staffanstorp</name>
		<countryCode>SE</countryCode>
		<lat>55.64277</lat>
		<lng>13.20638</lng>
		<adminCode1>M</adminCode1>
		<adminName1>Skåne</adminName1>
		<adminCode2>1230</adminCode2>
		<adminName2>Staffanstorp</adminName2>
		<adminCode3/>
		<adminName3/>
	</code>
</geonames>

The RESTEasy framework is a Java based framework. Therefore, the developer will be interested in working with POJOs (Plain Old Java Object) and not raw XML. The RESTEasy framework facilitates this by using built-in JAXB (Java API for XML Binding) features. To match the XML payload structure I am creating two Java classes GeoNames and GeoCode. The source code of the two classes is shown below:

package com.resteasy.model;

import java.util.ArrayList;

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

@XmlRootElement(name="geonames")
public class GeoNames {

	private int resultsCount = 0;
	private ArrayList<GeoCode> codes = new ArrayList<GeoCode>();

	@XmlElement(name="totalResultsCount")
	public int getResultsCount() {
		return resultsCount;
	}
	public void setResultsCount(int resultsCount) {
		this.resultsCount = resultsCount;
	}

	@XmlElement(name="code")
	public ArrayList<GeoCode> getCodes() {
		return this.codes;
	}
	public void setCode(ArrayList<GeoCode> codes) {
		this.codes = codes;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("Names....");
		sb.append("\n");
		sb.append("Results Count: ");
		sb.append(this.resultsCount);
		sb.append("\n");
		for (GeoCode code : this.codes) {
			sb.append("**********\n");
			sb.append(code);
			sb.append("**********\n");
		}

		return sb.toString();
	}
}

 

package com.resteasy.model;

import java.math.BigDecimal;

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

import org.codehaus.jackson.annotate.JsonProperty;

@XmlRootElement (name="code")
public class GeoCode {

	private int postalCode = 0;
	private String name = null;
	private String countryCode = null;
	private BigDecimal latitude = null;
	private BigDecimal longitude = null;
	private String adminCode1 = null;
	private String adminName1 = null;
	private String adminCode2 = null;
	private String adminName2 = null;
	private String adminCode3 = null;
	private String adminName3 = null;

	@XmlElement (name="postalcode")
	public int getPostalCode() {
		return postalCode;
	}
	public void setPostalCode(int postalCode) {
		this.postalCode = postalCode;
	}

	@XmlElement (name="name")
	public String getName() {
		return name;
	}

	@JsonProperty("placeName")
	public void setName(String name) {
		this.name = name;
	}

	@XmlElement (name="countryCode")
	public String getCountryCode() {
		return countryCode;
	}
	public void setCountryCode(String countryCode) {
		this.countryCode = countryCode;
	}

	@XmlElement (name="lat")
	public BigDecimal getLatitude() {
		return latitude;
	}

	@JsonProperty("lat")
	public void setLatitude(BigDecimal latitude) {
		this.latitude = latitude;
	}

	@XmlElement (name="lng")
	public BigDecimal getLongitude() {
		return longitude;
	}
	@JsonProperty("lng")
	public void setLongitude(BigDecimal longitude) {
		this.longitude = longitude;
	}

	@XmlElement (name="adminCode1")
	public String getAdminCode1() {
		return adminCode1;
	}
	public void setAdminCode1(String adminCode1) {
		this.adminCode1 = adminCode1;
	}

	@XmlElement (name="adminName1")
	public String getAdminName1() {
		return adminName1;
	}
	public void setAdminName1(String adminName1) {
		this.adminName1 = adminName1;
	}

	@XmlElement (name="adminCode2")
	public String getAdminCode2() {
		return adminCode2;
	}
	public void setAdminCode2(String adminCode2) {
		this.adminCode2 = adminCode2;
	}

	@XmlElement (name="adminName2")
	public String getAdminName2() {
		return adminName2;
	}
	public void setAdminName2(String adminName2) {
		this.adminName2 = adminName2;
	}

	@XmlElement (name="adminCode3")
	public String getAdminCode3() {
		return adminCode3;
	}
	public void setAdminCode3(String adminCode3) {
		this.adminCode3 = adminCode3;
	}

	@XmlElement (name="adminName3")
	public String getAdminName3() {
		return adminName3;
	}
	public void setAdminName3(String adminName3) {
		this.adminName3 = adminName3;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("PostalCode: ");
		sb.append(this.postalCode);
		sb.append("\n");
		sb.append("Name: ");
		sb.append(this.name);
		sb.append("\n");
		sb.append("Country Code: ");
		sb.append(this.countryCode);
		sb.append("\n");
		sb.append("Latitude: ");
		sb.append(this.latitude);
		sb.append("\n");
		sb.append("Longitude: ");
		sb.append(this.longitude);
		sb.append("\n");
		sb.append("Admin Name 1: ");
		sb.append(this.adminName1);
		sb.append("\n");
		sb.append("Admin Code 1: ");
		sb.append(this.adminCode1);
		sb.append("\n");
		sb.append("Admin Name 2: ");
		sb.append(this.adminName2);
		sb.append("\n");
		sb.append("Admin Code 2: ");
		sb.append(this.adminCode2);
		sb.append("\n");
		sb.append("Admin Name 3: ");
		sb.append(this.adminName3);
		sb.append("\n");
		sb.append("Admin Code 3: ");
		sb.append(this.adminCode1);
		sb.append("\n");

		return sb.toString();
	}
}

I have used annotations to achieve XML binding between the payload and Java classes. Detailed explanation of the JAXB annotation is outside the purview of this blog post. However the annotation names are self-explanatory. There are some JSON annotations in GeoCode, however our current focus is only on XML annotations i.e. annotations whose names are prefixed by “Xml”. In case there is a difference between the XML element and Class property use the name variable in the XMLElement annotation to map the XML element name to the class property. For e.g. refer the following annotation for latitude property in GeoCode class.

	@XmlElement (name="lat")
	public BigDecimal getLatitude() {
		return latitude;
	}

The next step is to create an interface which will act as a Proxy for communicating with the Geonames API. Here’s the source code of the proxy GeoNamesService. RESTful Web Services specific annotations are used. For explanation refer to my previous blog post of RESTful Web Services.

package com.resteasy.client;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.jboss.resteasy.client.ClientResponse;

import com.resteasy.model.GeoNames;
import com.resteasy.model.PostalCodesJSON;

@Path("")
public interface GeoNamesService {

	@GET
	@Produces("application/xml")
	ClientResponse<GeoNames> getPostalCodeInformation(@QueryParam("postalcode") int postalCode,
			@QueryParam("maxRows") int rows, @QueryParam("username") String userName);

	@GET
	@Produces("application/xml")
	ClientResponse<GeoNames> getPostalCodes();

	@GET
	@Produces("application/json")
	ClientResponse<PostalCodesJSON> getPostalCodesinJSON(@QueryParam("postalcode") int postalCode,
			@QueryParam("maxRows") int rows, @QueryParam("username") String userName);

}

I have defined three methods, a parameterized getPostalCodeInformation, getPostalCodes and getPostalCodesinJSON. The first two handle XML payload and the last one is for JSON. Let’s focus on the first two. The methods are expected to return the ClientResponse class with a type variable aligned to the XML payload. Therefore for the XML payload we have defined the type as GeoNames, a class whose structure has been aligned to the XML structure. The second method getPostalCodes does not accept any argument.

	@GET
	@Produces("application/xml")
	ClientResponse<GeoNames> getPostalCodes();

Update(2nd Jan 2012):The following libraries have been included in the classpath for RESTfulWSClientJSON:

  • commons-codec-1.2.jar
  • commons-httpclient-3.1.jar
  • commons-logging-1.0.4.jar
  • jackson-core-asl-1.6.3.jar
  • jackson-jaxrs-1.6.3.jar
  • jackson-mapper-asl-1.6.3.jar
  • resteasy-jackson-provider-2.2.2.GA.jar
  • resteasy-jaxrs-2.2.2.GA.jar
  • jaxrs-api-2.2.2.GA.jar

The following libraries are needed for the other two test classes.

  • commons-codec-1.2.jar
  • commons-httpclient-3.1.jar
  • commons-logging-1.0.4.jar
  • resteasy-jaxrs-2.2.2.GA.jar
  • jaxrs-api-2.2.2.GA.jar
  • jaxb-impl-2.2.4.jar
  • resteasy-jaxb-provider-2.2.2.GA.jar

Please note that GeoCode is a shared class for all the clients. Therefore comment out the JsonProperty related code when using these two client classes.

End Update
Now let’s take it for a test run. Here’s the source code of the test class RESTFulWSClientNoParam.

package com.resteasy.client;

import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.ProxyFactory;

import com.resteasy.model.GeoNames;

public class RESTFulWSClientNoParam {

	private static final String GEONAMES_URL = "http://api.geonames.org/postalCodeSearch?" +
			"postalcode=24501&maxRows=10&username=demo";

	public static void main(String[] args) {

		GeoNamesService service = ProxyFactory.create(GeoNamesService.class, GEONAMES_URL);
		ClientResponse<GeoNames> response = service.getPostalCodes();

		if (response.getStatus() == 200 ) {
			GeoNames names = response.getEntity();
			System.out.println(names);

		} else {
			System.out.println("Request processing failed. HTTP Status: " +        response.getStatus()
			+ " " + response.toString());
		}
		response.releaseConnection();

	}
}

The test class run output is

Names....
Results Count: 4
**********
PostalCode: 24501
Name: Lynchburg
Country Code: US
Latitude: 37.38311
Longitude: -79.17833
Admin Name 1: Virginia
Admin Code 1: VA
Admin Name 2: Lynchburg (city)
Admin Code 2: 680
Admin Name 3:
Admin Code 3: VA
**********
**********
PostalCode: 24501
Name: Rasoolabad
Country Code: PK
Latitude: 34.18583
Longitude: 71.76333
Admin Name 1: NWFP Peshawar
Admin Code 1:
Admin Name 2:
Admin Code 2:
Admin Name 3:
Admin Code 3:
**********
**********
PostalCode: 24501
Name: Chitral Karoona
Country Code: PK
Latitude: 34.18583
Longitude: 71.76333
Admin Name 1: NWFP Peshawar
Admin Code 1:
Admin Name 2:
Admin Code 2:
Admin Name 3:
Admin Code 3:
**********
**********
PostalCode: 24501
Name: Staffanstorp
Country Code: SE
Latitude: 55.64277
Longitude: 13.20638
Admin Name 1: Skåne
Admin Code 1: M
Admin Name 2: Staffanstorp
Admin Code 2: 1230
Admin Name 3:
Admin Code 3: M
**********

The RESTful Web Services client class ProxyFactory creates the GeoNamesService interface implementation proxy using the create method. The method takes two arguments, one is the Client Proxy class and second is the string URL of the RESTful Web Service. The ClientResponse class provides getStatus method which maps to HTTP status code. The rest of the code is self-explanatory.

As we are using the complete URL including the query parameter the Path annotation (line number 13) in the client proxy interface GeoNamesService is provided a value “” instead of the usual “/”.

Typically RESTful Web Services API accept arguments. To illustrate parameterized RESTful Web Service APIs look at the method getPostalCodeInformation in GeoNamesService interface.

	@GET
	@Produces("application/xml")
	ClientResponse<GeoNames> getPostalCodeInformation(@QueryParam("postalcode") int postalCode,
			@QueryParam("maxRows") int rows, @QueryParam("username") String userName);

The method getPostalCodeInformation accepts three arguments namely postal code, max. rows and user name. Here’s the test class RESTFulWSClient.

package com.resteasy.client;

import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.ProxyFactory;

import com.resteasy.model.GeoNames;

public class RESTFulWSClient {

	private static final String GEONAMES_URL = "http://api.geonames.org/postalCodeSearch";

	public static void main(String[] args) {

		GeoNamesService service = ProxyFactory.create(GeoNamesService.class, GEONAMES_URL);
		ClientResponse<GeoNames> response = service.getPostalCodeInformation(24501, 10, "demo");

		if (response.getStatus() == 200 ) {
			GeoNames names = response.getEntity();
			System.out.println(names);
		} else {
			System.out.println("Request processing failed. HTTP Status: " + response.getStatus()
			+ " " + response.getLocation());
		}
		response.releaseConnection();

	}
}

The output of the test class is similar to the earlier output.

So far we have covered the XML payload RESTEasy Client processing. Let’s have a look at the JSON option.
Here’s the JSON URL:

http://api.geonames.org/postalCodeSearchJSON?postalcode=24501&maxRows=10&username=demo

Here’s the JSON output:

{"postalCodes":[{"adminName2":"Lynchburg (city)","adminCode2":"680","adminCode1":"VA","postalCode":"24501","countryCode":"US",
"lng":-79.178326,"placeName":"Lynchburg","lat":37.383112,"adminName1":"Virginia"},{"postalCode":"24501","countryCode":"PK","lng":71.7633333,"placeName":"Rasoolabad","lat":34.1858333,
"adminName1":"NWFP Peshawar"},{"postalCode":"24501","countryCode":"PK","lng":71.7633333,"placeName":"Chitral Karoona","lat":34.1858333,"adminName1":"NWFP Peshawar"},{"adminName2":"Staffanstorp","adminCode2":"1230","adminCode1":"M","postalCode":"24501",
"countryCode":"SE","lng":13.206381797790527,"placeName":"Staffanstorp","lat":55.642772521210716,
"adminName1":"Skåne"}]}

The structure of the JSON payload is slightly different from the XML payload. Therefore I have defined a new Class PostalCodesJSON. This class continues to reuse GeoCode class.

package com.resteasy.model;

import java.util.ArrayList;
import java.util.List;

public class PostalCodesJSON {
	
	public List<GeoCode> postalCodes = new ArrayList<GeoCode>();
		
	public List<GeoCode> getCodes() {
		return this.postalCodes;
	}
	public void setCode(List<GeoCode> codes) {
		this.postalCodes = codes;
	}
	
	@Override 
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("Names....");
		sb.append("\n");
		sb.append("\n");
		for (GeoCode code : this.postalCodes) {
			sb.append("**********\n");
			sb.append(code);
			sb.append("**********\n");
		}
		
		return sb.toString();
	}	
}

Note that we have defined GeoCode as a List in the PostalCodesJSON class. They can also be defined as a GeoCode array. The rest of the code remains unchanged.

Refer to the third method defined in getPostalCodesinJSON. It accepts three arguments similar to the getPostalCodeInformation method.

	@GET
	@Produces("application/json")
	ClientResponse<PostalCodesJSON> getPostalCodesinJSON(@QueryParam("postalcode") int postalCode,
			@QueryParam("maxRows") int rows, @QueryParam("username") String userName);

In case there are differences in the JSON property name and corresponding Class property name, use the JsonProperty annotation to map the JSON property to class property. For example, refer latitude property in GeoCode class.

	@JsonProperty("lat")
	public void setLatitude(BigDecimal latitude) {
		this.latitude = latitude;
	}

There is a reason why the JsonProperty annotation is defined on the setLatitude method. As a consuming application, we are deserializing from JSON format to POJO property. This process invokes the setter method. In case of serialization the JsonProperty would have to be defined in the getLatitude method.

Here’s the test class RESTFulWSClientJSON for JSON payload.

package com.resteasy.client;

import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.ProxyFactory;

import com.resteasy.model.PostalCodesJSON;

public class RESTFulWSClientJSON {

	private static final String GEONAMES_URL = "http://api.geonames.org/postalCodeSearchJSON";

	public static void main(String[] args) {

		GeoNamesService service = ProxyFactory.create(GeoNamesService.class, GEONAMES_URL);
		ClientResponse<PostalCodesJSON> response = service.getPostalCodesinJSON(24501, 10, "demo");

		if (response.getStatus() == 200 ) {
			PostalCodesJSON codes = response.getEntity();
			System.out.println(codes);
		} else {
			System.out.println("Request processing failed. HTTP Status: " + response.getStatus()
			+ " " + response.getLocation());
		}
		response.releaseConnection();

	}
}

Here’s the test class run output:

Names....

**********
PostalCode: 24501
Name: Lynchburg
Country Code: US
Latitude: 37.383112
Longitude: -79.178326
Admin Name 1: Virginia
Admin Code 1: VA
Admin Name 2: Lynchburg (city)
Admin Code 2: 680
Admin Name 3: null
Admin Code 3: VA
**********
**********
PostalCode: 24501
Name: Rasoolabad
Country Code: PK
Latitude: 34.1858333
Longitude: 71.7633333
Admin Name 1: NWFP Peshawar
Admin Code 1: null
Admin Name 2: null
Admin Code 2: null
Admin Name 3: null
Admin Code 3: null
**********
**********
PostalCode: 24501
Name: Chitral Karoona
Country Code: PK
Latitude: 34.1858333
Longitude: 71.7633333
Admin Name 1: NWFP Peshawar
Admin Code 1: null
Admin Name 2: null
Admin Code 2: null
Admin Name 3: null
Admin Code 3: null
**********
**********
PostalCode: 24501
Name: Staffanstorp
Country Code: SE
Latitude: 55.642772521210716
Longitude: 13.206381797790527
Admin Name 1: Skåne
Admin Code 1: M
Admin Name 2: Staffanstorp
Admin Code 2: 1230
Admin Name 3: null
Admin Code 3: M
**********

That covers the RESTEasy Client Framework tutorial.

Advertisements

17 thoughts on “RESTEasy Client Framework (RESTful Web Services Client) – A Tutorial

  1. Hi
    I get this error when I run this source code.
    Exception in thread “main” org.jboss.resteasy.client.ClientResponseFailure: Unable to find a MessageBodyReader of content-type text/xml;charset=”UTF-8″ and type class com.resteasy.model.GeoNames
    at org.jboss.resteasy.client.core.BaseClientResponse.createResponseFailure(BaseClientResponse.java:488)
    at org.jboss.resteasy.client.core.BaseClientResponse.createResponseFailure(BaseClientResponse.java:479)
    at org.jboss.resteasy.client.core.BaseClientResponse.readFrom(BaseClientResponse.java:384)
    at org.jboss.resteasy.client.core.BaseClientResponse.getEntity(BaseClientResponse.java:346)
    at org.jboss.resteasy.client.core.BaseClientResponse.getEntity(BaseClientResponse.java:307)
    at com.resteasy.client.RestEasyClient.main(RestEasyClient.java:27)
    Java Result: 1

    1. Hi Joseph,

      I ran all my test classes and they ran successfully. Maybe you are missing some dependent jars in the classpath. I have added an update dated 22nd Nov 2011 in the blog post listing all the jars in my classpath. Compare and check if you are missing any. Off hand the jackson* and resteasy* jars are critical. Hope this helps.

      1. Zied,

        The jar list is already put in the blog post. Refer the section starting with ‘Update(2nd Jan 2012)’. Hope that helps!

  2. No offense but the library is bloated and potentially conflicting. Why is there multiple providers (Jackson, Jettison, YAML, JAXB) present on the class path, the app would use only one or two. In fact, RESTEasy specifically warns against JAXB and Jackson provider conflict.
    http://docs.jboss.org/resteasy/docs/2.3.0.GA/userguide/html/json.html#Possible_Jackson_Problems
    Then there’s unused RESTEasy Spring support, transaction support…the app would work with half the jars and probably work better.

    1. Hi Passer By,

      You are correct, I did not sort out the dependency issue due to sheer laziness. Will update the post with applicable dependencies.

  3. This is an excellent tutorial for RESTEasy clients – thanks!

    One mistake in the listings is that after you say “… I have defined a new class PostalCodesJSON”, you then have the source code for RESTFulWSClientJSON instead of PostalCodesJSON.

    1. Hi Julio,
      Thanks for the words of appreciation. I have fixed the mistake in the post. Thanks for bringing it to my notice.

  4. Great tutorial, but RESTFulWSClientJSON is referenced and listed where PostalCodesJSON should be, then listed again towards the end.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s