Sunday, January 14, 2007

Implementing a Remote File Explorer in Flex 2: List View

The first application I want to implement in Flex is a standard split-pane file explorer, with a folder tree in the left pane and a list of folder contents in the right pane.

A basic implementation of the list pane turned out to be very simple:

<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  creationComplete="initApp(); itemsService.send();">

  <mx:Script source="explorer.as"/>

  <mx:HTTPService id="itemsService" url="/flex/items.jsp" 
    resultFormat="e4x" showBusyCursor="true"
    fault="handleFault(event)">
    <mx:request>
      <folderID>{folderID}</folderID>
    </mx:request>
  </mx:HTTPService>

  <mx:XMLListCollection id="itemsList" 
      source="{itemsService.lastResult.item}"/>

  <mx:DataGrid id="itemGrid" width="100%" height="100%" 
 dataProvider="{itemsList}">
    <mx:columns> 
      <mx:DataGridColumn dataField="title" headerText="Title"/>
      <mx:DataGridColumn dataField="size" headerText="Size"/>
      <mx:DataGridColumn dataField="type" headerText="Type"/>
      <mx:DataGridColumn dataField="lastModified" headerText="Last Modified"/>
    </mx:columns>
  </mx:DataGrid>
</mx:Application>

Working from the bottom of the file up:

4. The DataGrid is a scrollable, sortable table component. Viewers can resize and reposition columns by dragging the column headers.

3. The XMLListCollection is the data source for the table. Flex automatically updates the table when the data source changes.

2. The HTTPService defines how to retrieve data from the server as a simple XML document, in this case returned by a JSP. Flex automatically unmarshals the XML into the data source whenever it receives a successful response. The request element defines the dynamic query parameters that should be added to each request.

3. The Script references a small bit of ActionScript to handle initialization and request faults:

import mx.controls.Alert;
import mx.rpc.events.FaultEvent;

[Bindable]
public var folderID:String;

private function initApp():void {
  folderID = Application.application.parameters.folderID;
}

private function handleFault(e:FaultEvent):void {
  Alert.show("Failed to contact the server.");
}

The XML document returned by the server corresponds to the rows and columns in the table:

<items>
  <item>
    <title>myscript.as</title>
    <size>10.1K</size>
    <type>ActionScript</type>
    <lastModified>8:34 AM 01/15/07</lastModified>
  </item>
  ...
</items>

The only other thing I had to do was modify the wrapper HTML page to pass a folder ID to Flex:

<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <title>File Explorer</title>
      <script src="AC_OETags.js" language="javascript"></script>
      <style>
 body { margin: 0px; overflow:hidden }
      </style>
  </head>

  <body scroll='no'>
    <script language="JavaScript" type="text/javascript">
      <!--
      AC_FL_RunContent(
      "src", "explorer",
      "width", "100%",
      "height", "100%",
      "align", "middle",
      "id", "flexstore",
      "quality", "high",
      "bgcolor", "#869ca7",
      "name", "explorer",
      "flashVars", "folderID=<%=folderID%>",
      "allowScriptAccess","sameDomain",
      "type", "application/x-shockwave-flash",
      "pluginspage", "http://www.adobe.com/go/getflashplayer"
 );
      // -->
    </script>
    <noscript>
      <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
 id="flexstore" width="100%" height="100%"
 codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
 <param name='flashVars' value='folderID=<=folderID%>'></param>
 <param name="movie" value="explorer.swf"></param>
 <param name="quality" value="high"></param>
 <param name="bgcolor" value="#869ca7"></param>
 <param name="allowScriptAccess" value="sameDomain"></param>
 <embed src="explorer.swf" quality="high" bgcolor="#869ca7"
   width="100%" height="100%" name="flexstore" align="middle"
   flashVars="folderID=<%=folderID%>"
   play="true"
   loop="false"
   quality="high"
   allowScriptAccess="sameDomain"
   type="application/x-shockwave-flash"
   pluginspage="http://www.adobe.com/go/getflashplayer">
 </embed>
      </object>
    </noscript>
  </body>
</html>

I compiled the MXML file and was able to view the file list in the browser. Very nice!

Friday, January 12, 2007

Getting Started with Flex 2

After reading about Flex 2, I wanted to give it a try for myself. Adobe provides a free command-line compiler in the Flex SDK, downloadable with registration from their surprisingly clunky web site.

The download is just a zip file that you can extract and immediately begin using. Within a few minutes I was able to compile and test the sample applications, and from there it wasn't too hard to figure out the minimum setup I needed to create my own first application:

  1. Create an empty directory under the document root of your web server.
  2. Copy samples/explorer/AC_OETags.js to the new directory.
  3. Copy samples/explorer/explorer.html to the new directory. Rename it first.html
  4. Edit first.html and replace all instances of explorer with first
  5. Copy samples/explorer/build.sh to the new directory.
  6. Edit build.sh and change to the relative path to mxmlc so it points to wherever you extracted the SDK.

Once that was done, I created a new main layout file named first.mxml to declare my application:

<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
</mx:Application>

I used mxmlc to compile this into first.swf. I was then able to view first.html in a browser and successfully access my first (empty) Flex application.

Building user interfaces with Flex 2

After several years in the trenches, I am very sick of building rich browser-based user interfaces using DHTML. Even relying on fancy libraries like scriptaculous or the Yahoo User Interface Library, the interface never acts quite as nice as a desktop application. Loading can be slow. Uneditable labels and components are selectable. Objects can stutter and flicker or get stuck on unexpected boundaries when dragged. Different combinations of browsers and operating systems yield different results and bugs.

The whole programming model is also tedious. Not so much because of JavaScript, but because HTML is awkward for declaring layouts and wiring together components and data sources.

