Clients like REST part 1

September 16, 2010 § Leave a Comment

I do appreciate the versatility offered by REST-style web services when designing clients, and that’s precisely what I will be discussing in this series of posts. There is a lot to be said about a web-service approach such as REST where the client can use pretty much any HTTP-based API to retrieve/update/delete data. I will take you through the steps of defining a server-side Java class, use REST annotations to turn it into a web service and show you four ways to access such a service: Through the Apache Commons HTTP API, through the native J2SE API, through the JAX-WS API and finally through the Spring REST Client API.
This post is the first one of a mini-series:

Clients Like REST part 1 Apache HTTP Commons Client
Clients Like REST part 2 Native J2SE API Client
Clients Like REST part 3 JAX-WS client API

A few things to note:

  1. This is not about the proper design of the server-side code, I will probably devote another blog to explore best-practices in this area and comment on the use/misuse of Annotations
  2. This is not about the best protocol for data transport, again I could probably compare the merits of various data protocols for various cases/load usage/performance requirements in another blog
  3. This is about setting up four different (Java) clients to perform similar operations on the data using four different APIs, you are left to judge which one fits best your particular project
  4. The first part will illustrate a client using Apache Commons HttpClient, part 2 will illustrate a client simply using the J2SE API

Before proceeding let’s just say that in general I am not a huge fan of RESTful services. More precisely my issue with REST is the way organizations tend to implement REST services, not necessarily because of disagreements with the REST philosophy as described in Roy Fielding dissertation (I  really like chapter five). All too often REST is used as a lame excuse to expose some poorly designed functionality using XML-over-HTTP and pompously calling that “web services”. Of course, I cannot help but note that  many recent developments in REST try to precisely bridge the gap with SOAP, such as the usage of WSDL 2.0 to accommodate  contracts (see the good introduction at http://www.w3.org/TR/2007/REC-wsdl20-primer-20070626/)

Now back to the main point of this post: I will take you through the implementation of a simple car statistics web service which allows the client to invoke five operations: Get all basic statistics (top speed in mph, 0 to 60 time, 1/4 mile time and 60 to 0 stopping distance), get statistics for a particular car, add statistics for a new car, update the statistics of an existing car and delete a particular statistics. Hopefully this example will prove sufficiently sufficiently universal.

We’ll start by defining the server-side classes; as mentioned previously I will devote another blog to explore best-practices in this area, for now let’s just concentrate on the class definitions. First let’s define the main service interface describing the five operations and note that nothing in the code pertains to web services:

package com.apptotest.service;
public interface IAutoStatService {
    public AllAutoStatistics getAllAutoStatistics();

    public AutoStatistics updateAutoStatistics(AutoStatistics astats);

    public AutoStatistics addAutoStatistics(AutoStatistics astats);

    public AutoStatistics getAutoStatistics(Long id);

    public AutoStatistics deleteAutoStatistics(Long id);
}

Here is the definition of the AutoStatistics entity class, just note the presence of the @XmlRootElement(name=”AutoStatistics”) annotation. Since we’ll be using JAXB 2 in the background, the @XmlRootElement annotation in this context refers to both an XML Schema Type and a Global Element Definition of that type and the name attribute is the name of the Global Element Definition. By contrast a property or attribute of a class would be tagged with the @XmlElement annotation to indicate a Schema Element Definition. We are omitting here the namespace attribute in @XmlRootElement. Finally do note that I choose to override equals() and hashCode(), the reason will become obvious when we get at the client code towards the end of the post.

package com.apptotest.service;

import javax.xml.bind.annotation.XmlRootElement;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;

@XmlRootElement(name="AutoStatistics")
public class AutoStatistics {
    private String name;
    private Long id;
    private Float maxSpeed;
    private Float zeroToSixtyTimeInSecs;
    private Float quarterMileTimeInSecs;
    private Float sixtyToZeroDistanceInFt;

    public AutoStatistics() {}

    public AutoStatistics(String name, Long id, Float maxSpeed, Float zeroToSixtyTimeInSecs,
            Float quarterMileTimeInSecs, Float sixtyToZeroDistanceInFt) {
        this.name = name;
        this.id = id;
        this.maxSpeed = maxSpeed;
        this.zeroToSixtyTimeInSecs = zeroToSixtyTimeInSecs;
        this.quarterMileTimeInSecs = quarterMileTimeInSecs;
        this.sixtyToZeroDistanceInFt = sixtyToZeroDistanceInFt;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Float getMaxSpeed() {
        return maxSpeed;
    }
    public void setMaxSpeed(Float maxSpeed) {
        this.maxSpeed = maxSpeed;
    }
    public Float getZeroToSixtyTimeInSecs() {
        return zeroToSixtyTimeInSecs;
    }
    public void setZeroToSixtyTimeInSecs(Float zeroToSixtyTimeInSecs) {
        this.zeroToSixtyTimeInSecs = zeroToSixtyTimeInSecs;
    }
    public Float getQuarterMileTimeInSecs() {
        return quarterMileTimeInSecs;
    }
    public void setQuarterMileTimeInSecs(Float quarterMileTimeInSecs) {
        this.quarterMileTimeInSecs = quarterMileTimeInSecs;
    }
    public Float getSixtyToZeroDistanceInFt() {
        return sixtyToZeroDistanceInFt;
    }
    public void setSixtyToZeroDistanceInFt(Float sixtyToZeroDistanceInFt) {
        this.sixtyToZeroDistanceInFt = sixtyToZeroDistanceInFt;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37).
            append(name).
            append(id).
            append(maxSpeed).
            append(zeroToSixtyTimeInSecs).
            append(quarterMileTimeInSecs).
            append(sixtyToZeroDistanceInFt).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) { return false; }
        if (obj == this) { return true; }
        if (obj.getClass() != getClass()) { return false; }
        AutoStatistics rhs = (AutoStatistics) obj;
        return new EqualsBuilder().
            append(name, rhs.name).
            append(id, rhs.id).
            append(maxSpeed, rhs.maxSpeed).
            append(zeroToSixtyTimeInSecs, rhs.zeroToSixtyTimeInSecs).
            append(quarterMileTimeInSecs, rhs.quarterMileTimeInSecs).
            append(sixtyToZeroDistanceInFt, rhs.sixtyToZeroDistanceInFt).
            isEquals();
    }

    @Override
    public String toString() {
         return ToStringBuilder.reflectionToString(this);
    }
}

