Creating WS-RF Factory/Instance services with WSRFLite: an example with WS-Agreement

Roland Kübert

Revision History
Revision 0.327.11.2009rk - kuebert@hlrs.de
Third draft.
Revision 0.216.01.2009rk - kuebert@hlrs.de
Second draft.
Revision 0.114.01.2009rk - kuebert@hlrs.de
First draft.

Abstract

This is a follow-up tutorial to My first WSRFLiter service and describes how to develop a full-fledge WSRF web service for WSRFLite, the lightweight hosting environment for services using the Web Services Resource Framework (WSRF) and bundeled with UNICORE.

This article explains how to develop stateful web services with WSRFLite by showing how the WS-Agreement specification can be implemented.

In this article, we will show how to

  • create an Eclipse project for WSRFLite which we will use to build our project (it can be done without eclipse, naturally, but it's nicer this way),
  • how to build classes for our schema files,
  • define our service's interface (or better, interfaces, as we will develop a service according to the factory/resource pattern),
  • implement the specified interface,
  • test our services,
  • deploy them into WSRFLite and
  • access them from client software.

The complete source code of the finished project is available for download.

This document is licensed under the Creative Commons Attribution-Noncommercial 2.0 Germany license, which can be found at http://creativecommons.org/licenses/by-nc/3.0/de/deed.en.


Table of Contents

1. A short introduction to WSRF and WS-Agreement

In this section, we shortly introduce the Web Services Resource Framework and the WS-Agreement specification.

1.1. The Web Services Resource Framework

Lorem ipsum dolor sit amet, consectetuer sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.

Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.

1.2. Web Services Agreement Specification

Lorem ipsum dolor sit amet, consectetuer sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.

Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.

2. Obtaining, installing and configuring WSRFLite

2.1. Obtaining WSRFLite

WSRFLite can be obtained via the Download page at Sourceforge.net (direct download). Don't bother looking at Unicore.eu because you will finally be redirected to Sourceforge anyway.

As of the writing of this document, v1.8.6 is the current stable release; it has been released on 23. January 2008. The name of the archive file is wsrflite-1.8.6-all.zip.

2.2. Installing WSRFLite

Saying installation is perhaps a bit to exaggerated, since what you really have to do is simply unzip the downloaded archive. It does not really matter where you unzip it though. Under Windows, I recommend something like c:/dev/wsrflite-1.8.6, while under Linux you may just unzip it to your home directory so that you get ~/wsrflite-1.8.6 or, if you have the privileges, do as I do and put it under /opt/wsrflite-1.8.6.

Anyways, from now on I will refer to the installation directory just as $WSRFLite. And in case that you wondered: yes, that was the whole installation. ;)

2.3. Adapting the start script

The included start script for Linux has a bug and for Windows there is simply no script. So depending on which OS you work with, perform the necessary steps.

Linux: Adapting start.sh

At first, make $WSRFLite/bin/start.sh executable by running chmod u+x $WSRFLite/bin/start.sh.

The original start.sh contains the following code which, when run, will give you the following exception: java.lang.NoClassDefFoundError: de/fzj/unicore/wsrflite/Kernel:

