Clients like REST part 3
October 20, 2010 § Leave a Comment
This post is the third 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 |
I will now show you how to access REST services using the JAX-WS client API, but first let me explain the motivation behind this selection. After all JAX-WS is typically used with SOAP services and assumes that a WSDL resides on the server side, so why bother at all with JAX-WS when you can use the JAX-RS client API to consume logically enough REST services? I think that there are quite a few situations where JAX-WS makes sense, and besides JAX-RS examples to access a REST service abound, so here is a list of possible reasons for wanting to use JAX-WS to access a REST service:
- You want to use a single API on the client side to handle both SOAP and REST services (The Dispatch API is particularly flexible, allowing you to handle SOAP-style messages with Header + Payload or REST-style messages with just a Payload, you are also free to use a data binding tool other than JAXB if needs be, Castor comes to mind)
- You want to use an API included in the JDK
- You want to support synchronous, asynchronous and one-way calls without resorting to 3rd-party solutions like COMET
- You want to abstract the actual binding of the transport (JAX-WS allows you to use SOAP 1.1 over HTTP, SOAP 1.2 over HTTP or XML over HTTP)
- You want to abstract the class of objects used for messages: The JAX-WS Dispatch API mandates the support of at least three types (
javax.xml.transform.Source,javax.xml.soap.SOAPMessageandjavax.activation.DataSourceuseful for MIME messages)
We will go over the client code to access the same service – defined through the interface IAutoStatService – and I will note along the way where does the API feel awkward to work with and where it does have some very interesting potential. Again I will illustrate how to add/delete/get/get all and update AutoStatistics, but you will notice right away that everything is very much XML-oriented (as opposed to, say, json). The reason is simple: No matter what binding you use for transport (SOAP 1.1 over HTTP, SOAP 1.2 over HTTP or XML over HTTP) it assumes an XML format. This can be limiting, but unless you use a 3rd party client library such as Jettison you are stuck with XML manipulation when using the plain-vanilla JAX-WS API from the JDK. I made a deliberate attempt to avoid using non-JDK API such as CXF on the client side even though classes such as JaxWsProxyFactoryBean can be quite useful. On the other hand if you are perfectly happy working with XML you will see that JAX-WS integrates naturally with the javax.xml.transform.Source and the javax.xml.transform.Transformer classes. From there any high-level XML API (I used XPath for e.g.) can be readily used.
On the awkward side you will note that JAX-WS expects a WSDL-oriented development and forces you to define two qualified names for the service and the port even though the Dispatch class allows you to handle raw XML. This is illustrated in the javax.xml.ws.Service.createDispatch(QName portName, Class<Source> type, Mode mode) method where you will set the Mode to Service.Mode.PAYLOAD as opposed to Service.Mode.MESSAGE to express the need to work with XML contents without a SOAP envelope, as is the case with REST services.
I also noticed a bug in the CXF implementation (2.2.3) I was using; even though my GET operations are invoked without an additional argument I could not simply call javax.xml.ws.Dispatch.invoke(null) as it would throw an exception, I had to pass in a fake request that would get ignored on the server side without any visible side-effect. My understanding is that the latest Glassfish and Axis2 do not suffer from such a bug. This shouldn’t detract you from using CXF on the server-side…
So here is the code:
package com.apptotest.client;
import static org.junit.Assert.*;
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* We'll use the JAX-WS 2.x API (included in the SDK) to connect and test the following
* REST operations:
* - POST
* - DELETE
* - GET
* - PUT
*/
public class AutoStatServiceImplJAXWSClientTest {
private static final String hostname = "http://localhost:7650/autoStats/AutoStatService";
private Transformer transformer;
@Before
public void setUp() throws Exception {
transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "no");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
}
/**
* corresponding to URL: http://localhost:7650/cxfweb_ajax/cxf/rest/CustomerService/add
* @throws Exception
*/
@Test
public final void testAddAutoStat() throws Exception {
String xmlAutoStatistics = "9000006150.0BMW 335ix13.3119.04.7";
QName serviceName = new QName("http://test", "fakeservicename");
QName portName = new QName("http://test", "fakeportname");
Service service = Service.create(serviceName);
service.addPort(portName, HTTPBinding.HTTP_BINDING, hostname + "/add");
Dispatch dispatch = service.createDispatch(portName, Source.class, Service.Mode.PAYLOAD);
Map requestContext = dispatch.getRequestContext();
requestContext.put(MessageContext.HTTP_REQUEST_METHOD, "POST");
Map> httpRequestHeaders = new HashMap>();
httpRequestHeaders.put("Content-Type", Arrays.asList(new String[] { "application/xml" }));
httpRequestHeaders.put("Accept", Arrays.asList(new String[] { "application/xml" }));
httpRequestHeaders.put("Content-Length", Arrays.asList(new String[] { Integer.toString(xmlAutoStatistics.length()) }));
requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpRequestHeaders);
Source result = dispatch.invoke(new StreamSource(new ByteArrayInputStream(xmlAutoStatistics.getBytes())));
StringWriter writer = new StringWriter();
transformer.transform(result, new StreamResult(writer));
Map responseContext = dispatch.getResponseContext();
if (!responseContext.get(org.apache.cxf.message.Message.RESPONSE_CODE).equals(new Integer(200))) {
fail("GET method failed: " + responseContext.get("org.apache.cxf.service.model.MessageInfo"));
} else {
String expectedResponse = "9000006150.0BMW 335ix13.3119.04.7";
assertTrue(writer.toString().contains(expectedResponse));
}
}
/**
* 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 {
QName serviceName = new QName("http://test", "fakeservicename");
QName portName = new QName("http://test", "fakeportname");
Service service = Service.create(serviceName);
service.addPort(portName, HTTPBinding.HTTP_BINDING, hostname + "/delete/10003");
Dispatch dispatch = service.createDispatch(portName, Source.class, Service.Mode.PAYLOAD);
Map requestContext = dispatch.getRequestContext();
requestContext.put(MessageContext.HTTP_REQUEST_METHOD, "DELETE");
Map> httpRequestHeaders = new HashMap>();
httpRequestHeaders.put("Content-Type", Arrays.asList(new String[] { "application/xml" }));
httpRequestHeaders.put("Accept", Arrays.asList(new String[] { "application/xml" }));
requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpRequestHeaders);
String sampleBodyContent = ""
+ "HELLO THERE!!!" + "";
DOMSource request = createDOMSourceFromString(sampleBodyContent);
Source result = dispatch.invoke(request);
StringWriter writer = new StringWriter();
transformer.transform(result, new StreamResult(writer));
Map responseContext = dispatch.getResponseContext();
if (!responseContext.get(org.apache.cxf.message.Message.RESPONSE_CODE).equals(new Integer(200))) {
fail("GET method failed: " + responseContext.get("org.apache.cxf.service.model.MessageInfo"));
} else {
String expectedResponse = "";
assertTrue(writer.toString().contains(expectedResponse));
}
}
/**
* corresponding URL: http://localhost:7650/autoStats/AutoStatService/all
* @throws Exception
*/
@Test
public final void testGetAutoStats() throws Exception {
QName serviceName = new QName("http://test", "fakeservicename");
QName portName = new QName("http://test", "fakeportname");
Service service = Service.create(serviceName);
service.addPort(portName, HTTPBinding.HTTP_BINDING, hostname + "/all");
Dispatch dispatch = service.createDispatch(portName, Source.class, Service.Mode.PAYLOAD);
Map requestContext = dispatch.getRequestContext();
requestContext.put(MessageContext.HTTP_REQUEST_METHOD, "GET");
Map> httpRequestHeaders = new HashMap>();
httpRequestHeaders.put("content-type", Arrays.asList(new String[]{ "application/xml;charset=UTF-8" }));
httpRequestHeaders.put("Content-Type", Arrays.asList(new String[]{ "application/xml;charset=UTF-8" }));
httpRequestHeaders.put("Accept", Arrays.asList(new String[]{ "application/xml" }));
requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpRequestHeaders);
String sampleBodyContent = ""
+ "HELLO THERE!!!" + "";
DOMSource request = createDOMSourceFromString(sampleBodyContent);
Source result = dispatch.invoke(request);
StringWriter writer = new StringWriter();
transformer.transform(result, new StreamResult(writer));
XPath xpath= XPathFactory.newInstance().newXPath();
InputSource xmlSource = new InputSource(new StringReader(writer.toString()));
Map responseContext = dispatch.getResponseContext();
if (!responseContext.get(org.apache.cxf.message.Message.RESPONSE_CODE).equals(new Integer(200))) {
fail("GET method failed: " + responseContext.get("org.apache.cxf.service.model.MessageInfo"));
} else {
XPathExpression allAutoStatisticsExpr = xpath.compile("//AllAutoStatistics");
Element allStatsElement = (Element) allAutoStatisticsExpr.evaluate(xmlSource, XPathConstants.NODE);
assertNotNull(allStatsElement);
NodeList allStatsChildren = allStatsElement.getChildNodes();
assertTrue("AllAutoStatistics num,of children: " + allStatsChildren.getLength(), allStatsChildren.getLength() >= 6);
Node allStatsChild = allStatsChildren.item(0);
assertTrue(allStatsChild.hasChildNodes());
}
}
/**
* corresponding URL: http://localhost:7650/autoStats/AutoStatService/autostats/1000002
* @throws Exception
*/
@Test
public final void testGetAutoStatAsXml() throws Exception {
QName serviceName = new QName("http://test", "fakeservicename");
QName portName = new QName("http://test", "fakeportname");
Service service = Service.create(serviceName);
service.addPort(portName, HTTPBinding.HTTP_BINDING, hostname + "/autostats/1000002");
Dispatch dispatch = service.createDispatch(portName, Source.class, Service.Mode.PAYLOAD);
Map requestContext = dispatch.getRequestContext();
requestContext.put(MessageContext.HTTP_REQUEST_METHOD, "GET");
Map> httpRequestHeaders = new HashMap>();
httpRequestHeaders.put("content-type", Arrays.asList(new String[]{ "application/xml;charset=UTF-8" }));
httpRequestHeaders.put("Content-Type", Arrays.asList(new String[]{ "application/xml;charset=UTF-8" }));
httpRequestHeaders.put("Accept", Arrays.asList(new String[]{ "application/xml" }));
requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpRequestHeaders);
String sampleBodyContent = ""
+ "HELLO THERE!!!" + "";
DOMSource request = createDOMSourceFromString(sampleBodyContent);
Source result = dispatch.invoke(request);
StringWriter writer = new StringWriter();
transformer.transform(result, new StreamResult(writer));
Map responseContext = dispatch.getResponseContext();
if (!responseContext.get(org.apache.cxf.message.Message.RESPONSE_CODE).equals(new Integer(200))) {
fail("GET method failed: " + responseContext.get("org.apache.cxf.service.model.MessageInfo"));
} else {
String expectedResponse = "1000002181.0Alfa Romeo 8C Competizione12.4105.04.2";
assertTrue(writer.toString().contains(expectedResponse));
}
}
/**
* corresponding URL: http://localhost:7650/cxfweb_ajax/cxf/rest/CustomerService/edit
* @throws Exception
*/
@Test
public final void testUpdateAutoStat() throws Exception {
String xmlAutoStatistics = "1000003130.0Audi A5 2.0T Quattro - Updated JAX-WS14.8130.06.2";
QName serviceName = new QName("http://test", "fakeservicename");
QName portName = new QName("http://test", "fakeportname");
Service service = Service.create(serviceName);
service.addPort(portName, HTTPBinding.HTTP_BINDING, hostname + "/edit");
Dispatch dispatch = service.createDispatch(portName, Source.class, Service.Mode.PAYLOAD);
Map requestContext = dispatch.getRequestContext();
requestContext.put(MessageContext.HTTP_REQUEST_METHOD, "POST");
Map> httpRequestHeaders = new HashMap>();
httpRequestHeaders.put("Content-Type", Arrays.asList(new String[] { "application/xml" }));
httpRequestHeaders.put("Accept", Arrays.asList(new String[] { "application/xml" }));
httpRequestHeaders.put("Content-Length", Arrays.asList(new String[] { Integer.toString(xmlAutoStatistics.length()) }));
requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpRequestHeaders);
Source result = dispatch.invoke(new StreamSource(new ByteArrayInputStream(xmlAutoStatistics.getBytes())));
StringWriter writer = new StringWriter();
transformer.transform(result, new StreamResult(writer));
Map responseContext = dispatch.getResponseContext();
if (!responseContext.get(org.apache.cxf.message.Message.RESPONSE_CODE).equals(new Integer(200))) {
fail("GET method failed: " + responseContext.get("org.apache.cxf.service.model.MessageInfo"));
} else {
assertTrue(writer.toString().contains(expectedResponse));
}
}
/**
* @param input
* @return
* @throws Exception
*/
private DOMSource createDOMSourceFromString(String input) throws Exception {
byte[] bytes = input.getBytes();
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(false);
domFactory.setFeature("http://xml.org/sax/features/namespaces", false);
domFactory.setFeature("http://xml.org/sax/features/validation", false);
domFactory.setFeature("http://apache.org/xml/features/validation/schema", false);
DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
Document domTree = domBuilder.parse(stream);
Node node = domTree.getDocumentElement();
DOMSource domSource = new DOMSource(node);
return domSource;
}
}
Clients like REST part 2
October 8, 2010 § Leave a Comment
This post is the second 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 |
This post will show you how to access the same service – defined through the interface IAutoStatService – using the native J2SE API and in particular how to take full advantage of the HttpURLConnection class. I think that this is a pretty powerful concept: You can access a web service from a Java client without a 3rd party web-service library and without the need to write some type of parser.
Here are some notes about this client implementation:
- It relies on the lowest common denominator to all java apps only, the JDK
- To be complete I will mention that I am using the Jackson JSON processor to construct JSON strings from a POJO and vice versa de-serializing a JSON string coming on an input stream into a POJO; it’s just a convenience tool that does not contradict the above point (using just the native J2SE API)
- Again, 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>:<port>/autoStats/AutoStatService/add
- Since we are mainly relying on the HttpURLConnection class you will be forced to think in terms of the details of the HTTP protocol: What to set for Content-Type, Accept or Content-Length request properties? How to decode the HTTP response code? etc… But it’s not that bad and as a bonus your code would be easily portable to another language since these constructs are pretty generic
- You are not limited to a particular data format: if the server side produces JSON, you can de-serialize JSON into POJO, if the server side produces XML, you can de-serialize XML into POJO or use XQuery, etc…
- The testHttpURLConnection(HttpURLConnection connection) method is completely optional but helpful when debugging. It’s mainly included to show you how the various parts of the HTTP message (header/body) are constructed in each of the following cases: POST, DELETE and GET
package com.apptotest.client;
import static org.junit.Assert.*;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.Permission;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* TODO: We'll use a standard J2SE client (based on {@link HttpURLConnection} to connect
* and test the following REST operations:
* - POST
* - DELETE
* - GET
* - PUT
* This type of coding requires (some) help from the Jackson JSON library to interpret the
* {@link InputStream}.
*/
public class AutoStatServiceImplJ2SEClientTest {
private static final String hostname = "http://localhost:7650/autoStats/AutoStatService";
private URL url;
private HttpURLConnection conn;
private InputStream in;
private OutputStream out;
private ObjectMapper mapper;
@Before
public void setUp() throws Exception {
mapper = new ObjectMapper();
}
@After
public void tearDown() throws Exception {
if (in != null) { in.close(); }
if (out != null) { out.close(); }
if (conn != null) { conn.disconnect(); }
}
/**
* corresponding to URL: http://localhost:7650/autoStats/AutoStatService/add
* @throws Exception
*/
@Test
public final void testAddAutoStat() throws Exception {
AutoStatistics stat = new AutoStatistics("BMW 335i", 9000007L, 150F, 4.7F, 13.3F, 119F);
String jsonValue = mapper.writeValueAsString(stat);
url = new URL(hostname + "/add");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Content-Length", Integer.toString(jsonValue.length()));
conn.getOutputStream().write(jsonValue.getBytes());
conn.getOutputStream().flush();
conn.connect();
testHttpURLConnection(conn);
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
fail("POST method failed: " + conn.getResponseCode() + "\t" + conn.getResponseMessage());
} else {
InputStream responseContent = (InputStream) conn.getContent();
AutoStatistics respStat1 = mapper.readValue(responseContent, AutoStatistics.class);
assertNotNull(respStat1);
assertEquals(stat, respStat1);
}
}
/**
* corresponding to URL: http://localhost:7650/cxfweb_ajax/cxf/rest/AutoStatService/delete/{id}
* where {id} gets expanded by the REST template
*/
@Test
public final void testDeleteAutoStat() throws Exception {
url = new URL(hostname + "/delete/1000001");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("DELETE");
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.connect();
testHttpURLConnection(conn);
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
fail("DELETE method failed: " + conn.getResponseCode() + "\t" + conn.getResponseMessage());
} else {
InputStream responseContent = (InputStream) conn.getContent();
AutoStatistics respStat1 = mapper.readValue(responseContent, AutoStatistics.class);
assertNotNull(respStat1);
assertEquals(new AutoStatistics(), respStat1);
}
}
/**
* corresponding URL: http://localhost:7650/autoStats/AutoStatService/all
* @throws Exception
*/
@Test
public final void testGetAutoStats() throws Exception {
url = new URL(hostname + "/all");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
in = conn.getInputStream();
testHttpURLConnection(conn);
if (conn.getContentType().equals("application/json")) {
mapper = new ObjectMapper();
AllAutoStatistics stats = mapper.readValue(in, AllAutoStatistics.class);
assertNotNull(stats);
assertNotNull(stats.getAllStats());
Collection<AutoStatistics> allStats = (Collection<AutoStatistics>) 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)));
}
}
/**
* corresponding URL: http://localhost:7650/autoStats/AutoStatService/autostats/1000002
* @throws Exception
*/
@Test
public final void testGetAutoStatAsXml() throws Exception {
url = new URL(hostname + "/autostats/1000002");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-Type", "application/xml;charset=UTF-8");
conn.setRequestProperty("Accept", "application/xml");
conn.connect();
in = conn.getInputStream();
testHttpURLConnection(conn);
if (conn.getContentType().equals("application/xml")) {
char[] buff = new char[128];
InputStreamReader isr = new InputStreamReader(in);
StringBuilder builder = new StringBuilder();
while (isr.read(buff) != -1) {
builder.append(buff);
}
assertTrue(builder.length() > 0);
String expectedResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><AutoStatistics><id>1000002</id><maxSpeed>181.0</maxSpeed><name>Alfa Romeo 8C Competizione</name><quarterMileTimeInSecs>12.4</quarterMileTimeInSecs><sixtyToZeroDistanceInFt>105.0</sixtyToZeroDistanceInFt><zeroToSixtyTimeInSecs>4.2</zeroToSixtyTimeInSecs></AutoStatistics>";
assertTrue(builder.toString().contains(expectedResponse));
}
}
/**
* corresponding URL: http://localhost:7650/autoStats/AutoStatService/autostats/1000002
*
* @throws Exception
*/
@Test
public final void testGetAutoStatAsJson() throws Exception {
url = new URL(hostname + "/autostats/1000002");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
in = conn.getInputStream();
testHttpURLConnection(conn);
if (conn.getContentType().equals("application/json")) {
mapper = new ObjectMapper();
AutoStatistics stat = mapper.readValue(in, AutoStatistics.class);
assertNotNull(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);
}
}
/**
* corresponding URL: http://localhost:7650/autoStats/AutoStatService/edit
* @throws Exception
*/
@Test
public final void testUpdateAutoStat() throws Exception {
AutoStatistics stat = new AutoStatistics("Audi A5 2.0T Quattro - Updated J2SE", 1000003L, 130F, 6.2F, 14.8F, 130F);
String jsonValue = mapper.writeValueAsString(stat);
url = new URL(hostname + "/edit");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Content-Length", Integer.toString(jsonValue.length()));
conn.getOutputStream().write(jsonValue.getBytes());
conn.getOutputStream().flush();
conn.connect();
testHttpURLConnection(conn);
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
fail("POST method failed: " + conn.getResponseCode() + "\t" + conn.getResponseMessage());
} else {
InputStream responseContent = (InputStream) conn.getContent();
AutoStatistics respStat1 = mapper.readValue(responseContent, AutoStatistics.class);
assertNotNull(respStat1);
assertEquals(stat, respStat1);
// and test
url = new URL(hostname + "/all");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
in = conn.getInputStream();
AllAutoStatistics stats = mapper.readValue(in, AllAutoStatistics.class);
assertNotNull(stats);
assertNotNull(stats.getAllStats());
Collection<AutoStatistics> allStats = stats.getAllStats();
assertTrue(allStats.contains(new AutoStatistics("Audi A5 2.0T Quattro - Updated J2SE", 1000003L, 130F, 6.2F, 14.8F, 130F)));
}
}
/**
* @param connection
* @throws Exception
*/
private void testHttpURLConnection(HttpURLConnection connection) throws Exception {
boolean connAllowUserInteraction = connection.getAllowUserInteraction();
String connContentType = connection.getContentType();
String connContentEncoding = connection.getContentEncoding();
String connRequestMethod = connection.getRequestMethod();
boolean connDoInput = connection.getDoInput();
boolean connDoOutput = connection.getDoOutput();
Permission connPermission = connection.getPermission();
URL connURL = connection.getURL();
Map<String, List<String>> connHeaderFields = connection.getHeaderFields();
System.out.println("connAllowUserInteraction: " + connAllowUserInteraction);
System.out.println("connContentType: " + connContentType);
System.out.println("connContentEncoding: " + connContentEncoding);
System.out.println("connRequestMethod: " + connRequestMethod);
System.out.println("connDoInput: " + connDoInput);
System.out.println("connDoOutput: " + connDoOutput);
System.out.println("connPermission: " + connPermission);
System.out.println("connURL: " + connURL);
if (connHeaderFields != null) {
Set<Entry<String, List<String>>> connHeaderFieldsEntries = connHeaderFields.entrySet();
for (Entry<String, List<String>> entry : connHeaderFieldsEntries) {
System.out.println("connHeaderField: " + entry);
}
}
}
}
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:
- 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
- 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
- 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
- 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
- It is a fairly popular/stable HTTP API wrapper
- 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>:<port>/autoStats/AutoStatService/add
- 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.