And here is the definition of the AllAutoStatistics entity class, note here that it provides a static snapshot of the Collection of AutoStatistics objects:

package com.apptotest.service;

import java.util.Collection;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="AllAutoStatistics")
public class AllAutoStatistics {
    private Collection allStats;

    public Collection getAllStats() {
        return allStats;
    }

    public void setAllStats(Collection allStats) {
        this.allStats = allStats;
    }
}

And here is the heart of the server-side code, the implementation of the IAutoStatService interface. A couple of points: The class is annotated with @Produces( { “application/json”, “application/xml” } ) which defines the media type(s) that the methods of a AutoStatServiceImpl can produce and with @Path( “/AutoStatService” ) which identifies the URI path that AutoStatServiceImpl will serve requests for.

package com.apptotest.service;

import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

@Produces( { "application/json", "application/xml" } )
@Path("/AutoStatService")
public class AutoStatServiceImpl implements IAutoStatService {

    private static Map stats = new HashMap();

    /**
     * We mimic the effects of populating the collection from a database
     */
    public AutoStatServiceImpl() {
        AutoStatistics stat1 = new AutoStatistics("Acura TSX V6", 1000001L, 130F, 5.9F, 14.4F, 133F);
        stats.put(stat1.getId(), stat1);
        AutoStatistics stat2 = new AutoStatistics("Alfa Romeo 8C Competizione", 1000002L, 181F, 4.2F, 12.4F, 105F);
        stats.put(stat2.getId(), stat2);
        AutoStatistics stat3 = new AutoStatistics("Audi A5 2.0T Quattro", 1000003L, 130F, 6.2F, 14.8F, 130F);
        stats.put(stat3.getId(), stat3);
        AutoStatistics stat4 = new AutoStatistics("Cadillac CTS-V", 1000004L, 191F, 4.1F, 12.3F, 114F);
        stats.put(stat4.getId(), stat4);
        AutoStatistics stat5 = new AutoStatistics("Chevrolet Camaro SS Coupe", 1000005L, 155F, 5.9F, 4.6F, 119F);
        stats.put(stat5.getId(), stat5);
        AutoStatistics stat6 = new AutoStatistics("Nissan 370Z", 1000006L, 155F, 5.2F, 13.7F, 115F);
        stats.put(stat6.getId(), stat6);
    }