#
#put all jars in lib/ on the classpath
#
JARS=lib/*.jar
CP=.
for JAR in $JARS ; do
    CP=$CP:$JAR
done


echo Reading code from $CP

cd $INST
		

The problem is that the change to the $INST directory is performed after the classpath is built, but it needs to happen before so just put cd $INST before the classpath creation like this:

cd $INST

#
#put all jars in lib/ on the classpath
#
JARS=lib/*.jar
CP=.
for JAR in $JARS ; do
    CP=$CP:$JAR
done

echo Reading code from $CP
		

Save the file and then you're ready to contiune to the next section.

Windows: Creating start.bat

No start script for Windows is supplied with WSRFLite. You can use the following code and save it to $WSRFLite/start.bat in the WSRFLite base directory.

java -Xmx128m -classpath ./lib/xmlParserAPIs-2.6.2.jar;./lib/xfire-xmlbeans-1.2.4.jar;./lib/xfire-jsr181-api-1.0-M1.jar;./lib/xfire-core-1.2.4.jar;./lib/xfire-aegis-1.2.4.jar;./lib/xercesImpl-2.6.2.jar;./lib/xbean_xpath-2.2.0.jar;./lib/xbean-2.2.0.jar;./lib/wstx-asl-3.2.0.jar;./lib/wsrftypes-1.4.jar;./lib/wsrflite-1.8.6.jar;./lib/wsrflite-1.8.6-sources.jar;./lib/wsrflite-1.8.6-javadoc.jar;./lib/wsdl4j-1.6.1.jar;./lib/stax-utils-snapshot-20040917.jar;./lib/stax-api-1.0.1.jar;./lib/servlet-api-2.5-6.1.3.jar;./lib/saxon-xpath-8.6.1.jar;./lib/saxon-dom-8.6.1.jar;./lib/saxon-8.6.1.jar;./lib/mysql-connector-java-5.1.5.jar;./lib/mail-1.4.jar;./lib/junit-3.8.1.jar;./lib/jetty-util-6.1.3.jar;./lib/jetty-sslengine-6.1.3.jar;./lib/jetty-6.1.3.jar;./lib/jdom-1.0.jar;./lib/jaxen-1.1-beta-9.jar;./lib/hsqldb-1.8.0.5.jar;./lib/ehcache-1.2.jar;./lib/commons-logging-1.0.4.jar;./lib/commons-httpclient-3.1-rc1.jar;./lib/commons-collections-2.1.jar;./lib/commons-codec-1.2.jar;./lib/activation-1.1.jar;./lib/XmlSchema-1.1.jar;. de.fzj.unicore.wsrflite.Kernel
	
[Note]

Please note that the Windows batch file is not as sophisticated as the shell script already supplied with WSRFLite. Calling the batch script from arbitrary directories will most likely not work, you have to call it from $WSRFLITE. Additionally, if you deploy jar files to $WSRFLite/lib, you have to add them manually to the classpath!

2.4. Configuring WSRFLite

The central configuration file for WSRFLite is $WSRFLITE/conf/wsrflite.xml. It contains generic parameters, for example if SSL should be used and where the containers keystore is located, and service descriptions, which detail on the deployed services. For using the example shop service, you have to change the service description; you also have to change the path to the keystore and, if you do not necessarily need SSL, you should disable it.

Disabling SSL

To disable SSL, just change the property unicore.wsrflite.ssl from true to false.

This gives us the possibility to monitor packages as they are going over the wire.

Specifying the keystore

The keystore specified in wsrflite.xml is wrong; technically, when you disable SSL you do not need to correct it, but since you will sooner or later trip over this error, there's no point in not adding 5 letters to the config file. ;)

Change the value of the property unicore.wsrflite.ssl.keystore from demo_keystore to conf/demo_keystore. As already said, if you start the server with SSL disabled, this property is never read; if you start it with SSL enabled, you get an exception:

	SCHWERWIEGEND: Could not start server.
	java.io.FileNotFoundException: /opt/wsrflite-1.8.6/demo_keystore (No such file or directory)
    	at java.io.FileInputStream.open(Native Method)
	

If the keystore is specified correctly, you get this in the logging statements:

	INFO: Security Settings:
	SSL is enabled
	Using keystore conf/demo_keystore, type JKS.
	

Okay, that's all, you have successfully installed and configured WSRFLite and can now go on to develop your service.

3. Creating an eclipse project

We want to use Eclipse when writing our implementation code, so that we are able to use its fancy features (code completion etc.). This sections shows what has to be done to set up an Eclipse project for convenient development of WSRFLite services.

3.1. Creating the new project

Open Eclipse and click File->New->Java Project, which opens the following dialog. Name your project (wsrflite-wsag, for exmaple), then click finish.

Create a new Java project with default settings.

3.2. Preparing the project structure

Basically, we need 4 folders, of which one (src) has already been created during project setup:

  • src - holds our Java class files
  • test - holds our JUnit tests
  • schema - this is where the schema files go
  • lib - necessary libraries go here

Just create these folder by right-clicking on the project root and selecting New->Folder.

3.3. Defining a user library

You probably will build more services for WSRFLite, therefore it is recommended that you create a user library containing all necessary jar files. This saves the burden of having to import all .jar files seperately. It is the easiest to just add all .jar files in the $WSRFLite/lib directory to the user library, than add the user library to the class path.

Right click on the project folder, then click on Build Path->Add Libraries.

Select User Library, then click Next.

Creating a new user library.

Click User libraries, then New

Select User libraries here.

Select New here.

Enter a name, for example wsrflite-1.8.6, then click Ok.

Name your library.

Click Add Jars and select all .jar files from the $WSRFLite/lib directory.

Select your newly created library, then click finish.

Selected libraries are marked with a check.

The library is now part of your build path and can also be reused in other projects.

3.4. Creating a build.xml file

We have several tasks, which we need to perform, before we can deploy our service, once it is ready, into WSRFLite. Although we do not even have prepared all of these steps, we pretty much know already what we have to accomplish:

  • Use XMLBeans to create, compile and jar-package classes from our schema and WSDL file(s)
  • compile our service interface and implementation (which we still need to create)
  • build a jar archive of our service files and
  • deploy the created jar archives to WSRFLite.

Basically, we only need the abovementioned four tasks, but for convenience, the following build.xml file, which will be put into the project root directory, has the additional tasks init, clean, compileTests and test. The presented build file is far from perfect, but simple enough to be easily understood and can be easily extended, if you wish to do so.

Example 1. The basic build.xml file

<project name="UNICORE6-WS-Agreement" basedir="." default="all">
	<description>Build file for a WSRFLite service</description>

	<property environment="env" />

	<property file="build.properties" />

	<!-- Classes for runnign XMLBeans -->
	<path id="beans.classes">
		<!-- distribution jars from WSRFlite which must include xbean-2.2.0.jar -->
		<fileset dir="${WSRFLITE_LOCATION}/lib/">
			<include name="**/*.jar" />
		</fileset>
	</path>

	<!-- Classes for building the service -->
	<path id="build.classes">
		<fileset dir="${WSRFLITE_LOCATION}/lib/">
			<include name="**/*.jar" />
		</fileset>
		<fileset dir="${LOCAL_LIB_DIR}">
			<include name="**/*.jar" />
		</fileset>
	</path>

	<target name="init">
		<mkdir dir="${BUILD_DIR}" />
	</target>

	<target name="clean">
		<delete dir="${BUILD_DIR}" />
		<delete>
			<fileset dir="lib/" includes="*.jar" />
		</delete>
	</target>

	<target name="xmlbeans" description="create the XMLBeans classes for the xsd and wsdl files">
		<echo>Creating XMLBeans types for the example schema</echo>
		<java classname="org.apache.xmlbeans.impl.tool.SchemaCompiler" classpathref="beans.classes" fork="true">
			<arg value="-compiler" />
			<arg value="${env.JAVA_HOME}/bin/javac" />
			<arg value="-dl" />
			<arg value="-noupa" />
			<arg value="-out" />
			<arg value="${LOCAL_LIB_DIR}/${XMLBEANS_TYPES_JAR}" />
			<!-- files to XMLBeanify follow ... -->
			<arg value="schema/ws-agreement/AgreementPortType.wsdl" />
			<arg value="schema/ws-agreement/agreement_types.xsd" />
		</java>
	</target>

	<target name="compile" depends="init, xmlbeans">
		<javac srcdir="${SRC_DIR}" destdir="${BUILD_DIR}" classpathref="build.classes" debug="on" verbose="false" />
	</target>

	<target name="compileTests" depends="jarService">
		<javac srcdir="${TEST_DIR}" destdir="${BUILD_DIR}" classpathref="build.classes" debug="on" verbose="false" excludes="de/hlrs/wsag/service/impl/benchmark/*.java" />
		<echo>Test files now end up in the source folder...</echo>
	</target>

	<target name="jarService" depends="compile">
		<jar jarfile="${LOCAL_LIB_DIR}/${JAR_NAME}" basedir="build/">
			<include name="**/*" />
		</jar>
	</target>


	<target name="deploy" depends="jarService">
		<echo>Deploying JARs to WSRFLite lib dir at ${WSRFLITE_LOCATION}/lib</echo>
		<copy todir="${WSRFLITE_LOCATION}/lib" file="${LOCAL_LIB_DIR}/${JAR_NAME}" />
		<copy todir="${WSRFLITE_LOCATION}/lib" file="${LOCAL_LIB_DIR}/${XMLBEANS_TYPES_JAR}" />
		<echo>Do not forget to adapt the wsrflite.conf file in ${WSRFLITE_LOCATION}/lib</echo>
	</target>


	<target name="test" depends="compile, compileTests">
		<junit dir="build/" fork="true" printsummary="withOutAndErr" showoutput="true">
			<formatter type="plain" usefile="false" />
			<classpath>
				<path refid="build.classes" />
			</classpath>
			<test name="de.hlrs.wsag.service.impl.AgreementFactoryTest" />
			<test name="de.hlrs.wsag.service.impl.AgreementTest" />
		</junit>
	</target>

	<target name="all" depends="clean,init,xmlbeans,compile,jarService" />