Adobe Flex 2 seems to offer a very compelling alternative to DHTML for rich browser-based application interfaces. With Flex you build interfaces in three parts:

  1. XML files that declare layouts, components and other static objects. Flex includes a full set of standard interface components such as trees, lists, menus and form controls. You can also create custom components.
  2. Script files (ActionScript) for implementing event handlers and other dynamic behavior.
  3. Server-side data sources and callbacks to handle data flow to and from the client.

Besides being compelling from a programming perspective, the standard components look and feel beautiful and responsive on all browsers and platforms. This demo is my favorite since it shows off each component along with the code required to use it.

Sunday, December 17, 2006

Solving 403 Forbidden Error in Apache on Fedora Core 6

I am trying out Fedora Core 6 in a VMWare image downloaded from thoughtpolice. I started apache using apachectl, created a test file in /var/www/html/test.html, tried to access it from a browser, and received a 403 Forbidden error.

This incantation solved the problem:

# chcon -R -t httpd_sys_content_t /var/www/html/

Sunday, November 26, 2006

Naming Exceptions

When defining a WebMethod for JAX-WS 2.0, it appears that the best naming convention is to leave off the word Exception when defining method-specific exceptions. For example:
  @WebMethod(operationName = "GetMember", action = "GetMember")
  public void getMember(@WebParam(name = "MemberID")
                        Long memberID,
                        @WebParam(name = "Member",
                                  mode = Mode.OUT)
                        Holder member) 
    throws InvalidMemberID {

    if (! isValid(memberID)) {
      throw new InvalidMemberID(memberID);
    }

    ...
  }

I originally named the exception InvalidMemberIDException, but this results in a SOAP fault with the same name in the WSDL, rather than just InvalidMemberID. The client tool wsimport will then use this WSDL to generate an exception named InvalidMemberIDException_Exception.

Monday, October 23, 2006

Implementing Web Service Security: Accessing a Secure Service

In my last post I finished a handler to verify the signature in a SOAP message following a simple web service security scheme.

Next I was ready to use JAX-WS 2.0 to create a client to call my secure service. The wsimport tool does the work of generating the client code, but I needed to figure out how to insert my custom security element into the message header before it gets sent to the server.

The process of preparing to send a message from the client should look something like this:

  1. Create the message body, specifying the operation and relevant data or parameters.
  2. Add a SOAP Header element to the message if it does not already have one.
  3. Add a Security element to the header.
  4. Add the Timestamp and AccessID elements to the Security element.
  5. Create the Signature element based on the access ID, timestamp and canonicalized text of the message body and add it to the Security element.

The client code that invokes a specific web service operation should only have to worry about the first step of this process. The remaining steps are the same for all operations and should be handled transparently from the viewpoint of the caller.

The message processing pipeline for JAX-WS is similar whether you are sending or receiving messages. A message always passes through a handler chain before and after the message is actually processed. In this case, I needed to create a handler to insert the security element into the message header as described above.

My signature creation handler extended the same base handler I used on the server. In this handler the action happens in the handleOutbound method:

package member.client;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import java.util.logging.Logger;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;

import javax.xml.ws.handler.soap.SOAPMessageContext;

import member.common.Canonicalizer;
import member.common.Signature;
import member.common.BaseHandler;
import member.common.Timestamp;

/*
 * Adds a signature to outbound messages.
 */
public class SignatureCreationHandler extends BaseHandler {

  /**
   * TO DO: replace these with configuration properties
   **/
  private static final String ACCESS_ID = "karl";
  private static final String SECRET_KEY = "secret";

  private static final Logger log = Logger.getLogger("member");

  public boolean handleOutbound(SOAPMessageContext smc) throws SOAPException {

    SOAPMessage message = smc.getMessage();
    SOAPPart part = message.getSOAPPart();
    SOAPEnvelope envelope = part.getEnvelope();
    
    SOAPBody body = envelope.getBody();
    if (body == null) { 
      body = envelope.addBody();
    }
    
    SOAPHeader header = envelope.getHeader();
    if (header == null) {
      header = envelope.addHeader();
    }

    addSecurityElement(header, body);

    return true;
  }

  private void addSecurityElement(SOAPHeader header, SOAPBody body) 
    throws SOAPException {

    QName qName = getQName(body, SECURITY_ELEMENT);
    if (qName == null) { return; }
    SOAPHeaderElement securityElement = header.addHeaderElement(qName);
    
    qName = new QName("", TIMESTAMP_ELEMENT, "");
    SOAPElement timestampElement = securityElement.addChildElement(qName);
    
    String timestamp = Timestamp.create();
    timestampElement.addTextNode(timestamp);
    
    qName = new QName("", ACCESS_ID_ELEMENT, "");
    SOAPElement clientIDElement = securityElement.addChildElement(qName);
    clientIDElement.addTextNode(ACCESS_ID);

    qName = new QName("", SIGNATURE_ELEMENT, "");
    SOAPElement signatureElement = securityElement.addChildElement(qName);

    String bodyText = Canonicalizer.toText(body);
    String signedText = timestamp + ":" + ACCESS_ID + ":" + bodyText;

    String signature = Signature.create(SECRET_KEY, signedText);

    signatureElement.addTextNode(signature);
  }
}

Once the handler was written, the next challenge was to incorporate it into the service endpoint interface (SEI) that the wsimport tool generates based on a WSDL file. The endpoint interface is what a client uses to access the service.

To customize the endpoint interface, I created a binding file declaring a handler-chains element and passed it to the wsimport tool. The tool then added the specified nodes to the WSDL before generating the SEI:

<bindings 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    wsdlLocation="http://localhost:8082/member?wsdl"
    xmlns="http://java.sun.com/xml/ns/jaxws">    
  <bindings node="wsdl:definitions">
    <package name="member.client"/>
  </bindings>
  <bindings node="wsdl:definitions" 
    xmlns:javaee="http://java.sun.com/xml/ns/javaee">
    <javaee:handler-chains>
      <javaee:handler-chain>
 <javaee:handler>
   <javaee:handler-class>member.client.SignatureCreationHandler</javaee:handler-class>
 </javaee:handler>
      </javaee:handler-chain>
    </javaee:handler-chains>
  </bindings>