    @Override
    @POST
    @Path("/add")
    @Consumes( { "application/json", "application/xml" } )
    public AutoStatistics addAutoStatistics(AutoStatistics astats) {
        if (astats.getId() == null || astats.getId() == 0L || stats.keySet().contains(astats.getId())) {
            System.err.println("unable to add: " + astats);
            return null;
        } else {
            stats.put(astats.getId(), astats);
        }
        return astats;
    }

    @Override
    @DELETE
    @Path("/delete/{id}")
    public AutoStatistics deleteAutoStatistics(@PathParam("id") Long id) {
        stats.remove(id);
        return new AutoStatistics();
    }

    @Override
    @GET
    @Path("/all")
    public AllAutoStatistics getAllAutoStatistics() {
        AllAutoStatistics allStats = new AllAutoStatistics();
        allStats.setAllStats(stats.values());
        return allStats;
    }

    @Override
    @GET
    @Path("/autostats/{id}")
    public AutoStatistics getAutoStatistics(@PathParam("id") Long id) {
        return stats.get(id);
    }

    @Override
    @POST
    @Path("/edit")
    @Consumes( { "application/json", "application/xml" } )
    public AutoStatistics updateAutoStatistics(AutoStatistics astats) {
        if (stats.containsKey(astats.getId())) {
            stats.put(astats.getId(), astats);
            return astats;
        }
        return null;
    }
}

Once you have compiled and deployed your code (I used CXF 2.2) we can turn, at last, to the client. This post is about writing clients for RESTful services and I have chosen to start with the Apache Commons HttpClient because

  1. It is a fairly popular/stable HTTP API wrapper
  2. There is nothing in this API that is REST-specific or even Web Services-specific; this point is worth stressing, by properly setting up the @javax.ws.rs.Path annotation on the server class the client will be able to call the addAutoStatistics() operation, for example, simply by constructing a POST request in the form http://<host&gt;:<port>/autoStats/AutoStatService/add
  3. It throws intelligent (read human readable) exceptions when an HTTP operation fails

You will note that the client code is wrapped in JUnit test cases; that’s because I want to explicitly show the success/failure of the operations. I also want to demonstrate the statelessness of the various operations, so setUp() and tearDown() come in handy. Now the unit testing framework in itself is quite irrelevant to this exercise but if you are just getting started with your client code then setting up test cases is indeed a good idea.

package com.apptotest.client;

import static org.junit.Assert.*;
import java.io.ByteArrayInputStream;
import java.util.Collection;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * We'll use Apache Commons HttpClient to connect and test the following
 * REST operations:
 * - POST
 * - DELETE
 * - GET
 * - PUT
 */
public class AutoStatServiceImplCommonsHttpClientTest {
    private static final String hostname = "http://localhost:7650/autoStats/AutoStatService";
    private HttpClient httpClient;
    private ObjectMapper mapper;

    @Before
    public void setUp() throws Exception {
        httpClient = new HttpClient();
        mapper = new ObjectMapper();
    }

    @After
    public void tearDown() throws Exception {}