</project>
	

Some variables have been inserted into the build file, which are taken from a build.properties file; if we use the build file for another service, we only need to adapt the build.properties and can leave the build.xml file untouched. The following properties have to be defined:
  • WSRFLITE_LOCATION - points to the wsrflite installation directory, e.g. c:/dev/wsrflite-1.8.6
  • XMLBEANS_TYPES_JAR - name of the jar file that will contain the XMLBeans-generated classes, e.g. wsag_xmlbeans.jar
  • JAR_NAME= - name of the jar file that will contain the service interface and imlpementation, e.g. wsag_wsrflite.jar
  • BUILD_DIR - directory where files are compiled to, e.g. build
  • SRC_DIR - directory that contains the soruce data, e.g. src
  • TEST_DIR - directory that contains the test classes, e.g. test
  • LOCAL_LIB_DIR - directory that contains project-local libraries, e.g. lib
  • SCHEMA_PATH - directory that contains the WS-Agreement schema files, e.g. schema/ws-agreement
You can use the following list and put it into a file named build.properties, which you have to put into the project root (alongside build.xml):
WSRFLITE_LOCATION=c:/dev/wsrflite-1.8.61
XMLBEANS_TYPES_JAR=wsag_xmlbeans.jar
JAR_NAME=wsag_wsrflite.jar
BUILD_DIR=build
SRC_DIR=src
TEST_DIR=test
LOCAL_LIB_DIR=lib
SCHEMA_PATH=schema/ws-agreement

1

This is the only value you definitely need to adapt. If you follow the tutorial, the other default values can be left alone

3.5. Importing the schema files

We don't need all files provided by WS-Agreement, just the basic agreement types (like Agreements, Offers, Terms, ...), agreement state types and the agreement port type (to get the resource properties element). Import these two files, either directly into the schema folder or (as I prefer) create a sub-folder named ws-agreement (if you put the files in a different folder, you need to adapt the build.properties file). You can either exract both these files from the WS-Agreement specification document or download theme directly from my home page ( agreement types, agreement state types, agreement port type).

An important note: we made a small change to the schema files, as they need to have attributeFormDefault="unqualified" in the root element, otherwise they don't work.

4. Building classes for schema files

Before we start declaring our interface and implementing our service, let's already use XMLBeans to automatically build the classes for the schema files we previously imported.

Ensure that you have create the lib directory (or whatever LOCAL_LIB_DIR points to), then run the ant task xmlbeans. You should get something like the following output:

xmlbeans:
     [echo] Creating XMLBeans types for the example schema
     [java] Time to build schema type system: 0.943 seconds
     [java] Time to generate code: 1.014 seconds
     [java] Time to compile code: 2.609 seconds
     [java] Compiled types to: lib\wsag_xmlbeans.jar
BUILD SUCCESSFUL
Total time: 6 seconds
	

Now, add the resulting library file to your build path by right-clicking on it and selecting Build Path->Add to Build Path. If you don't see the file, select your build folder and press F5 to refresh.

5. Creating our service interface

Now that we have created the Java types for everything we need from WS-Agreement, we can start building our service interfaces. Service interfaces are, in WSRFLite, simple Java interfaces. We create two interfaces, one for the AgreementFactory, one for the Agreementitself, like this:

Example 2. The factory interface

package de.hlrs.wsag.service;

import javax.jws.WebMethod;
import javax.jws.WebService;

