XSUL Logo

Guide to Building Web Services with WS/XSUL

Introduction: The Basics

This guide is intended to allow you to write and use a web service with WS/XSUL as quickly as possible. You will see what steps are involved in writing a Web Service implementation and what are available ways to use it.

A web service is an entity identified by a URI, whose public interfaces and bindings are defined and described using XML based standard Web Service Definition Language (WSDL). Its definition can be discovered by other software systems. Client then interact with the it in a manner prescribed by its WSDL definition, using XML based messages conveyed by Internet protocols. The protocol we will use here is called SOAP over HTTP. SOAP, which used to stand for Simple Object Access Protocol, is a standard way to encode the XML message that is delivered to a web service. More about SOAP later.

We divide the process of building a service into following steps:

Obtaining source code

Currently code is only available in CVS (browse sample_decoder and interop sample source code in repository) and needs to be checked out:

cvs -d :pserver:anonymous@cvs.extreme.indiana.edu:/l/extreme/cvspub login
CVS password: cvsanon

cvs -d :pserver:anonymous@cvs.extreme.indiana.edu:/l/extreme/cvspub co xsul/interop
cvs -d :pserver:anonymous@cvs.extreme.indiana.edu:/l/extreme/cvspub co xsul/sample_decoder

After successful checkout of code from CVS you should build it and run tests. To do it make sure to have ANT 1.6.2 and run:

ant all selftest

You should see that build process succeeded and test service was executed without exception.

Designing the WSDL document for the Service

WS/XSUL implements "start with WSDL" approach to writing Web Services. This provides for a contract for Web Service described in WSDL and helps to achieve very good interoperability. You should either obtain WSDL that describes service you need to implement or write your own WSDL. Using existing WSDL file is a good idea. In the directory sample_decoder/config look at the file decoder-example.wsdl. A WSDL document consists of a set of definitions. They are:

Types in WSDL

The most complex of these is the "Types" definition. The first thing to notice is that the types element contains a single xml schema element and there are several elements to that schema. The first is our specification of our array of strings (view the latest version of WSDL).

<complexType name='ArrayOfString'>
  <sequence>
       <element name='item' type='xsd:string' maxOccurs='unbounded'/>
  </sequence>
</complexType>

Next we define a message type "DecoderParameters" which is used to describe input to our service: this type defines a sequence of elements : the "Topic", "CorrelationId", and so on.

<complexType name="DecoderParameters">
	<annotation><documentation xml:lang="en">
	  Type of input message: sequence of parameters.
	</documentation></annotation>

	<sequence>
		<element minOccurs="1" maxOccurs="1" name="Topic" type="xsd:string"/>
		<element minOccurs="1" maxOccurs="1" name="CorrelationId" type="xsd:string"/>
		<element minOccurs="1" maxOccurs="1" name="InputFile" type="xsd:string"/>
		<element minOccurs="1" maxOccurs="1" name="OutputDirectory" type="xsd:string"/>
		<element minOccurs="0" maxOccurs="1" name="StringArr" type="typens:ArrayOfString"/>

		<element name="nproc" type="xsd:int" minOccurs="0" maxOccurs="1" default="64" >
		  <annotation><documentation xml:lang="en">
				Example parameter with default value.
		  </documentation></annotation>
		</element>
	</sequence>
	<attribute name="SomeStringAttrib" type="string"/>
	<attribute name="SomeBoolAttrib" type="boolean"/>
</complexType>

Notice that each element can be provided with a documentation annotation. This annotation can be used by the client to provide a human readable interface to the service. The portal can provide an automatic way to do this.

Note: we have provided a default parameter value for one parameter:

<element name="nproc" type="xsd:int" minOccurs="0" maxOccurs="1" default="64" >

This allows clients for services with many values that the user may not want to change, to present the user with a default values. The minOccurs=0 means that the client can leave it out completely if they do not want to change the value.

The rest of the type schema defines an element that is called, "runDecoder" and is of type "typens:DecoderParameters" then a complex type for the return message as well as a element, "runDecoderResult" that is the output message from the decoder service.

<element name="runDecoder" type="typens:DecoderParameters"/>

<complexType name="DecoderResults">
	<annotation><documentation xml:lang="en">
	  Type of output message: now just one return parameter.
	</documentation></annotation>
	<sequence>
	  <element minOccurs="1" maxOccurs="1" name="Status" type="string"/>
	</sequence>
</complexType>

<element name="runDecoderResult" type="typens:DecoderResults"/>

This and the rest of the WSDL consists of element that you will not want to change except for the names. For your service simply replace "JythonService" everywhere with your service name.

xsdconfig and wsdlconfig files