    /**
     * corresponding to URL: http://localhost:7650/autoStats/AutoStatService/add
     * @throws Exception
     */
    @Test
    public final void testAddAutoStat() throws Exception {
        PostMethod post = new PostMethod(hostname + "/add");
        AutoStatistics mystat1 = new AutoStatistics("BMW 335d", 9000001L, 149F, 5.3F, 13.8F, 116F);
        String jsonValue = mapper.writeValueAsString(mystat1);
        RequestEntity requestEntity = new StringRequestEntity(jsonValue, "application/json", "UTF-8");
        post.setRequestEntity(requestEntity);
        int statusCode = httpClient.executeMethod(post);
        if (statusCode != HttpStatus.SC_OK) {
            fail("POST method failed: " + post.getStatusLine());
        } else {
            System.out.println("POST method succeeded: " + post.getStatusLine());
            byte[] httpResponse = post.getResponseBody();
            AutoStatistics respStat1 = mapper.readValue(new ByteArrayInputStream(httpResponse), AutoStatistics.class);
            assertNotNull(respStat1);
            assertEquals(mystat1, respStat1);
        }
        post.releaseConnection();
    }

    /**
     * corresponding to URL: http://localhost:7650/autoStats/AutoStatService/delete/{id}
     * where {id} gets expanded by the REST template
     * @throws Exception
     */
    @Test
    public final void testDeleteAutoStat() throws Exception {
        DeleteMethod delete = new DeleteMethod(hostname + "/delete/1000001");
        int statusCode = httpClient.executeMethod(delete);
        if (statusCode != HttpStatus.SC_OK) {
            fail("DELETE method failed: " + delete.getStatusLine());
        } else {
            System.out.println("DELETE method succeeded: " + delete.getStatusLine());
            byte[] httpResponse = delete.getResponseBody();
            AutoStatistics respStat = mapper.readValue(new ByteArrayInputStream(httpResponse), AutoStatistics.class);
            assertNotNull(respStat);
            assertEquals(new AutoStatistics(), respStat);
        }
        delete.releaseConnection();
    }

    /**
     * corresponding URL: http://localhost:7650/autoStats/AutoStatService/all
     * @throws Exception
     */
    @Test
    public final void testGetAutoStats() throws Exception {
        GetMethod get = new GetMethod(hostname + "/all");
        get.addRequestHeader("content-type", "application/json;charset=UTF-8");
        get.addRequestHeader("Content-Type", "application/json;charset=UTF-8");
        get.addRequestHeader("Accept", "application/json");

        int statusCode = httpClient.executeMethod(get);
        if (statusCode != HttpStatus.SC_OK) {
            fail("GET method failed: " + get.getStatusLine());
        } else {
            byte[] httpResponse = get.getResponseBody();
            AllAutoStatistics stats = mapper.readValue(new ByteArrayInputStream(httpResponse), AllAutoStatistics.class);
            assertNotNull(stats);
            assertNotNull(stats.getAllStats());
            Collection allStats = (Collection) stats.getAllStats();
            assertTrue(allStats.contains(new AutoStatistics("Alfa Romeo 8C Competizione", 1000002L, 181F, 4.2F, 12.4F, 105F)));
            assertTrue(allStats.contains(new AutoStatistics("Cadillac CTS-V", 1000004L, 191F, 4.1F, 12.3F, 114F)));
            for (AutoStatistics stat : allStats) {
                System.out.println(stat);
            }
        }
        get.releaseConnection();
    }

    /**
     * corresponding URL: http://localhost:7650/autoStats/AutoStatService/autostats/1000002
     * @throws Exception
     */
    @Test
    public final void testGetAutoStatAsXml() throws Exception {
        GetMethod get = new GetMethod(hostname + "/autostats/1000002");
        get.addRequestHeader("content-type", "application/xml;charset=UTF-8");
        get.addRequestHeader("Content-Type", "application/xml;charset=UTF-8");
        get.addRequestHeader("Accept", "application/xml");

        int statusCode = httpClient.executeMethod(get);
        if (statusCode != HttpStatus.SC_OK) {
            fail("GET method failed: " + get.getStatusLine());
        } else {
            byte[] httpResponse = get.getResponseBody();
            String strResponse = new String(httpResponse);
            System.out.println("response: " + strResponse);
            String expectedResponse = "1000002181.0Alfa Romeo 8C Competizione12.4105.04.2";
            assertTrue(strResponse.contains(expectedResponse));
        }
        get.releaseConnection();
    }

