Clients like REST part 3

October 20th, 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.SOAPMessage and javax.activation.DataSource useful 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;
 }
}

Advertisement

Tagged: , , , , , , , , , , , , , , , ,

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 )

Connecting to %s

What’s this?

You are currently reading Clients like REST part 3 at Computing Thoughts by Roger Rached.

meta

Follow

Get every new post delivered to your Inbox.