import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementTemplateType;
import org.w3.x2005.x08.addressing.EndpointReferenceDocument;

/**
 * Factory interface that allows to create an agreement (WS-Agreement style).
 */
@WebService(targetNamespace="http://schemas.ggf.org/graap/2007/03/ws-agreement", portName="AgreementFactory")
public interface AgreementFactory { 1
	
	
	/**
	 * Creates an agreement and returns the epr document which contains the
	 * endpoint reference pointing to the document.
	 */
	@WebMethod(action="http://schemas.ggf.org/graap/2007/03/ws-agreement/createAgreement")
	public EndpointReferenceDocument createAgreement(AgreementTemplateType offer);
}
		

1

Notice that we do not need to implement any other interface or extend a class.


As you can see, the factory only provides WS-Agreement's createAgreement operation, defined with the namespaces from WS-Agreement.

Next comes the interface to our resource. We extend from WSResource and don't add any other operations:

package de.hlrs.wsag.service;

import javax.jws.WebService;

import de.fzj.unicore.wsrflite.xmlbeans.WSResource;

@WebService(targetNamespace="http://schemas.ggf.org/graap/2007/03/ws-agreement", portName="Agreement")
public interface Agreement extends WSResource {

}
		
[Tip]

Hint: You can just copy the code from above and paste it into eclipse, which should automatically generate the package and file for you.

6. Implementing the factory

6.1. The factory class

Our factory implementation looks like this:

package de.hlrs.wsag.service.impl;

import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementTemplateType;
import org.w3.x2005.x08.addressing.AttributedURIType;
import org.w3.x2005.x08.addressing.EndpointReferenceDocument;
import org.w3.x2005.x08.addressing.EndpointReferenceType;

import de.fzj.unicore.wsrflite.Kernel;
import de.hlrs.wsag.service.AgreementFactory;

/**
 * WS-IAgreement factory class used for creating agreements.
 * 
 * @author Roland Kuebert
 */
public class AgreementFactoryImpl implements AgreementFactory {

	/**
	 * Receives an offer and decides on acceptance. <p/> If the agreement is
	 * accepted, an EndpointReferenceDocument is returned, otherwise the
	 * <code>null</code> value is returned.
	 * 
	 * @param offer the agreement offer
	 * 
	 * @return {@link EndpointReferenceDocument} an EndpointReferenceDocument
	 * if the offer is accepted, otherwise <code>null</code>.
	 */
	public EndpointReferenceDocument createAgreement(AgreementTemplateType offer) {
		return null;
	}
	
}
		

This class just inherits from the factory and implements the inherited method - at the moment just returning null. We will implement a test for this method before we implement it.

6.2. Creating the factory test

Before we start with the real impementation, let's create a JUnit test for the factory. To do this, first create another source folder called test, then create the following class in there:

package de.hlrs.wsag.service.impl;

import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementOfferDocument;
import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementType;
import org.w3.x2005.x08.addressing.EndpointReferenceDocument;

import de.fzj.unicore.wsrflite.Kernel;
import de.fzj.unicore.wsrflite.persistence.HsqldbPersist;
import de.fzj.unicore.wsrflite.persistence.IPersistenceProperties;
import de.fzj.unicore.wsrflite.xfire.JettyTestCase;
import de.fzj.unicore.wsrflite.xfire.XFireClientFactory;
import de.fzj.unicore.wsrflite.xfire.XFireKernel;
import de.hlrs.wsag.service.AgreementFactory;

/**
 * Test case for the <code>AgreementFactoryImpl</code> class.
 * 
 * @author Roland Kuebert
 */
public class AgreementFactoryTest extends JettyTestCase {

	/** If the setup has been done before. */
	static boolean haveInit = false;

	/** Default Id used for created agreements. */
	private final static String AGREEMENT_ID = "abcdefg-12345";

	/** Agreement factory proxy. */
	private AgreementFactory agreementFactory;

	/**
	 * Sets up the agreement factory proxy and performs service
	 * registration with the kernel.
	 * 
	 * @throws Exception
	 */
	@Override
	protected void setUp() throws Exception { 1
		if (haveInit == false) {
			System.setProperty(HsqldbPersist.clearDBOnStartup, "true");
			super.setUp();
			addServices();

			// Logging
			LogManager.getLogManager().reset();
			ConsoleHandler c = new ConsoleHandler();
			c.setLevel(Level.OFF);
			Logger.getLogger("de").addHandler(c);

			// Create a proxy for the agreement factory.
			agreementFactory = (AgreementFactory) (new XFireClientFactory())
					.createPlainWSProxy(AgreementFactory.class, getBaseurl()
							+ "/AgreementFactoryImpl", null);
		}
	}

	/**
	 * Adds the factory and instance services to the kernel.
	 */
	@Override
	protected void addServices() throws Exception { 2
		Kernel.getKernel().setProperty(
				IPersistenceProperties.WSRF_PERSIST_CLASSNAME,
				HsqldbPersist.class.getName());
		Kernel.getKernel().setProperty(
				IPersistenceProperties.WSRF_PERSIST_STORAGE_DIRECTORY, ".");

		XFireKernel.exposeAsService("AgreementFactoryImpl",
				AgreementFactory.class, AgreementFactoryImpl.class, false);
		XFireKernel.exposeAsService("Agreement", AgreementImpl	.class,
				AgreementHomeImpl.class, true);
	}

	
	/**
	 * Tests the factory's <code>createAgreement</code> operation.
	 * 
	 * @throws Exception
	 */
	public void testCreateAgreement() throws Exception {

		try {
			EndpointReferenceDocument document = agreementFactory
					.createAgreement(createOffer());
			assertNotNull("EndpointReferenceDocument must not be null", document); 3
			
			System.out.println("Received EPR: " + document);
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}
	}
	