    /**
     * corresponding URL: http://localhost:7650/cxfweb_ajax/cxf/rest/AutoStatService/autostats/1000002
     * @throws Exception
     */
    @Test
    public final void testGetAutoStatAsJson() throws Exception {
        GetMethod get = new GetMethod(hostname + "/autostats/1000002");
        get.addRequestHeader("content-type", "application/json;charset=UTF-8");
        get.addRequestHeader("Content-Type", "application/json;charset=UTF-8");
        get.addRequestHeader("Accept", "application/json");

        int statusCode = httpClient.executeMethod(get);
        if (statusCode != HttpStatus.SC_OK) {
            fail("GET method failed: " + get.getStatusLine());
        } else {
            byte[] httpResponse = get.getResponseBody();
            AutoStatistics stat = mapper.readValue(new ByteArrayInputStream(httpResponse), AutoStatistics.class);
            System.out.println("response: " + stat);
            assertEquals("name", "Alfa Romeo 8C Competizione", stat.getName());
            assertEquals("id", 1000002L, stat.getId().longValue());
            assertEquals("maxSpeed", 181.0F, stat.getMaxSpeed().floatValue(), 0.0);
            assertEquals("zeroToSixtyTimeInSecs", 4.2F, stat.getZeroToSixtyTimeInSecs().floatValue(), 0.0);
            assertEquals("quarterMileTimeInSecs", 12.4F, stat.getQuarterMileTimeInSecs().floatValue(), 0.0);
            assertEquals("sixtyToZeroDistanceInFt", 105.0F, stat.getSixtyToZeroDistanceInFt().floatValue(), 0.0);
        }
        get.releaseConnection();
    }

    /**
     * corresponding URL: http://localhost:7650/autoStats/AutoStatService/edit
     * @throws Exception
     */
    @Test
    public final void testUpdateAutoStat() throws Exception {
        PutMethod put = new PutMethod(hostname + "/edit");
        AutoStatistics stat = new AutoStatistics("Audi A5 2.0T Quattro - Updated Http Commons", 1000003L, 130F, 6.2F, 14.8F, 130F);
        String jsonValue = mapper.writeValueAsString(stat);
        RequestEntity requestEntity = new StringRequestEntity(jsonValue, "application/json", "UTF-8");
        put.setRequestEntity(requestEntity);
        int statusCode = httpClient.executeMethod(put);
        if (statusCode != HttpStatus.SC_OK) {
            fail("PUT method failed: " + put.getStatusLine());
        } else {
            System.out.println("PUT method succeeded: " + put.getStatusLine());
            byte[] httpResponse = put.getResponseBody();
            AutoStatistics respStat = mapper.readValue(new ByteArrayInputStream(httpResponse), AutoStatistics.class);
            assertNotNull(respStat);
            assertEquals(stat, respStat);
        }
    }

}

The next installment will show you how to write a client using just the J2SE API.

Apache CXF How-Tos, well not exactly

October 27, 2009 § 15 Comments

Apache CXF offers a very good JAX-WS implementation but the documentation is frustrating at best. There are invaluable advices in the CXF wiki but for someone trying to get started the lack of step-by-step setup advice can be frustrating. In this post I’ll try to give some hints to make the official documentation friendlier. We’ll start with JAXB-related topics.