The decoder-example.xsdconfig file is a small technical file that is used as part of the xmlbeans code generation which is described in the next section. You will need to create such a file for your service by copying them and modifying then in text editor. You may want also to create .wsdlconfig file (for example decoder-example.wsldconfig) if you want only to overwrite name of Java interface generated and do not interfere with automatic XmlBeans naming.

Generating Java Code from the WSDL

In order to build the service in Java, you need to translate the specification in the WSDL into java classes that you can use. These java classes generate and read all the xml documents that conform to you WSDL spec. The tool we use to do this is based on something called wsdlc, which provides a way to generate java from files from ANT build.xml script.

If you look in the top-level sample_decoder directory you will see "generated". In generated you will see a subdirectory called java_interfaces which contains the file DecoderPortType.java:

/* DO NOT MODIFY!!!! This file was generated automatically by xwsdlc (version 2.0.3-PRE3) */
package decoder.service;
public interface DecoderPortType {
    public decoder.xmlbeans.RunDecoderResultDocument runDecoder(decoder.xmlbeans.RunDecoderDocument input);
}

The xwsdlc ANT task generates JAR file with compiled java classes into generated/xmlbeans_typelib directory and puts the source code for the xmlbeans into generated/xmlbeans_typesrc directory (Java source code helps with debugging etc). Xwsdlc uses our config files and the wsdl files (and file that has .wsdl, .xsdconfig or .wsdlconfig extensions is used unless specified otherwise in ANT task).

The generate source code and JAR file are already in CVS but if make chnages to WSDL files you need to rerun xwsdlc task to regenerate code:

ant generate

Implementing the Service Back End

If you look at src/decoder you will see two directories:

If you want to make a new web service just create a new directory inside src/ here that is a copy of decoder.

The service implements a Java interface derived from the WSDL port type. The implementation of DecoderPortType is in is in the file DecoderImpl.java The service implements the service interface methods (in this case there is only one operation):

package decoder.service;

import decoder.xmlbeans.ArrayOfString;
import decoder.xmlbeans.DecoderParameters;
import decoder.xmlbeans.DecoderResults;
import decoder.xmlbeans.RunDecoderDocument;
import decoder.xmlbeans.RunDecoderResultDocument;

/**
 * Service is implementation of Decoder interface.
 */
public class DecoderImpl implements DecoderPortType {

	public RunDecoderResultDocument runDecoder(RunDecoderDocument inputMsg)
	{
		//extract parameters from input message
		DecoderParameters params = inputMsg.getRunDecoder();
		String topic = params.getTopic();
		String inputFile = params.getInputFile();
		String outputDirectory = params.getOutputDirectory();
		String[] arr = params.getStringArr().getItemArray();
		for (int i = 0; i < arr.length; i++) {
			System.out.println("arr["+i+"]="+arr[i]);
		}
		int nproc = params.getNproc();
		// do something with input
		// ... [PUT YOUR CODE HERE]
		// prepare response message
		RunDecoderResultDocument resultDoc = RunDecoderResultDocument.Factory.newInstance();
		DecoderResults result = resultDoc.addNewRunDecoderResult();
		result.setStatus("OK");
		return resultDoc;
	}
}

The important point to notice here is the way the parameters are extracted from the input message and the way the output message is constructed. We use the automatically generated xmlbeans classes. The RunDecoderDocument is the class generated from the schema in the wsdl. More specifically, for each top-level element in the schema we have a document class. In our wsdl we had two elements: runDecoder and runDecoderResults. The document class is simply a container for a sequence of these objects. So if the inputMsg is a document, we can get the contained element with the "getRunDecoder()" method which returns the first element. In this case its type was defined in our wsdl schema by <element name="runDecoder" type="typens:DecoderParameters"/> so the java class is DecoderParameters. This type has elements and attributes, such as Topic and CorrelationId, and we can extract them. If an argument is an array of strings Xmlbeans generates handy method to extract them as Java native arrays.

In our case the runDecoder method just takes input message and produces new output message that is returned. Real service would take input message and do something with it (such as invoking backend logic) and then would complete the method by creating a RunDecoderResultDocument as a return value.

To learn more about using XML Beans read introductionary article about XML Beans by David Bau (He also wrote set of excellent articles explaining The Design of XMLBeans part 1, part 2, and part 3). and then read Getting Started with XMLBeans and Tutorial: First Steps with XMLBeans followed by more detailed information on Methods for Types Generated From Schema, Java Types Generated from User-Derived Schema Types.

Additional resources including links to other articles are available at the XML Beans official site.

Start service

The file DecoderService.java is the main service. It is very simple and very easy to modify for another service (in fact just substitute DecoderService with your service name.) We step through this code very briefly.

public class DecoderService {
    private static HttpBasedServices httpServices;
    private static final String SERVICE_NAME = "decoder";