	public AgreementOfferDocument createOffer() { 4
		AgreementOfferDocument offerDoc = AgreementOfferDocument.Factory.newInstance();
		AgreementType offer = offerDoc.addNewAgreementOffer();

		offer.setAgreementId(AGREEMENT_ID);
		System.out.println(offerDoc.xmlText());
		
		return offerDoc;
	}
}

1

This operation sets up our whole environment. Here, we

  • add the services we want to test to the Kernel (basically, we deploy them programatically),

  • set up the logging environment and

  • create a proxy object for the AgreementFactor, which we use to communicate with the deployed factory service.

2

This operation does the actual work of programmatically deploying the services.

3

This is our test. We call the createAgreement operation and ensure that we are getting an EndpointReference to the created agreement.

4

This operation just creates a minimal offer which we can send to the agreement factory.

What you notice immediately: we reference a class AgreementHomeImpl, but we haven't created it yet, so let's just do it:

package de.hlrs.wsag.service.impl;

import java.util.logging.Logger;

import de.fzj.unicore.wsrflite.xmlbeans.impl.WSResourceHomeImpl;
import de.fzj.unicore.wsrflite.xmlbeans.impl.WSResourceImpl;

public class AgreementHomeImpl extends WSResourceHomeImpl {

	protected static Logger logger=Logger.getLogger(AgreementHomeImpl.class.getName());
	
	@Override
	protected WSResourceImpl doCreateInstance(){
		return new AgreementImpl();
	}
	
}

				

Ouch, tricked again - where's AgreementImpl coming from? We have to create that as well (explanation will follow in good time):

package de.hlrs.wsag.service.impl;

import javax.xml.namespace.QName;

import de.fzj.unicore.wsrflite.xmlbeans.impl.WSResourceImpl;
import de.hlrs.wsag.service.Agreement;

public class AgreementImpl extends WSResourceImpl implements Agreement {

	@Override
	public QName getResourcePropertyDocumentQName() {
		// TODO Auto-generated method stub
		return null;
	}
	
}
				

If you run the the test, it should fail and you should get the following console output:

2009-01-19 16:31:38.706::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2009-01-19 16:31:38.741::INFO:  jetty-6.1.3
2009-01-19 16:31:38.888::INFO:  Started SocketConnector @ 0.0.0.0:65321
createAgreement called
				

As you can clearly see, the test failed because we returned null in the implementation, but expected something else in the test - an endpoint reference to an agreement, obviously. Once we have this correct, our test will no longer fail. Furthermore, you can see from the output createAgreement called that our implementation is really called.

6.3. Creating an agreement instance

We now replace the code in AgreementFactoryImpl to really create a service instance and return it's epr. Substitute the code with this:

	/**
	 * Receives an offer and decides on acceptance. <p/> If the agreement is
	 * accepted, an EndpointReferenceDocument is returned, otherwise the
	 * <code>null</code> value is returned.
	 * 
	 * @param offer
	 *            the agreement offer
	 * 
	 * @return {@link EndpointReferenceDocument} an EndpointReferenceDocument if
	 *         the offer is accepted, otherwise <code>null</code>.
	 */
	public EndpointReferenceDocument createAgreement(
			AgreementOfferDocument offer) {

		EndpointReferenceDocument response = EndpointReferenceDocument.Factory
				.newInstance();

		try {

			response.addNewEndpointReference();

			// create the service wsa:To address
			String addr = Kernel.getKernel().getProperty(Kernel.WSRF_BASEURL)
					+ "/Agreement?res=" + makeNewInstance(m); 1

			EndpointReferenceType eprt = EndpointReferenceType.Factory
					.newInstance();
			AttributedURIType address = eprt.addNewAddress();
			address.setStringValue(addr);
			response.setEndpointReference(eprt); 2

		} catch (Exception e) {
			e.printStackTrace();
		}

		return response;
	}

	/**
	 * create a new WSResource on the associated WSRF service
	 * 
	 * @return
	 */
	protected String makeNewInstance() throws Exception {
		return Kernel.getKernel().getServiceHome("Agreement")
				.createWSRFServiceInstance(null); 3
	}
			

1

We use the Kernel class to get the containers base URL and add the path to the instance using the makeNewInstance operation.

2

The address is added to an endpoint reference and returned to the caller.

3

The Kernel is used to obtain the service home of the Agreement service. Using the service home, we can create and instance and return it's unique ID to the caller.

If you run the test again, you will get something like this:

			
2009-01-19 17:13:12.021::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2009-01-19 17:13:12.053::INFO:  jetty-6.1.3
2009-01-19 17:13:12.089::INFO:  Started SocketConnector @ 0.0.0.0:65321
http://localhost:65321/services/Agreement?res=b0e06126-5580-4333-bf63-bd2b9598e027
Received EPR: <add:EndpointReference xmlns:add="http://www.w3.org/2005/08/addressing" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <add:Address>http://localhost:65321/services/Agreement?res=b0e06126-5580-4333-bf63-bd2b9598e027</add:Address>
</add:EndpointReference>

			

This shows that a resource has been created and its address is http://localhost:65321/services/Agreement?res=b0e06126-5580-4333-bf63-bd2b9598e027 .

Now the only thing that is missing is the implementation of our instance service, which we will be doing in the next section.

7. Implementing the service instance

7.1. Creating a unit test