Joe Morison, my colleague at Lab49, already posted an excellent article to address the CXF documentation shortcomings; in it he shows how to create a complete Maven project with a WSDL-first approach to set up CXF. His article will get you started in no time. You are now ready to resume reading the CXF wiki and experiment with more advanced features but as soon as you hit the article on building a simple JAX-WS service you hit a wall: The showcased HelloWorld web service advertises two use cases where one can pass an interface to a web method and one can return a Map from a web method, but apart from the javadoc nothing tells you how to proceed. I will go over the required JAXB annotations to allow you to build a consume the CXF wiki-advertised web service.

First let’s tackle the problem of passing an Interface to a web method.

/* Advanced usecase of passing an Interface in. JAX-WS/JAXB does
 * not support interfaces directly.  Special XmlAdapter classes need to
 * be written to handle them
 */
String sayHiToUser(User user);

As is this code will compile but will fail at runtime with a message similar to the following one:
jaxws.hello.server.User is an interface, and JAXB can’t handle interfaces.

One possible solution is to use the XmlJavaTypeAdater JAXB annotation to tell the stack that to use the implementation of the User Interface; this annotation is extremely useful as it allows you to custom marshal most Java objects into XML. Do note that the adapter class is a static inner class as opposed to an inner class because JAXB can’t handle non-static inner classes. Using a non-static inner class would result in a runtime exception similar to the following one:

jaxws.hello.server.UserImpl$UserAdapater is a non-static inner class, and JAXB can’t handle those.

Here is the code for the annotated interface:

package jaxws.hello.server;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlJavaTypeAdapter(UserImpl.UserAdapter.class)
public interface User {
 /**
 * @return
 */
 public abstract Long getId();

 /**
 * @return the name
 */
 public abstract String getName();
}

And here is the code for the implementing class containing the adapter:

package jaxws.hello.server;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;

/**
* @author RRached
*/
@XmlType(name="UserType", propOrder = {"id", "firstName", "lastName"})
@XmlAccessorType(XmlAccessType.FIELD)
public class UserImpl implements User {
 @XmlElement(required=true)
 private Long id;
 @XmlElement(required=true)
 private String firstName;
 @XmlElement(required=true)
 private String lastName;
 /**
 *
 */
 public UserImpl() {}
 /**
 * @param id
 * @param firstName
 * @param lastName
 */
 public UserImpl(Long id, String firstName, String lastName) {
   this.id = id;
   this.firstName = firstName;
   this.lastName = lastName;
 }
 /* (non-Javadoc)
 * @see jaxws.server.hello.User#getId()
 */
 public Long getId() {
   return id;
 }
 /**
 * @return the firstName
 */
 public String getFirstName() {
   return firstName;
 }
 /**
 * @return the lastName
 */
 public String getLastName() {
   return lastName;
 }
 /* (non-Javadoc)
 * @see jaxws.hello.server.User#getName()
 */
 public String getName() {
   return firstName + " " + lastName;
 }
 /**
 *
 */
 protected static class UserAdapter extends XmlAdapter<UserImpl, User>{
   /* (non-Javadoc)
    * @see javax.xml.bind.annotation.adapters.XmlAdapter#marshal(java.lang.Object)
   */
   @Override
   public UserImpl marshal(User u) throws Exception {
     return (UserImpl)u;
   }
   /* (non-Javadoc)
    * @see javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal(java.lang.Object)
    */
   @Override
   public User unmarshal(UserImpl v) throws Exception {
     return v;
   }
  }
}

A few thing to note about this code:

  • The XmlType annotation is used to tell JAXB that every UserImpl object will be serialized using a UserType type in the xsd schema; the propOrder specifies the order in which content is marshalled to XML and is unmarshalled from XML, as opposed to marshalling and unmarshalling in no specific order due to Java Reflection not making any guarantees about attributes’ order
  • The XmlAccessType.FIELD tells JAXB to bond all non-static non-transient fields to XML unless those fields are explicitly annotated with the XmlTransient annotation; this results in getter/setter pairs not getting automatically marshalled.
  • The UserAdapter is the adapter referred to in the User interface annotation (@XmlJavaTypeAdapter(UserImpl.UserAdapter.class)) and happens to be a (static) inner class because this example is a simple one with a one-to-one correspondence between the interface and the implementing class; nothing stops the adapter from being a full-fledged class
  • Every adapter needs to implements a marshall() and unmarshall() methods