</bindings>

After running the wsimport tool, I was able to use the generated classes to run a simple test program that successfully executed an operation on the secure service:

package member.client;

import java.math.BigInteger;

import java.util.List;
import java.rmi.RemoteException;

public class Client {

  public static void main (String[] args) throws Exception {

    MemberService service = new MemberService();
    MemberPort port = service.getMemberServicePort();

    BigInteger memberID = new BigInteger("2");

    List sn = port.memberGroups(memberID);
  }
}

As illustrated by this simple test program, the handler manages security transparently for the client.

Implementing Web Service Security: Verifying the Message Signature

In my last post I sketched out some of the implementation details for a simple web service security scheme. Next I wanted to flesh out the implementation and incorporate it into my sample web service.

The process of verifying a message from the client should look something like this:

  1. Receive the message and parse it into a DOM.
  2. Retrieve the security element from the header.
  3. Retrieve the signature submitted by the client from the security element.
  4. Recreate the signature based on the submitted access ID, timestamp and canonicalized text of the message body (along with the shared secret key).
  5. If the signatures match, allow processing to continue. Otherwise generate a fault and abort processing.

The verification process should happen transparently from the standpoint of any particular web service operation, such as the getMembers method I wrote about earlier.

JAX-WS provides handlers to support this type of transparent pre- or post-processing. A handler that has access to the entire SOAP message (including the header) must implement the javax.xml.ws.handler.soap.SOAPHandler interface.

The SOAPHandler interface honestly seems a bit odd to me. The same method handleMessage is invoked on each message both before and after processing (See the Tech Tip for some good diagrams). Except in the seemingly unlikely event that you want the same behavior in both these situations, you need to test a context property and split the implementation of handleMessage into two branches. There is probably an explanation for this, but for the moment it is lost on me.

I started by implementing a generic starting point for this and other handlers I might need to write. The base handler simplifies handling of either the inbound or outbound processing phase:

package member.common;

import java.util.Iterator;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.*;

import javax.xml.ws.handler.soap.SOAPMessageContext;