As we have done before for the agreement factory, we prepare a unit test to test the implementation we will be doing. The following class is the basis for our tests and has two - yet empty - test methods in it, along with the skeleton that helps to create an agreement instance.

package de.hlrs.wsag.service.impl;

import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementIdDocument;
import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementOfferDocument;
import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementType;
import org.oasisOpen.docs.wsrf.rp2.GetResourcePropertyDocumentResponseDocument;
import org.w3.x2005.x08.addressing.EndpointReferenceDocument;
import org.w3.x2005.x08.addressing.EndpointReferenceType;

import de.fzj.unicore.wsrflite.Kernel;
import de.fzj.unicore.wsrflite.persistence.HsqldbPersist;
import de.fzj.unicore.wsrflite.persistence.IPersistenceProperties;
import de.fzj.unicore.wsrflite.utils.Utilities;
import de.fzj.unicore.wsrflite.xfire.JettyTestCase;
import de.fzj.unicore.wsrflite.xfire.XFireClientFactory;
import de.fzj.unicore.wsrflite.xfire.XFireKernel;
import de.fzj.unicore.wsrflite.xmlbeans.client.BaseWSRFClient;
import de.hlrs.wsag.service.Agreement;
import de.hlrs.wsag.service.AgreementFactory;

/**
 * Test case for the <code>AgreementTest</code> class.
 * 
 * @author Roland Kuebert
 */
public class AgreementTest extends JettyTestCase {

	/** Agreement factory proxy. */
	private AgreementFactory agreementFactory;
	/** Agreement WSRF client. */
	private BaseWSRFClient baseWsrfAgreementClient;
	
	/** Default Id used for created agreements. */
	private final static String AGREEMENT_ID = "abcdefg-12345";
	
	/**
	 * Sets up the agreement factory proxy and performs service
	 * registration with the kernel.
	 * 
	 * @throws Exception
	 */
	@Override
	protected void setUp() throws Exception {
			System.setProperty(HsqldbPersist.clearDBOnStartup, "true");
			super.setUp();
			addServices();

			// Logging
			LogManager.getLogManager().reset();
			ConsoleHandler c = new ConsoleHandler();
			c.setLevel(Level.OFF);
			Logger.getLogger("de").addHandler(c);

			// Create a proxy for the agreement factory.
			agreementFactory = (AgreementFactory) (new XFireClientFactory())
					.createPlainWSProxy(AgreementFactory.class, getBaseurl()
							+ "/AgreementFactoryImpl", null);
	}

	/**
	 * Adds the factory and instance services to the kernel.
	 */
	@Override
	protected void addServices() throws Exception {
		Kernel.getKernel().setProperty(
				IPersistenceProperties.WSRF_PERSIST_CLASSNAME,
				HsqldbPersist.class.getName());
		Kernel.getKernel().setProperty(
				IPersistenceProperties.WSRF_PERSIST_STORAGE_DIRECTORY, ".");

		XFireKernel.exposeAsService("AgreementFactoryImpl",
				AgreementFactory.class, AgreementFactoryImpl.class, false);
		XFireKernel.exposeAsService("Agreement", Agreement.class,
				AgreementHomeImpl.class, true);

	}
	
	/**
	 * Creates an agreement.
	 * 
	 * @throws Exception
	 */
	public void createAgreement() throws Exception {

		try {
			AgreementOfferDocument offerDoc = AgreementOfferDocument.Factory
					.newInstance();
			AgreementType offer = offerDoc.addNewAgreementOffer();
			offer.setAgreementId(AGREEMENT_ID);
			EndpointReferenceDocument document = agreementFactory
					.createAgreement(offerDoc);

			assertNotNull("EndpointReferenceDocument must not be null", document);
			
			EndpointReferenceType epr = document.getEndpointReference();

			String agreementUrl = getBaseurl()+"/Agreement";
			baseWsrfAgreementClient = new BaseWSRFClient(agreementUrl, epr);
			
			// Do not cache stuff at the client.
			baseWsrfAgreementClient.setUpdateInterval(-1);
			
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}

	}

}

As you can see, this test sets up a BaseWSRFClient, which is a generic WSRF client. Since there are no tests, there's nothing you can run.

7.2. Obtaining the resource property document

The BaseWSRFClient has the following operations:

// TODO Add operations list here.

The first thing we want to try is to use the BaseWSRFClient to obtain a resource property document. We add the operation testGetResourcePropertyDocument():

	/**
	 * Queries the service for the resource property document and asserts that
	 * a non-null value is returned.
	 */
	public void testGetResourcePropertyDocument() {
		try {
			createAgreement();
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}
		try {
			String rpString = baseWsrfAgreementClient.getResourcePropertyDocument();
			assertNotNull(rpString);
			System.out.println(rpString);
			GetResourcePropertyDocumentResponseDocument rpDoc
				= GetResourcePropertyDocumentResponseDocument.Factory.parse(rpString);
			assertNotNull(rpDoc);
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}
	}
					

The output of the program should be like this:


2009-01-19 18:14:32.261::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2009-01-19 18:14:32.293::INFO:  jetty-6.1.3
2009-01-19 18:14:32.327::INFO:  Started SocketConnector @ 0.0.0.0:65321
http://localhost:65321/services/Agreement?res=52a1ccbe-17c0-41ae-b50a-ba30569a4595
<rp:GetResourcePropertyDocumentResponse
 xmlns:rp="http://docs.oasis-open.org/wsrf/rp-2"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <rl:CurrentTime xmlns:rl="http://docs.oasis-open.org/wsrf/rl-2">
		2009-01-19T18:14:32.964+01:00
	</rl:CurrentTime>
  <rl:TerminationTime xmlns:rl="http://docs.oasis-open.org/wsrf/rl-2">
		2009-01-20T18:14:32.724+01:00
	</rl:TerminationTime>