Next let’s tackle the problem of returning a Map from a web method:

/* Map passing
 * JAXB also does not support Maps.  It handles Lists great, but Maps are
 * not supported directly.  They also require use of a XmlAdapter to map
 * the maps into beans that JAXB can use.
 */
@XmlJavaTypeAdapter(IntegerUserMapAdapter.class)
Map<Integer, User> getUsers();

This is how you could declare the users map in the HelloWorldImpl class:

@XmlJavaTypeAdapter(value=IntegerUserMapAdapter.class)
@XmlElement(name = "users")
private Map<Integer, User> users = new LinkedHashMap<Integer, User>();

Creating the IntegerUserMapAdapter class is a multi-stage process to ultimately flatten the Map into a XML-friendly structure; I choose to represent this particular map using the following class (notice that it is a regular POJO) but any other way (using an array, for example) will do:

package jaxws.hello.server;</code>

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlType;

/**
 * @author RRached
 *
 */
@XmlType(name="IntegerUserType")
@XmlAccessorType(XmlAccessType.FIELD)
public class IntegerUserType {
 // produce a wrapper XML element around this collection
 @XmlElementWrapper(name="userList")
 // map each member of this list to an XML element named appointment
 @XmlElement(name="users")
 private List<IntegerUser> users;
 /**
  *
  */
 public IntegerUserType() {}
 /**
  * @param map
  */
 public IntegerUserType(Map<Integer, User> map) {
   users = new ArrayList();
   Set<Entry<Integer, User>> set = map.entrySet();
   for (Entry<Integer, User> idUserEntry : set) {
      users.add(new IntegerUser(idUserEntry.getKey(), idUserEntry.getValue()));
   }
 }
 /**
  * @return the users
  */
 public List<IntegerUser> getUsers() {
   return users;
 }
 /**
  *
  */
 protected static class IntegerUser {
   @XmlElement(name="id")
   private Integer id;
   @XmlElement(name="user")
   private User user;
   /**
    *
    */
   protected IntegerUser() {}
   /**
    * @param id
    * @param user
    */
   protected IntegerUser(Integer id, User user) {
      this.id = id;
      this.user = user;
   }
   /**
    * @return {@link Integer}
    */
   protected Integer getId() {
      return id;
   }
   /**
    * @return {@link User}
    */
   protected User getUser() {
      return user;
   }
 }
}
  • The usage of the JAXB notation is optional but greatly helps in creating a clean schema; this way, when you’ll decide to use an XSD-first approach for your project, you’ll have a template. Nothing stops you from using a JAXBContext.newInstance() to stream the schema to an output file and starting cleanly by editing that XSD
  • A List is a commonly used strategy to flatten a Map
  • The no-ag constructor is a JAXB requirement

The last step is extending the XmlAdapter class to create the actual IntegerUserMapAdapter (you can certainly come up with more elegant names):

/**
 *
 */
package jaxws.hello.server;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.annotation.adapters.XmlAdapter;

/**
 * @author RRached
 *
 */
public class IntegerUserMapAdapter extends XmlAdapter<IntegerUserType, Map<Integer, User> > {
 @Override
 public IntegerUserType marshal(Map<Integer, User> map) throws Exception {
   return new IntegerUserType(map);
 }

 @Override
 public Map<Integer, User> unmarshal(IntegerUserType type) throws Exception {
   Map map = new HashMap();
   List<IntegerUser> users = type.getUsers();
   for (IntegerUserType.IntegerUser idUser : users) {
      map.put(idUser.getId(), idUser.getUser());
   }
   return map;
 }
}

That’s it!
I will soon be adding more tips to complement the CXF wiki documentation.

Where Am I?

You are currently browsing entries tagged with Web Services at Computing Thoughts by Roger Rached.

Follow

Get every new post delivered to your Inbox.