    /**
     * Launch service from command line
     */
    public static void main(String[] args) {
        XsulVersion.exitIfRequiredVersionMissing(XsulVersion.SPEC_VERSION); //sanity check
        XsulVersion.exitIfRequiredVersionMissing("2.0.3");
        int tcpPort = args.length > 0 ? Integer.parseInt(args[0]) : 0;
        httpServices = new HttpBasedServices(tcpPort);
        System.out.println("Server started on "+httpServices.getServerPort());
        String wsdlLoc = args.length > 1 ? args[1] : "config/decoder-example.wsdl";
        System.out.println("Using WSDL for service description from "+wsdlLoc);
        XService xsvc = httpServices.addService(
            new XmlBeansBasedService(SERVICE_NAME, wsdlLoc, new DecoderImpl()));
        xsvc.startService();
        System.out.println("Service started");
        System.out.println("Service WSDL available at "+getServiceWsdlLocation());
    }
}

The service expects a command line with a port number for an argument. A second argument can be used to specify an alternative location for where to find the service wsdl. The service first creates an "HttpBasedService" at the desired port. What this does is to create a mini http server running at that port. We then add a service to this server by means of the addService call. This adds an implementation of our DecoderPortType to this service with the name "decoder". It also makes the wsdl document for the service available through the URL "http://hostname:port/decoder?wsdl"

Before running make sure to set your CLASSPATH using one of handy classpath.* scripts:

Then start service, for example:

java decoder.service.DecoderService 3333

To start service with logging for service implementation to see what are input parameters:

java -Dlog=decoder.service:ALL decoder.service.DecoderService 3333

And to start service with all logging turned on to help debugging potential problems:

java -Dlog=:ALL decoder.service.DecoderService 3333

The Web Service Client

Static Client: Using XmlBeans Generated Code

DecoderClient uses classes generated by Xwsdlc to invoke Decoder Web Service:

private static void runClient(String[] args) throws Exception {
	URI base = ((new File(".")).toURI());

	if(args.length < 4) {
		usage("four args required: WSDL_URL topic inputFileURL outputDirURL");
	}

	String wsdlLoc = args[0];

	System.err.println("invoking operation runDecoder using WSDL from "
						   + wsdlLoc);

	WSIFClient wcl = XmlBeansWSIFRuntime.newClient(wsdlLoc);
	DecoderPortType stub = (DecoderPortType)wcl.generateDynamicStub(DecoderPortType.class);

	RunDecoderDocument inputMsg = RunDecoderDocument.Factory.newInstance();
	DecoderParameters params = inputMsg.addNewRunDecoder();
	params.setTopic(args[1]);
	params.setInputFile(args[2]);
	params.setOutputDirectory(args[3]);
	String[] arr = new String[]{"argA", "argB"};
	params.addNewStringArr().setItemArray(arr);
	params.setNproc(77); //override default
	// prepare response message
	RunDecoderResultDocument outputMsg = stub.runDecoder(inputMsg);
	DecoderResults result = outputMsg.getRunDecoderResult();
	System.out.println("status="+result.getStatus());
}

Dynamic Client: Generating XML-on-the fly

We have very simple to use WSDL dynamic invoker that works fine for simple types, for example:

java -Dlog=trace xsul.dii.XsulDynamicInvoker "http://localhost:3333/decoder?wsdl" runDecoder Topic File Dir 1

If you have complex types you need to generate XML and then apss it to the WSIF runtime The main program uses the xsul WSIFProvider which provides a generic way to talk to any service with only knowledge of the service's WSDL. The WSIF framework automatically generates all the client classes need to make the call on-the-fly. The only part that is specific to our application is in the runClient method: TODO: TBW fill in

The important thing to notice here is the way we create the input message. We are really creating an xml document on the fly. An interesting part in this example is given for the parameter that is an array of strings. In this case the appropriate code is

        WSIFMessage in = op.createInputMessage();
        WSIFMessage out = op.createOutputMessage();
        WSIFMessage fault = op.createFaultMessage();
        in.setObjectPart("Topic", args[1]);
        String argstring = "";
        for(int i = 2; i < args.length; i++) {
            argstring= argstring+"<item>"+args[i]+"</item>";
        }
        XmlElement sarray = builder.parseFragmentFromReader(
        new StringReader("<StringArr>"+argstring+"</StringArr>"));
        in.setObjectPart("StringArr",sarray);

In this case we are creating an array of strings by explicitly creating an xmlstring and parsing it into a basic XmlElement. To completely understand this one needs to study the XML Pull Parser API: this program begins with a declaration of a static object builder of class XmlInfosetBuilder. This is used to construct the xml objects needed to make the request.

Do you want to learn more?

Make sure to visit WS/XSUL main page, read source code, check interop sample, and contact us with your questions.


Aleksander Slominski
Last modified: $Id: index.html,v 1.7 2005/08/13 13:06:27 aslom Exp $
Valid XHTML 1.0!