</rp:GetResourcePropertyDocumentResponse>

					

This shows us that the resource already has resource properties: the current time and the termination. What we now want is to add resource properties from WS-Agreement, which we will then hopefully be able to see in the output of this test.

7.3. Adding resource properties

So how do we add the resource properties we want to have an agreement, like Id, Context and Terms? In principle, this is easy, even though it is the most cumbersome thing we have to do until now.

For each resource property we want our service to have, we need to have a corressponding class that extends de.fzj.unicore.wsrflite.xmlbeans.ResourceProperty. This class encapsulates the ...Document, created by XMLBeans, that corresponds to what we want to expose as a property.

So, for the AgreementId, this is how our property class looks:


package de.hlrs.wsag.service.properties;

import org.ggf.schemas.graap.x2007.x03.wsAgreement.AgreementIdDocument;

import de.fzj.unicore.wsrflite.WSRFInstance;
import de.fzj.unicore.wsrflite.xmlbeans.ResourceProperty;

public class AgreementIdProperty extends ResourceProperty<Object> {

	private AgreementIdDocument agreementIdDocument;
	
	public AgreementIdProperty(WSRFInstance res) {
    	// reference to the resource is needed in order to access the entry resource property
		this.parentWSResource=res;

    	AgreementIdDocument doc = AgreementIdDocument.Factory.newInstance();
    	doc.setAgreementId(null);
    	agreementIdDocument = doc;
    }
	
	@Override
	public AgreementIdDocument[] getXml() {
		return new AgreementIdDocument[] { agreementIdDocument };
	}

	/**
	 * Gets the current value of <code>agreementIdDocument</code>
	 *
	 * @return AgreementIdDocument the current value of agreementIdDocument
	 */
	public AgreementIdDocument getAgreementIdDocument() {
		return agreementIdDocument;
	}

	/**
	 * Sets the agreementIdDocument to <code>agreementIdDocument</code>.
	 *
	 * @param agreementIdDocument the agreementIdDocument to set
	 */
	public void setAgreementIdDocument(AgreementIdDocument agreementIdDocument) {
		this.agreementIdDocument = agreementIdDocument;
	}

}


				

TODO explain this...

Okay, now that we have the correct class, we still need to adapt AgreementImpl. We need to override the initialise method and our AgreementId property to the list of properties:


	public void initialise(String serviceName, Map<String, Object> initobjs)throws Exception{
		super.initialise(serviceName, initobjs);

		AgreementIdProperty idProperty = new AgreementIdProperty(this);
		AgreementIdDocument idDocument = idProperty.getAgreementIdDocument();
		idDocument.setAgreementId("Foo");
		idProperty.setAgreementIdDocument(idDocument);
		properties.put(Utilities.toQName(AgreementIdDocument.type), idProperty);
	}
				
				

Running the test now should give you something like this:


2009-01-19 18:52:21.013::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2009-01-19 18:52:21.046::INFO:  jetty-6.1.3
2009-01-19 18:52:21.086::INFO:  Started SocketConnector @ 0.0.0.0:65321
http://localhost:65321/services/Agreement?res=cbf07fbf-c548-4ed9-93d0-65266321dedd
<rp:GetResourcePropertyDocumentResponse
	xmlns:rp="http://docs.oasis-open.org/wsrf/rp-2"
	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  	<rl:CurrentTime xmlns:rl="http://docs.oasis-open.org/wsrf/rl-2">
			2009-01-19T18:52:21.776+01:00
		</rl:CurrentTime>
  	<ws:AgreementId xmlns:ws="http://schemas.ggf.org/graap/2007/03/ws-agreement">
			Foo
		</ws:AgreementId>
  	<rl:TerminationTime
			xmlns:rl="http://docs.oasis-open.org/wsrf/rl-2">
			2009-01-20T18:52:21.514+01:00</rl:TerminationTime>
		</rp:GetResourcePropertyDocumentResponse>

				

It worked, nice job - although we have the value hard coded in the resource, which is definitely something we need to change.

7.4. Adapting the test and soft-coding the resource property

Now that we have a property, we basically want to set it by software, not fixed like we did. At first, let's adapt the test to set the ID to some string, so after

AgreementTemplateType offer = AgreementTemplateType.Factory.newInstance();
				

add

offer.setAgreementId("someAgreementId");
				

to set the agreement id in the offer to something more or less sensible.

Let's see what we do with this now. In the factory, we can obviously extract it from the document passed in:

String agreementId = offer.getAgreementOffer().getAgreementId()
				

That's fair enough, but how do we pass it to the resource? Easy. Maybe you have noticed before, that we pass a null reference to createWSRFServiceInstsance:

protected String makeNewInstance() throws Exception {
	return Kernel.getKernel().getServiceHome("Agreement").createWSRFServiceInstance(null);
}
				

createWSRFServiceInstance takes an object of type Map<String, Object> as an argument and the resource obtains this object in its initialise method, inhertied from WSResourceImpl, since it has the following signature:

public void initialise(String serviceName, Map initobjs) throws Exception
				

Obviously, we only need to create a map in our factory implementation, put the value from the offer in there and remove it in the instance implementation. We put it in a HashMap like this


Map<String, Object> m = new HashMap<String, Object>();
m.put("agreement_id", offer.getAgreementOffer().getAgreementId());
				
				

