This is a guide to developing applications to host in an InstantSOAP services. It will describe a range of ways that you can expose your buisness logic as an InstantSOAP application. By the end of this tutorial, you should be able to write services exposing simple string transformations and complex map to map transformations.
This guide assumes that you have run through the Getting Started guide and have deployed the echo-test war at the default location on the current machine. We also assume that you will be developing your client using CXF as your web-services toolkit.
The InstantSOAP web service can be configured in a multitude of ways.
In the project instantsoap-ws-cxf
, the details are
specified by a Spring
context. In this guide we will assume the default deployment is being
used.
The default configuration mounts an instance of
WebServiceDispatcherImpl
at the endpoint
/instantsoap/applications
relative to the WAR mountpoint.
This object is configured with a stack of handlers and delegates that
implement asynchronous handling of requests, queing and forwarding
requests to strategies that can actually process them.
When a request has been pushed all the way down the stack, it will
eventually hit an object implementing StrategyDispatcher
.
These are responsible for binding the name of the application to the
behavior. In the default deployment, there are three dispatchers
mounted. The first one exposes an interface that directly transforms the
inputs map into the responses map. The second one requries that there
is a single input and output, and provides an interface that transforms
this input string into the output string. The third one takes the inputs
and combines them with a description of a command-line. It then executes
that command-line with those inputs and returns the response.
To avoid you needing to edit this spring configuration, the default stack is also configured for automatic discoverfy of service providers. For the string and map processors, this can be achieved by either using the standard Java service provider interface mechanism, or by a spring-based method. For command-lines, this is done by providing the command-line template information and listing the location of this file in a well-known resource.
After the service spring context has been made but before the service
is invoked, all of the jars forming the classpath of the webapp will be
searched for buisness-logic providers. So, as you work through the
examples here, you can test them out by simply making sure they are in
the WEB-INF/lib
directrory of a copy of
instantsoap-ws-cxf.war
or
instantsoap-ws-echoTest.war
.
In this section, we will implement some buisness logic as simple string transformations.
Firstly, we need to set up the pom.xml
dependencies. To
implement the string provider API, you need to make sure that
instantsoap-string-processor
is available.
<dependencies> <dependency> <groupId>uk.ac.ncl.cs.instantsoap</groupId> <artifactId>instantsoap-string-processor</artifactId> <version>1.0</version> </dependency> </dependencies>
Next, we will write the boilerplate needed to wrap our application.
package example; import org.bjv2.util.serviceprovider.Spi; import uk.ac.ncl.cs.instantsoap.stringprocessor.StringProcessor; import uk.ac.ncl.cs.instantsoap.wsapi.InvalidJobSpecificationException; import uk.ac.ncl.cs.instantsoap.wsapi.JobExecutionException; import uk.ac.ncl.cs.instantsoap.wsapi.MetaData; import static uk.ac.ncl.cs.instantsoap.wsapi.Wsapi.metaData; @Spi public class BobAppender implements StringProcessor {
... your code goes here
}
The class is annotated with @Spi
. This will ensure that
the correct META-INF/services entries are written so that this
StringProcessor
can be discovered at run-time.
The StringProcessor
interface declares several methods.
Name | Description |
---|---|
describeApplication | The MetaData describing this service. |
getInput | The MetaData describing the single input parameter. |
getOutput | The MetaData describing the single output parameter. |
validate | Check that the input is well-formed. |
process | Return an output, given the input. This is the real implementation of the service. |
Our BobAppender
processor is going to append the string
BOB
on to the end of each input value. So that the user
can introspect this, we say as much in our implementation of
describeApplication
.
public MetaData describeApplication() { return metaData("bobAppender", "Append the string 'BOB' to the end of the input"); }
We will name the input input
and the output
output
.
public MetaData getInput() { return metaData("input", "The input string to process"); } public MetaData getOutputs() { return metaData("output", "The input string with 'BOB' appended"); }
In this example, all input strings are eligable for processing, so
validate
doesn't have any work to do.
public void validate(String data) throws InvalidJobSpecificationException { // nothing to do }
Finally, we can implement process
to do the actual work.
public String process(String data) throws JobExecutionException { return data + "BOB"; }
If you compile this code into a jar and drop this into a modified
instantsoap-ws-echoTest.war
then you should now see in
addition to the other available services, one called bobAppender.
While some logic is fairly rigid, some can be configured in a wide range of ways. To illustrate this, we will implement annother appender processor but this time make the appended string configurable. This will use Spring to configure the deployment of the service at run-time. In this example, we will only show code that illustrates differences with the previous example, rather than going through everything line-by-line.
Because we are going to be configuring this bean through spring, we do
not need to annotate it with @Spi
.
public class StringAppender implements StringProcessor {
Each of the configurable aspects of the service must be exposed as bean properties. In this case, it is only the suffix to be appended that is configurable.
private String suffix = ""; public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; }
We can use the value of the suffix to return customised meta data. For example, we can return an suffix-specific application description.
public MetaData describeApplication() { return metaData("append" + suffix, "Append the string '" + suffix + "' to the end of the input"); }
Also, the process
method must now return the input value
with the suffix appended, rather than 'BOB'.
public String process(String data) throws JobExecutionException { return data + suffix; }
At this point, we have a beany string appender. The next step is to
configure this and make it available to InstantSOAP. This is done by
writing a spring configuration file and placing it at the right place
in the JAR file. InstantSOAP follows the pattern that for a given
class that will build instances from Spring files, there will be
spring files under
META-INF/spring/${fully-qualified-class-name}
. In this
case, the class is
uk.ac.ncl.cs.instantsoap.strategydp.impl.SpringStringProcessorConfigurator
.
This file will be loaded by
SpringStringProcessorConfigurator
, and all of the beans
that implement StringProcessor will be mounted as applications in the
web application.
We can create any number of beans in this file. We can also create any number of beans that instantiate StringAppender, with different suffixes.
<bean name="appendSaysHi" class="example.StringAppender"> <property name="suffix" value=" Says Hi"/> </bean> <bean name="appendLover" class="example.StringAppender"> <property name="suffix" value="-lover"/> </bean>
If you compile this code into a jar and drop this into a modified
instantsoap-ws-echoTest.war
then you should now see in
addition to the other available services, one called "append Says Hi"
and one called "append-lover".
You can obtain the compete source for this, and the previous example, including the pom, in tar.gz or zip formats.
In the general case, your buisness logic will require multiple inputs
and will calculate multiple outputs. InstantSOAP provides a convenient
way to deploy this kind of calculation via the
MapProcessor
interface.
Here we will go through this API, and implement processors both using the service provider interface and Spring. The examples given here will be limited to the code that is instructive.
The methods provided by MapProcessor
are similar to those
in StringProcessor
. The major diferences are that firstly,
the processor can handle multiple applications by name, or if you
prefer, execute different buisness-logic when presented with different
names. Because of this, most methods take the name of the application as
their first parameter. The other difference is that the inputs and
outputs are maps of strings, so the methods describing, validating and
processing these are defined in terms of maps.
Name | Description |
---|---|
handlesApplication | True if this processor can handle applications by the given name, false otherwise. |
listApplications | Return a list of the names of all applications handled by this
processor. The handlesApplication method returns true for
exactly the names in this list. |
describeApplication | Given the name of an application, return the MetaData
describing what it does. |
getInputs | Given the name of an application, return a Set of
MetaData beans describing the inputs. |
getInputs | Given the name of an application, return a Set of
MetaData beans describing the outputs.
|
validate | Given the name of an application and a Map of inputs,
check that the inputs match what
is required. This will minimally check that all inputs listed by
getInputs are present, and may also validate that the
values fall within legal ranges. |
process | Given the name of an application and a Map of inputs,
return a Map of outputs. This will minimally contain all
of the outputs described by getOutputs . |
Our first implementation of this bean will take two strings representing whole numbers, and return the result of dividing the first by the seccond and any remainder. Firstly, we must ensure that the rigth dependencies are being pulled in by maven.
<dependencies> <dependency> <groupId>uk.ac.ncl.cs.instantsoap</groupId> <artifactId>instantsoap-string-processor</artifactId> <version>1.0</version> </dependency> </dependencies>
Next, because this class will be discoverd automatically, we must
annotate it with @Spi
.
@Spi public class DivMod implements MapProcessor
The meta-data methods for this are a little different from those in the
strings examples. They throw UnknownApplicationException
if
invoked with an appliation that they don't recognise.
public boolean handlesApplication(String application) { return "divMod".equals(application); } public List<String> listApplications() { return Collections.singletonList("divMod"); } public MetaData describeApplication(String application) throws UnknownApplicationException { if("divMod".equals(application)) { return metaData("divMod", "Calculate the division and modulus of two numbers."); } else { throw new UnknownApplicationException( "We can't handle the application: " + application); } } public Set<MetaData> getInputs(String application) throws UnknownApplicationException { if ("divMod".equals(application)) { return asSet( metaData("numerator", "Number to be divided"), metaData("denominator", "Number to divide by")); } else { throw new UnknownApplicationException( "We can't handle the application: " + application); } } public Set<MetaData> getOutputs(String application) throws UnknownApplicationException { if ("divMod".equals(application)) { return asSet( metaData("quotient", "The whole-number result of the division"), metaData("remainder", "The whole-number remainder of the division")); } else { throw new UnknownApplicationException( "We can't handle the application: " + application); } }
During validation, the procerssor must check that all needed inputs are available, and that they are parseable as integers. It is much cheeper for InstantSOAP to catch errors at validation time than at execution time, so it is good practice to do what validation you can here.
public void validate(String application, Map<String, String> inputs) throws InvalidJobSpecificationException, UnknownApplicationException { if ("divMod".equals(application)) { for(MetaData i : getInputs(application)) { if(!inputs.containsKey(i.getName())) { throw new InvalidJobSpecificationException( "Missing input: " + i.getName()); } try { Integer.parseInt(inputs.get(i.getName())); } catch (NumberFormatException e) { throw new InvalidJobSpecificationException( "Input " + i.getName() + " could not be parsed as an integer", e); } } } else { throw new UnknownApplicationException( "We can't handle the application: " + application); } }
Finally, we reach the code that will do the work. It calculates the division and remainder and returns these.
public Map<String, String> process(String application, Map<String, String> inputs) throws UnknownApplicationException, JobExecutionException { if ("divMod".equals(application)) { int numerator = Integer.parseInt(inputs.get("numerator")); int divisor = Integer.parseInt(inputs.get("divisor")); int quotient = numerator / divisor; int remainder = numerator % divisor; return asMap(pair("quotient", Integer.toString(quotient)), pair("remainder", Integer.toString(remainder))); } else { throw new UnknownApplicationException( "We can't handle the application: " + application); } }
You can expose this functionality as you did for the string examples,
by making a modified instantsoap-ws-echoTest
WAR.
You can obtain the compete source for this example, including the pom, in tar.gz or zip formats.
Just as the String transformations can be configured using Spring, so
can Map transformations. Exactly the same process is followed.
The spring file is named
uk.ac.ncl.cs.instantsoap.strategydp.impl.SpringMapProcessorConfigurator
in this case. Try to develop a map processor that uses an
operation
field to store an enumeration of
PLUS
, MINUS
, MULTIPLY
,
DIVIDE
, takes two arguments that it parses as doubles, and
returns the result of applying this operator to them in an output named
after the operator. Using Spring, deploy all four cases.