public abstract class BaseHandler 
  implements SOAPHandler {

  protected static final String SECURITY_ELEMENT = "Security";
  protected static final String ACCESS_ID_ELEMENT = "AccessID";
  protected static final String TIMESTAMP_ELEMENT = "Timestamp";
  protected static final String SIGNATURE_ELEMENT = "Signature";

  private static SOAPFactory factory;

  public Set getHeaders() {
    return null;
  }

  public boolean handleMessage(SOAPMessageContext smc) {

    try {
      return isOutbound(smc) ? handleOutbound(smc) : handleInbound(smc);
    } catch (SOAPException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Handles pre-processing of messages before being sent by a client
   * or processed by a server.
   **/
  protected boolean handleInbound(SOAPMessageContext smc) 
    throws SOAPException {
    return true;
  }

  /**
   * Handles post-processing of messages after being received by a client
   * or processed by a server.
   **/
  protected boolean handleOutbound(SOAPMessageContext smc) 
    throws SOAPException {
    return true;
  }

  public boolean handleFault(SOAPMessageContext smc) {
    return true;
  }
    
  public void close(MessageContext messageContext) {
  }
    
  /**
   * Tests whether the message is being sent or received.
   **/
  private boolean isOutbound(SOAPMessageContext smc) {

    Boolean outboundProperty = (Boolean)
      smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        
    return outboundProperty.booleanValue();
  }

  /**
   * Returns a factory required for generating SOAP faults.
   **/
  protected SOAPFactory getFactory() throws SOAPException {

    if (factory == null) {
      factory = SOAPFactory.newInstance();
    }

    return factory;
  }

  /**
   * Returns a qualified name based on the namespace declared by the
   * root element in the SOAP body.
   **/
  protected QName getQName(SOAPBody body, String localName) {

    Iterator i = body.getChildElements();
    if  (! i.hasNext()) { return null; }

    SOAPElement methodElement = (SOAPElement) i.next();
    String namespaceURI = methodElement.getNamespaceURI();
    String prefix = methodElement.getPrefix();

    return new QName(namespaceURI, localName, prefix);    
  }
}

Once I had the base handler in hand, I could write a signature verification handler focused exclusively on pre-processing of each incoming message. The handleInbound method does the actual work:

package member.server;

import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import java.util.logging.Logger;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;

import javax.xml.ws.soap.SOAPFaultException;

import javax.xml.ws.handler.soap.SOAPMessageContext;

import member.common.Canonicalizer;
import member.common.Signature;
import member.common.BaseHandler;
import member.common.Timestamp;

/*
 * Verifies the signature on inbound messages.
 */
public class SignatureVerificationHandler extends BaseHandler {

  private static final Logger log = Logger.getLogger("member");

  private static final long MAX_TIME_RANGE_MILLIS = 1000 * 60 * 5;

  public boolean handleInbound(SOAPMessageContext smc) throws SOAPException {

    SOAPMessage message = smc.getMessage();
    SOAPPart part = message.getSOAPPart();

    SOAPEnvelope envelope = part.getEnvelope();
    
    SOAPBody body = envelope.getBody();
    if (body == null) { 
      throw new RuntimeException("Missing required message body.");
    }
    
    SOAPElement securityElement = getSecurityElement(envelope);
    String accessID = getChildText(securityElement, ACCESS_ID_ELEMENT);
    String timestamp = getChildText(securityElement, TIMESTAMP_ELEMENT);
    Date submittedDate = Timestamp.parse(timestamp);
    
    long timeRangeMillis = 
      Math.abs(System.currentTimeMillis() - submittedDate.getTime());
    if (timeRangeMillis > MAX_TIME_RANGE_MILLIS) {
      SOAPFault fault = 
        getFactory().createFault("Timestamp outside allowed range",
     getQName(body, "InvalidTimestamp"));
      throw new SOAPFaultException(fault);
    }
    
    String submittedSignature = 
      getChildText(securityElement, SIGNATURE_ELEMENT);
    
    String bodyText = Canonicalizer.toText(body);
    String signedText = timestamp + ":" + accessID + ":" + bodyText;
    String secretKey = getSecretKey(accessID);
    String verifiedSignature = Signature.create(secretKey, signedText);
    
    if (! submittedSignature.equals(verifiedSignature)) {
      SOAPFault fault = 
        getFactory().createFault("Failed to verify the message signature",
     getQName(body, "InvalidSignature"));
      throw new SOAPFaultException(fault);
    }

    return true;
  }

  private SOAPElement getSecurityElement(SOAPEnvelope envelope) 
    throws SOAPException {

    SOAPHeader header = envelope.getHeader();

    if (header == null) {
      throw new RuntimeException("Missing required message header");
    }

    QName qName = getQName(envelope.getBody(), SECURITY_ELEMENT);
    if (qName == null) {
      throw new RuntimeException("Missing required method element.");
    }

    Iterator i = header.getChildElements(qName);
    if (! i.hasNext()) {
      throw new RuntimeException("Missing required security element.");
    }

    SOAPElement securityElement = (SOAPElement) i.next();

    return securityElement;
  }

  private String getChildText(SOAPElement element, String name) {
    
    Iterator i = element.getChildElements(new QName("", name, ""));
    if (! i.hasNext()) {
      return "";
    }
    
    SOAPElement child = (SOAPElement) i.next();

    Node textNode = child.getFirstChild();
    if (textNode == null) { 
      return ""; 
    }

    if (textNode.getNodeType() != Node.TEXT_NODE) {
      return "";
    }

    return ((CharacterData) textNode).getData();
  }

  /**
    * Placeholder method.
    **/
  private String getSecretKey(String accessID) {
    return "secret";
  }
}

The handler throws a fault if the timestamp or the signature is invalid. A real implementation of the getSecretKey method would also throw a fault if the access ID was invalid.

To add the handler to the message processing chain, I created a handlers.xml file in the member.server package directory:

<?xml version="1.0" encoding="UTF-8"?>

<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
  <handler-chain>
    <handler>
      <handler-class>member.server.SignatureVerificationHandler</handler-class>
    </handler>
  </handler-chain>
</handler-chains>

This apparently gets picked up by the apt tool and applied to all the web services in that package.

Before I could actually test the security scheme, I had to create a a client that would supply the correct security element in the message header. Keep reading for the shocking finale.

Sunday, October 22, 2006

Implementing Web Service Security

In my last post I looked into adding a security scheme to the example web service I implemented with JAX-WS 2.0.

I shied away from WS-Security because I wanted my service to be easy to access even with a minimal toolkit, without compromising authentication, confidentiality or data integrity. I opted to rely on SSL as the transport mechanism, and to define my own concise notation for signing each message with a shared secret key:

<S:Envelope xmlns:S="http://www.w3.org/2001/12/soap-envelope"
                 xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <S:Header>
    <ns1:Security>
      <AccessID>karl</AccessID>
      <Timestamp>2005-01-31T23:59:59.183Z</Timestamp>
      <Signature>DJbchm5gK...</Signature>
    </ns1:Security>
  </S:Header>
  <S:Body Id="SoapBody">
    <ns1:MemberGroups>
      <MemberID>2</MemberID>
    </ns1:MemberGroups>
  </S:Body>
</S:Envelope>

The elements of the scheme are:

AccessID A unique identifier assigned to each user who is authorized to access the web service.
Timestamp The current time to millisecond precision. The timestamp submitted by the user must be expressed in the UTC time zone, and must be within 5 minutes of the server clock for the message to be authenticated successfully.
Signature A message authentication code (MAC) generated using the HMAC_SHA1 algorithm, based on a shared secret key and a text string.

The text string used to generate the MAC has the following form:

Timestamp:AccessID:CanonicalizedMessage

The canonicalized message is the content of the Body element from the SOAP message. All spaces between elements must be stripped, and the top-level element must only have a namespace prefix. For example:

2006-09-28T07:05:07.484Z:karl:<ns1:MemberGroups><MemberID>2</MemberID></ns1:MemberGroups>

To simplify the creation of the security element for outbound messages from the client, as well as the authentication of inbound messages to the server, I wrote three small utility classes described below.

The simplest utility to handle the timestamp element of the security header. The timestamp string must conform to the standard Internet time/date format, and must furthermore be in the UTC time zone:

package member.common;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;

import java.util.Date;
import java.util.TimeZone;

/**
 * Creates and parses timestamps in IETF RFC 3339 format.
 **/
public class Timestamp {

  private static DateFormat getFormatter() {

    DateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
    f.setTimeZone(TimeZone.getTimeZone("UTC"));

    return f;
  }

  public static String create() {
    return getFormatter().format(new Date());
  }

  public static Date parse(String timestamp) {
    try {
      return getFormatter().parse(timestamp);
    } catch (ParseException e) {
      throw new RuntimeException("Invalid timestamp " + timestamp);
    }
  }
}

Generating a canonicalized version of the message is simple as long our schema consists of simple elements without attributes:

package member.common;

import java.util.Iterator;

import org.w3c.dom.CharacterData;
import org.w3c.dom.Node;
import javax.xml.soap.SOAPElement;

/**
 * Transforms the contents of a SOAP element into a standardized text
 * string.
 **/
public class Canonicalizer {

  public static String toText(SOAPElement element) {
    
    StringBuilder buffer = new StringBuilder();
    appendElement(buffer, element.getChildElements());

    return buffer.toString();
  }

  private static void appendElement(StringBuilder buffer, Iterator i) {
    
    while (i.hasNext()) {
      
      Node node = (Node) i.next();
      if (node.getNodeType() == Node.TEXT_NODE) {

        buffer.append(((CharacterData) node).getData());

      } else if (node instanceof SOAPElement) {

        SOAPElement element = (SOAPElement) node;
        String tag = element.getTagName();

        buffer.append("<").append(tag).append(">");
        appendElement(buffer, element.getChildElements());
        buffer.append("");
      }
    }
  }
}

Creating the signature is also a straightforward application of the HMAC_SHA1 algorithm:

package member.common;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Signature {

  private static final String MAC_ALG = "HmacSHA1";
  private static final String DIGEST_ALG = "SHA-1";

  /**
   * Creates a messsage authentication code for a text string.
   *
   * @param key A shared key
   * @param message A text message
   * @return A hex string representing the message authentication code
   **/
  public static String create(String key, String message) {

    try {

      byte[] keyBytes = key.getBytes("UTF-8");
      byte[] messageBytes = message.getBytes("UTF-8");

      Mac mac = Mac.getInstance(MAC_ALG);
      mac.init(new SecretKeySpec(keyBytes, MAC_ALG));

      byte[] signature = mac.doFinal(messageBytes);

      return toHex(signature);

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Converts a binary messsage authentication code into a hex string.
   **/
  public static String toHex(byte[] bytes) {
    
    StringBuffer buffer = new StringBuffer(bytes.length * 2);
    for (int i = 0; i < bytes.length; i++) {

      int b = bytes[i];
      int hi = (b & 0xF0) >>> 4;
      buffer.append(Character.forDigit(hi, 16));
      int lo = b & 0x0F;
      buffer.append(Character.forDigit(lo, 16));
    }

    return buffer.toString();
  }
}

My next post will focus on using these utility classes to add a security header to outbound messages from a client.

Sunday, September 24, 2006

Web Service Security

In my last two posts I used the JAX-WS 2.0 API to implement a simple web service that I can access with either a SOAP or REST request.

The next thing I wanted to do was to add security to the service:

  1. Authentication. The sender and recipient should both be able to verify each other's identiy.
  2. Confidentiality. The data exchanged between the sender and recipient should be kept confidential from anyone watching network traffic.
  3. Data integrity. The sender and recipient should both have a way to verify that no one has tampered with the data they exchange.

I wanted to meet these security requirements without compromising interoperability, including access via REST. Consumers should be still able to access the service with a minimal toolkit consisting of an HTTP client, an XML parser and commonly used cryptographic functions.

My initial thought was to use the WS-Security standard as implemented by the XWSS project. This project builds in turn on the XML Digital Signature and XML Encryption APIs.

The focus of WS-Security seems to be on message-based security. The goal is to provide a standard for signing and encrypting all or part of a SOAP message regardless of the underlying transport mechanism (HTTP, SMTP, JMS, etc.). It avoids any reliance on transport-based security, notably SSL.

The complexity of the WS-Security standard seemed to pose an obstacle to ensuring universal access to my service. I looked at the public web services offered by a number of major companies and found that none of them used WS-Security. Instead they all used a mix of more established protocols and algorithms, along with their own simple conventions. Most of their approaches could be expressed using the WS-Security schema, but I have not seen a major public service that uses it.

Common security schemes for public web services seem to take one of two forms:

  1. Signature Verification. Consumers generate a unique signature for each request based on the submitted data, a timestamp (to prevent a replay attack) and a secret key (typically using the HMAC_SHA1 algorithm). The service re-generates the signature for each request and verifies a match.
  2. Session Authentication. A consumer always starts by submitting a user identifier a key or password to an authentication service. The service responds with a short-lived session id or token for authenticating subsequent requests.

Session authentication not guarantee data integrity by itself, but may be combined with signature verification to achieve that goal. Such hybrid schemes generate signatures with the temporary session token rather than with a permanent key.

Both schemes rely on SSL to encrypt requests and responses and therefore ensure confidentiality.

One drawback to the simple signature verification scheme using HMAC seems to be that it requires the server to permanently store a retrievable secret key for each consumer. That could be avoided by generating an RSA public and private key pair for each consumer and using that to sign messages instead. With RSA the server would only have to store the public key for each consumer. Despite this advantage the major services seem to use HMAC, perhaps because RSA implementations are not as widely available.

Hybrid schemes avoid the problem of storing a secret key because login passwords can be stored in hashed form. However, I am not sure how inconvenient it would be to require the initial login request as part of each service invocation.

After researching this I decided to start by implementing signature verification. Using WS-Security, requests to my example web service would look something like this:

<S:Envelope xmlns:S="http://www.w3.org/2001/12/soap-envelope"
                 xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <S:Header>
    <wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/04/secext">
      <wsse:UsernameToken Id="AccessKey">
        <wsse:Username>karl</wsse:Username>
      </wsse:UsernameToken>
      <ds:Signature>
        <ds:SignedInfo>
          <ds:CanonicalizationMethod 
            Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod 
            Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
          <ds:Reference URI="#SoapBody">
            <ds:DigestMethod 
              Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>LyLsF0Pi4wPU...</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>DJbchm5gK...</ds:SignatureValue>
      </ds:Signature>
    </wsse:Security>
  </S:Header>
  <S:Body Id="SoapBody">
    <ns1:MemberGroups>
      <MemberID>2</MemberID>
    </ns1:MemberGroups>
  </S:Body>
</S:Envelope>

I would have to fit the timestamp in there somewhere as well. If I were just specifying my own notation, the request would look like this:

<S:Envelope xmlns:S="http://www.w3.org/2001/12/soap-envelope"
                 xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <S:Header>
    <ns1:Security>
      <AccessID>karl</AccessID>
      <Timestamp>2005-01-31T23:59:59.183Z</Timestamp>
      <Signature>DJbchm5gK...</Signature>
    </ns1:Security>
  </S:Header>
  <S:Body Id="SoapBody">
    <ns1:MemberGroups>
      <MemberID>2</MemberID>
    </ns1:MemberGroups>
  </S:Body>
</S:Envelope>

The only significant piece of information missing from my simpler notation is a "canonicalization method." Standardization of XML attributes and elements does not seem necessary as long as the service mandates a few simple conventions:

  1. All requests use UTF-8 character encoding.
  2. Request elements such as MemberGroups never have attributes.
  3. Request elements do not use namespace prefixes.
  4. No white space between elements.

The extra verbosity of WS-Standard seems warranted if it makes it easier for most consumers to use the service. I am not sure this is the case.

Thursday, September 21, 2006

A REST interface for a SOAP Web Service

In my last post I created a simple web service using JAX-WS 2.0.

The next thing I wanted to do was to make the same service accessible via a REST interface in addition to SOAP.

I expected my web service to receive REST requests that look like this:

GET /member/MemberGroups?MemberID=2

I expected the responses to these requests to look like this:

<ns1:MemberGroupsResponse>
  <MemberGroup>
    <ID>1</ID>
    <name>Orcs</name>
  </MemberGroup>
  <MemberGroup>
    <ID>2</ID>
    <name>Elves</name>
  </MemberGroup>
</ns1:MemberGroupsResponse>

I was hoping to do this simply by adding another annotation to my web service method. I looked at the REST sample provided with the JAX-WS 2.0 reference implementation, but it seemed cumbersome. I wanted a mechanism that would provide me with an automatic REST interface to each of my web service methods without having to do any additional work.

It occurred to me that a simple servlet filter sitting in front of the SOAP servlet might do the trick, as long as I was willing to stick to a few structural conventions:

  1. The URL of each REST method has the form SOAPaddress/SOAPoperation.
  2. It must be possible to map query parameters for each RESTful GET method into a valid SOAP request consisting of a flat list of child elements within the operation element.
  3. It must be possible to transform bare XML content submitted to a RESTful POST method into a valid SOAP request by wrapping the content in a soapenv:Body element.
  4. The return value for each REST method must be the contents of the soapenv:Body element in the SOAP response.
  5. All service methods must be capable of handling the simple messages received from REST requests, without any additional header, body or envelope information.

Here is the filter that I wrote:

package member.server;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Adapts REST requests to SOAP
 **/
public class RestFilter implements Filter {

  private static final String FORM_TYPE = "application/x-www-form-urlencoded";

  private FilterConfig filterConfig = null;
  private String prolog = null;

  public void init(FilterConfig filterConfig) throws ServletException {

    this.filterConfig = filterConfig;

    String ns = filterConfig.getInitParameter("namespace");
    if (ns == null) {
      throw new IllegalStateException("RestFilter requires a " +
          "namespace init parameter");
    }

    prolog = "<?xml version=\"1.0\"?><soapenv:Envelope " +
      "xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
      "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
      "xmlns:ns1=\"" + ns + "\">";
  }

  public void destroy() {
    filterConfig = null;
  }

  public void doFilter(ServletRequest request, ServletResponse response, 
         FilterChain chain) 
    throws IOException, ServletException {

    if (filterConfig == null) { return; }

    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    String contentType = request.getContentType();
    String method = httpRequest.getMethod();
    boolean isMeta = httpRequest.getParameter("wsdl") != null || 
      httpRequest.getParameter("xsd") != null;
    boolean isForm = 
      ("POST".equals(method) && FORM_TYPE.equals(contentType));

    boolean isRest = ((! isMeta && "GET".equals(method)) || isForm);

    if (! isRest) {

      // not a REST call, so proceed directly to the SOAP servlet
      chain.doFilter(request, response);

    } else {

      // transform the REST request into a SOAP request
      RestRequestWrapper restRequest = 
 new RestRequestWrapper(httpRequest, prolog);

      // extract the body from the SOAP response and return it raw
      RestResponseWrapper restResponse = 
 new RestResponseWrapper(httpResponse, restRequest.getOperation());

      ServletContext sc = filterConfig.getServletContext();
      RequestDispatcher rd = 
 sc.getRequestDispatcher(restRequest.getPortPath());
      rd.forward(restRequest, restResponse);

      restResponse.commit();
    }
  }
}

The filter check for REST requests based on the content type and HTTP method. If the request is a GET or it is a form submission, then the filter routes the request through the REST interface. The only exceptions are for WSDL or XML Schema documents, which pass directly to the SOAP servlet just like SOAP messages.

The request wrapper transforms the request into a SOAP message that can be passed to the SOAP servlet:

package member.server;

import java.io.*;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.Map;
import java.util.Vector;

import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Transforms REST requests into SOAP requests.
 *
 * TO DO: handle POST requests
 **/
public class RestRequestWrapper extends HttpServletRequestWrapper {

  private static final Vector EMPTY_VECTOR = new Vector();

  private String portPath;
  private String operation;
  private String message;
  private Hashtable<String, Vector> headers;

  public RestRequestWrapper(HttpServletRequest request, String prolog) {

    super(request);

    String requestURI = request.getRequestURI();

    int slash = requestURI.lastIndexOf('/');
    if (slash == -1 || slash == 0 || slash == requestURI.length() - 1) { 
      return; 
    }
    
    portPath = requestURI.substring(0, slash);
    operation = requestURI.substring(slash + 1);

    StringBuilder builder = new StringBuilder(2000);
    builder.append(prolog);
    builder.append("<soapenv:Body>");
    builder.append("<ns1:").append(operation).append(">");

    Iterator i = request.getParameterMap().entrySet().iterator();    

    while (i.hasNext()) {

      Map.Entry entry = (Map.Entry) i.next();

      String name = (String) entry.getKey();
      String[] values = (String[]) entry.getValue();

      for (int j = 0; j < values.length; j++) {
        builder.append("<").append(name).append(">");
        builder.append(values[j]);
        builder.append("</").append(name).append(">");
      }
    }

    builder.append("</ns1:").append(operation).append(">");
    builder.append("</soapenv:Body>");
    builder.append("</soapenv:Envelope>");

    message = builder.toString();

    buildHeaders(request);
  }

  String getPortPath() {
    return portPath;
  }

  String getOperation() {
    return operation;
  }
  
  public String getMethod() {
    return "POST";
  }

  private void buildHeaders(HttpServletRequest request) {

    headers = new Hashtable<String, Vector>();

    for (Enumeration e = request.getHeaderNames(); e.hasMoreElements();) {

      String name = (String) e.nextElement();
      Vector v = new Vector();
      Enumeration f = request.getHeaders(name);

      while (f.hasMoreElements()) {
        v.add(f.nextElement());
      }

      headers.put(name.toLowerCase(), v);
    }

    setHeader("content-type", getContentType());
    setHeader("content-length", Integer.toString(getContentLength()));
    setHeader("soapaction", '"' + operation + '"');
  }

  private void setHeader(String name, String value) {

    Vector v = new Vector();
    v.add(value);
    headers.put(name, v);
  }

  public Enumeration getHeaderNames() {
    return headers.keys();
  }

  public Enumeration getHeaders(String name) {

    Vector v = headers.get(name.toLowerCase());
    return (v == null) ? EMPTY_VECTOR.elements() : v.elements();
  }

  public String getHeader(String name) {

    Enumeration e = getHeaders(name);
    return e.hasMoreElements() ? (String) e.nextElement() : null;
  }

  public int getIntHeader(String name) throws NumberFormatException {

    String value = getHeader(name);
    return (value == null) ? -1 : Integer.parseInt(value);
  }

  public String getContentType() {
    return "text/xml; charset=utf-8";
  }

  public String getCharacterEncoding() {
    return "UTF-8";
  }

  public int getContentLength() {
    return message.length();
  }

  public ServletInputStream getInputStream() throws IOException {

    try {
      return new ByteArrayServletInputStream(message.getBytes("UTF-8"));
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }
}

Besides streaming the SOAP message to the servlet, the request wrapper must also ensure that servlet receives the appropriate headers.

The response wrapper simply accumulates the response generated by the SOAP servlet, and returns only the body to the client:

package member.server;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Extracts the body of SOAP response for returning as bare XML.
 *
 * TO DO: buffer response stream to handle larger responses
 **/
public class RestResponseWrapper extends HttpServletResponseWrapper {

  private static final String START_TAG = "<soapenv:Body>";
  private static final String END_TAG = "</soapenv:Body>"; 

  private StringWriter buffer = new StringWriter();
  private PrintWriter charOut = new PrintWriter(buffer);

  private ByteArrayServletOutputStream byteOut = 
    new ByteArrayServletOutputStream();

  private boolean isCommitted = false;

  public RestResponseWrapper(HttpServletResponse response, String operation) {
    super(response);
  }

  public PrintWriter getWriter() throws IOException {
    return charOut;
  }

  public ServletOutputStream getOutputStream() throws IOException {
    return byteOut;
  }

  public void flushBuffer() {
    // Prevent flushing by servlet
  }

  public boolean isCommited() {
    return isCommitted;
  }

  void commit() throws IOException {

    String message = byteOut.toString("UTF-8");
    int bodyStart = message.indexOf(START_TAG);
    int bodyEnd = message.indexOf(END_TAG);

    if (bodyStart != -1 && bodyEnd != -1) {
      message = message.substring(bodyStart + START_TAG.length(), bodyEnd);
    }

    setCharacterEncoding("UTF-8");
    setContentLength(message.length());
    setContentType("text/xml; charset=UTF-8");

    getResponse().getOutputStream().print(message);
    getResponse().flushBuffer();

    isCommitted = true;
  }
}

To test my filter, I added it to the web.xml for the web service I developed in my last post:

<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee">
  <listener>
    <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class>
  </listener>
   <filter>
     <filter-name>RestFilter</filter-name>
     <filter-class>member.server.RestFilter</filter-class>
     <init-param>
      <param-name>namespace</param-name>
      <param-value>http://xocoatl.blogspot.com/schema/1/</param-value>
    </init-param>
   </filter>
   <filter-mapping>
    <filter-name>RestFilter</filter-name>
    <url-pattern>/*</url-pattern>
   </filter-mapping>  
   <servlet>
    <servlet-name>member</servlet-name>
    <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>member</servlet-name>
    <url-pattern>/member</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>60</session-timeout>
  </session-config>
</web-app>

I compiled the filter along with the simple web service, deployed it to Resin, and was then able to talk to my service using either REST or SOAP.

Sunday, September 17, 2006

JAX-WS 2.0

This is a walkthrough of my first experience with the Java API for XML Web Services 2.0 (JAX-WS 2.0).

My objective was to create a web service with a single method that returns the groups to which a member belongs. JAX-WS gives you two basic ways to build a web service:

  1. Write a WSDL and use JAX-WS to generate a Java interface for you to implement.
  2. Write a set of Java classes and use annotations to control how JAX-WS maps your code to a web service.

I opted for the second approach. It was appealing to think that I could just write some code and turn it into a web service simply by adding some annotation.

I expected my web service to receive requests that look like this:

<?xml version="1.0" ?>
  <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                    xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
    <soapenv:Body>
      <ns1:MemberGroups>
        <MemberID>2</MemberID>
      </ns1:MemberGroups>
    </soapenv:Body>
</soapenv:Envelope>

I expected my web service to send responses that look like this:

<?xml version="1.0" ?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                  xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <soapenv:Body>
    <ns1:MemberGroupsResponse>
      <MemberGroup>
        <ID>1</ID>
        <name>Orcs</name>
      </MemberGroup>
      <MemberGroup>
        <ID>2</ID>
        <name>Elves</name>
      </MemberGroup>
    </ns1:MemberGroupsResponse>
  </soapenv:Body>
</soapenv:Envelope>

To complicate matters slightly, I wanted to run the web service in Resin, rather than Tomcat or the Sun Java Application Server.

My development environment includes:

  1. A Windows XP laptop
  2. cygwin and Xemacs
  3. JDK 1.5
  4. ant

I do not use an IDE, so I needed to be able to code my web service in emacs and deploy it from the command line.

Getting Started

I downloaded the JAX-WS 2.0 reference implementation and expanded it.

The samples directory contains a number of good examples. I was particularly interested the fromjava-* examples, including supplychain.

The documentation does not explain how to run the fromjava samples from the command line. Here is what I figured out:

  1. Set JAXWS_HOME to the JAX-WS installation directory.
  2. Go to the samples/fromjava directory.
  3. Build and run the web service using the server-j2se task:
    $ ant server-j2se
    Buildfile: build.xml
    Trying to override old definition of task apt
    
    setup:
    
    server-j2se:
    
    clean:
       [delete] Deleting directory C:\Docs\jaxws-ri\samples\fromjava\build
    
    setup:
        [mkdir] Created dir: C:\Docs\jaxws-ri\samples\fromjava\build
        [mkdir] Created dir: C:\Docs\jaxws-ri\samples\fromjava\build\classes
        [mkdir] Created dir: C:\Docs\jaxws-ri\samples\fromjava\build\war
    
    build-server-java:
          [apt] warning: Annotation types without processors: [javax.xml.bind.annotation.XmlRootElement, javax.xml.bind.annotation.XmlAccessorType, javax.xml.bind.annotation.XmlType, javax.xml.bind.annotation.XmlElement]
          [apt] 1 warning
         [echo] Starting endpoint... To stop: ant server-j2se-stop 
    
    BUILD SUCCESSFUL
    Total time: 6 seconds
    $ 

    The service runs fine despite the annotation warning, but it exits immediately. To work around this I simply put the main thread to sleep for a few minutes after starting the server:

    public class AddWebservice {
        
        public static void main (String[] args) throws Exception {
            Endpoint endpoint = Endpoint.publish (
                "http://localhost:8080/jaxws-fromjava/addnumbers",
                new AddNumbersImpl ());
    
            // Stops the endpoint if it receives request http://localhost:9090/stop
            new EndpointStopper(9090, endpoint);
    
     Thread.sleep(60000); // Addded to keep the JVM from exiting
        }
    }
  4. Start another shell to run the client.
  5. First test the server by requesting the WSDL:
    $ wget http://localhost:8080/jaxws-fromjava/addnumbers?wsdl
  6. Now build and run the client:
    $ ant client run

    The client is built by requesting the WSDL from the server and then generating a corresponding set of JavaBean classes for working with requests and responses.

Once the sample seemed to be running, I used the tcpmon tool from the Apache Axis project to intercept and read the actual SOAP messages between server and client:

  1. Download and run the tcpmon:
    $ java -cp axis.jar org.apache.axis.utils.tcpmon 8081 localhost 8080
  2. Go to the samples/fromjava/etc directory and change the port from 8080 to 8081 in build.properties, custom-client.xml and custom-schema.xml.
  3. Make sure the server is still running and then run the client again.

Implementing the Service

The supplychain sample seemed closest to what I wanted, so I copied that project as the basis for my experiment.

I was not able to get the SOAP messages to come out the way I wanted by modifying the code and annotations from the supplychain sample.

Finally I created a new project and imported the WSDL for the Amazon Simple Queue Service, since it has messages with the desired structure.

By studying the code generated from the Amazon SQS WSDL I was ultimately able to get what I wanted with only a few lines of code:

package member.server;

import java.math.BigInteger;

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

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebParam.Mode;
import javax.jws.WebService;
import javax.xml.ws.Holder;
import javax.xml.ws.RequestWrapper;
import javax.xml.ws.ResponseWrapper;

@WebService(name="MemberService", 
     targetNamespace = "http://xocoatl.blogspot.com/schema/1/")
public class MemberService {

  private static final long INVALID_ID = 1L;

  @WebMethod(operationName = "MemberGroups", action = "MemberGroups")
  public void getMembers(@WebParam(name = "MemberID")
    BigInteger memberID,
    @WebParam(name = "MemberGroup",
       mode = Mode.OUT)
    Holder<List<MemberGroup>> memberGroups) 
    throws InvalidMemberIDException {

    if (memberID.equals(BigInteger.valueOf(INVALID_ID))) {
      throw new InvalidMemberIDException("Invalid member ID");
    }

    memberGroups.value = new ArrayList();
    
    memberGroups.value.add(new MemberGroup(1, "Steelers"));
    memberGroups.value.add(new MemberGroup(1, "49ers"));
  }
}

MemberGroup is a simple unadorned JavaBean:

package member.server;

public class MemberGroup {
  
  private String name;
  private Integer id;

  public MemberGroup() {}

  public MemberGroup(Integer id, String name) {
    setID(id);
    setName(name);
  }
    
  public String getName() { 
    return name; 
  }

  public void setName(String name) { 
    this.name = name; 
  }
  
  public Integer getID() { 
    return id; 
  }

  public void setID(Integer id) { 
    this.id = id; 
  }
    
}

Note that the service method returns void and has an OUT parameter.

Deploying to Resin

For the final step, I generated the war file containing the service implementation:

$ ant clean build-server-java create-war

I copied the resulting war file to a fresh Resin instance with an empty webapps directory, and modified the server configuration to run my service as the root application:

<host id="" root-directory=".">
  <web-app id="/" document-directory="webapps/jaxws-member"/>
</host>

After a series of NoClassDefFoundError exceptions, I determined that the following jar files must be copied to the server lib directory:

  • jaxws-api.jar
  • jaxws-rt.jar
  • jsr173-api.jar
  • jsr181-api.jar
  • jsr250-api.jar
  • resolver.jar
  • saaj-api.jar
  • saaj-impl.jar
  • jaxb-api.jar
  • jaxb-impl.jar

Once that was sorted out, the web service ran fine.