and then implement a new version of makeNewInstance like this:


protected String makeNewInstance(Map<String, Object> m) throws Exception {
	return Kernel.getKernel().getServiceHome("Agreement")createWSRFServiceInstance(m);
	}

				

In the factory, we only need to change one more line, from

String addr = Kernel.getKernel().getProperty(Kernel.WSRF_BASEURL)
	+ "/Agreement?res=" + makeNewInstance();
				

to

String addr = Kernel.getKernel().getProperty(Kernel.WSRF_BASEURL)
	+ "/Agreement?res=" + makeNewInstance(m);
				

Adapt the initalise method in AgreementImpl to look like this:


public void initialise(String serviceName, Map<String, Object> initobjs)throws Exception{
	super.initialise(serviceName, initobjs);

	AgreementIdProperty idProperty = new AgreementIdProperty(this);
	AgreementIdDocument idDocument = idProperty.getAgreementIdDocument();
		
	if ( initobjs.containsKey("agreement_id") ) {
		String id = (String) initobjs.get("agreement_id");
		idDocument.setAgreementId(id);
	} else {
		idDocument.setAgreementId(null);
	}
	idProperty.setAgreementIdDocument(idDocument);
	properties.put(Utilities.toQName(AgreementIdDocument.type), idProperty);
}

				

7.5.  Testing the agreement id property

We want to have a test for our agreement id resource property, so let's add the following to AgreementTest:

/**
 * Queries the resource property AgreementId and compares it with the value
 * from the agreement offer. Fails if the values are not equal.
*/
public void testGetAgreementIdResourceProperty() {
	try {
		createAgreement();
	} catch (Exception e) {
		e.printStackTrace();
		fail();
	}

	try {
		String rpString
			= baseWsrfAgreementClient.getResourceProperty(Utilities.toQName(AgreementIdDocument.type));
		assertNotNull(rpString);
	
		AgreementIdDocument agreementIdDoc = AgreementIdDocument.Factory.parse(rpString);
		String id = agreementIdDoc.getAgreementId();
		assertEquals("Agreement Id should be '" + AGREEMENT_ID + "', is '"
			+ id + "'", AGREEMENT_ID, id);
	} catch (Exception e) {
		e.printStackTrace();
		fail();
	}
}
				

Running this test should succeed, as the property gets correctly set and will, therefore, be equal to the value that was given the offer.

7.6. Adding the missing resource properties

Now, the missing resource properties can be added in the same way. WS-Agreement foresees

  • AgreementId - we already got that
  • Name
  • Context
  • Terms

These properties can be added in the same way as described above for AgreementId, so we won't go into detail on these. You can download the whole Eclipse project with the completed implementation here if you want to have a look at the finished implementation.

8. Deploying the service and accessing it with a client

8.1. Adding service to the wsrflite.xml

The configuration file that holds the service information is called wsrflite.xml and can be found in the directory $WSRFLite/conf. Each service - factory and instance - needs an entry like this:

<service name="Name you wish your service to have" wsrf="false or true">
	<interface class="name of interface class (with package)" />
	<implementation class="name of implementation class (with package)"/>
</service>
			

For our services, we add:

<service name="AgreementFactory" wsrf="false">
	<interface class="de.hlrs.wsag.service.AgreementFactory" />
	<implementation class="de.hlrs.wsag.service.impl.AgreementFactoryImpl"/>
</service>

<service name="Agreement" wsrf="true">
	<interface class="de.hlrs.wsag.service.Agreement" />  
	<implementation class="de.hlrs.wsag.service.impl.AgreementHomeImpl"/>
</service>
			

8.2. Deploying the service classes

Apart from specifying the service's classes, we obviously need to make them accessible to WSRFLite. Therefore, all necessary libraries we use in our service - for our example, only wsag_wsrflite.jar and wsag_xmlbeans.jar - have to be copied to the $WSRFLite/lib directory.

You can either use the task deploy from the build.xml files or copy the files manually to the $WSRFLite/lib directory.

[Note]

If you are running under Windows, you need to specify the library files in the class path; the Linux start.sh script, however, does include them automatically.

8.3. Starting the container and checking for deployment

You can now start the container via your start script and have a look at the deployed services. Normally, the container binds to localhost, port 7777. If you connect to http://localhost:7777/services/, you should see the Agreement and AgreementFactory services, which will look something like this:

Available Services:

    * Agreement [wsdl]
    * AgreementFactory [wsdl]	

      Generated by XFire ( http://xfire.codehaus.org ) 
			

You can retrieve the WSDL description of your service by clicking on the corresponding [wsdl] link. With the WSDL file, you can, for example, generate stubs for Axis and access the service this way.

9. Summary

In this article, we have demonstrated on how to develop a full-fledged WS-RF service according to the WS-RF Factory/Instance pattern by showing how one can implement the WS-Agreement specification in WSRFLite. We have used Eclipse to develop, test and deploy our service, which makes the whole process very convenient.

Based on what has been demonstrated in this article, you should be able to develop your own services for WSRFLite, regardless of them being stateless or stateful web service and make use of the files we have presented here, like, for example, build.xml.

10. Acknowledgements

This work has been supported by the IRMOS project and has been partly funded by the EC Seventh Framework Programme FP7/2007-2011 under grant agreement n° 214777. This work expresses the opinions of the authors and not necessarily those of the European Commission. The European Commission is not liable for any use that may be made of the information contained in this work.

This document has been inspired by Borja Sotomayor's The Globus Toolkit 4 Programmer's Tutorial and has reused parts of its docbook sources and stylesheets.