The UncertWeb Processing Service is a generic framework for exposing processes on the web. Processes are exposed using two interfaces: SOAP/WSDL and JSON.
If you like to do things the easy way, clone the example project at https://github.com/itszootime/ps-example. This is a pre-configured Maven project complete with sample processes, all ready to package and deploy.
Alternatively, creating your own Maven webapp project is the next easiest way to use the framework. The UncertWeb Maven repository, hosted at the University of Münster, is required to resolve the necessary dependencies. Adding the following snippet to your pom.xml
file will include the repository in your project.
<repositories>
<!-- Other repositories may be here too -->
<repository>
<id>UncertWebMavenRepository</id>
<name>UncertWeb Maven Repository</name>
<url>http://giv-uw.uni-muenster.de/m2/repo</url>
</repository>
</repositories>
The framework dependency can then be added.
<dependencies>
<!-- Other dependencies may be here too -->
<dependency>
<groupId>org.uncertweb</groupId>
<artifactId>ps-framework</artifactId>
<version>0.2.6-SNAPSHOT</version>
</dependency>
</dependencies>
The last thing you'll need to do is add the framework servlet classes and mappings to the webapp configuration file, src/main/webapp/WEB-INF/web.xml
.
<web-app>
<!-- Other servlets/mappings/etc may be here too -->
<servlet>
<servlet-name>Service</servlet-name>
<servlet-class>org.uncertweb.ps.ServiceServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>Data</servlet>
<servlet-class>org.uncertweb.ps.DataServlet
</servlet>
<servlet-mapping>
<servlet-name>Service</servlet-name>
<url-pattern>/service/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Data</servlet-name>
<url-pattern>/data</url-pattern>
</servlet-mapping>
</web-app>
Enabling CORS allows processes to be accessed by a web page from a different domain. Without this, JavaScript developers will require a server-side proxy to access your processes. If you wish to enable CORS, add the following Maven dependency to your pom.xml
file.
<dependency>
<groupId>org.jcors</groupId>
<artifactId>jcors </artifactId>
<version>1.1</version>
</dependency>
In addition to the filter and mapping in your webapp configuration file.
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.jcors.web.CorsEnablingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/service/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/data</url-pattern>
</filter-mapping>
A process is created by extending the AbstractProcess
class. This class defines methods for returning input and output descriptions, metadata, and performing the actual process work.
Once your class has extended AbstractProcess
, implement the getInputIdentifiers
and getOutputIdentifiers
methods. These methods should return a list of unique identifiers for the inputs and outputs of your process. The chosen identifiers will be used throughout the process class.
@Override
public List<String> getInputIdentifiers() {
return Arrays.asList(new String[] { "FirstInput", "SecondInput" });
}
The types of these inputs and outputs are defined in the getInputDataDescription
and getOutputDataDescription
methods. These methods should each return a DataDescription
object for a given input or output identifier.
At a minimum, the DataDescription
object is constructed with one parameter: the class the input or output will be an instance of. This is used by the framework to determine which encoding class to use when parsing and generating data. The DataDescription
object can also have a minimum and maximum number of occurrences, and a raw flag to tell the framework to bypass encoding classes when handling referenced data. This is most useful when processing large data (such as raster coverages), when you may not want to read the whole file into memory.
@Override
public DataDescription getInputDataDescription(String identifier) {
if (identifier.equals("FirstInput")) {
// A is a double, minimum and maximum occurrences is 1 (default)
return new DataDescription(Double.class);
} else if (identifier.equals("SecondInput")) {
// B is a double, minimum occurences is 1 and maximum is unbounded
return new DataDescription(Double.class, 1, Integer.MAX_VALUE);
}
}
Processing work is performed in the run method. When a process request is received, this method is called and passed a ProcessInputs
instance containing the parsed inputs. Individual inputs can be retrieved using their identifier, after which they can be cast to the relevant class.
// Get first input (single as maximum occurrences is one)
SingleInput firstInput = inputs.get("FirstInput").getAsSingleInput();
Double first = firstInput.getObjectAs(Double.class);
// Get second input (multiple as maximum occurrences is greater than one)
MultipleInput secondInput = inputs.get("SecondInput").getAsMultipleInput();
List<Double> seconds = secondInput.getObjectsAs(Double.class);
The method returns an instance of ProcessOutputs
, which contains the output objects.
ProcessOutputs outputs = new ProcessOutputs();
outputs.add(new SingleOutput("Result", 0.5)); // fixed result for example
return outputs;
Once complete, add the fully qualified name of your process class to the framework configuration file, src/main/resources/config.json
.
{ "encodingClasses": [],
"gsonTypeAdapterClasses": [],
"processClasses": [ "com.example.YourProcessClass" ],
"additionalProperties": [] }
Your project is now ready to be built and packaged into a web application archive (WAR) file.
$ mvn clean package
The resulting WAR file can be deployed using any Java Servlet 2.5+ compatible web container, such as Apache Tomcat.
The framework automatically generates a WSDL document and associated schema. These can are accessible through /service?wsdl
and /service?schema
respectively. The WSDL document can be used with client code generation tools such as Apache Axis (guide) and Microsoft Visual Studio (guide), or workflow software such as Taverna.
If you wish to construct requests yourself, the child element of the SOAP body should take the following form:
<ps:ProcessIdentifierRequest xmlns:ps="http://www.uncertweb.org/ProcessingService">
<ps:InputIdentifierA>
<!-- inline data here -->
</ps:InputIdentifierA>
<ps:InputIdentifierB>
<ps:DataReference href="http://some.url/somedata.xml" mimeType="text/xml" />
</ps:InputIdentifierB>
</ps:ProcessIdentifierRequest>
The child element of the SOAP body in the response will take the following form:
<ps:ProcessIdentifierResponse xmlns:ps="http://www.uncertweb.org/ProcessingSerivce">
<ps:OuputIdentifierA>
<!-- inline or data reference here -->
</ps:OutputIdentifierA>
</ps:ProcessIdentifierResponse>
If any errors are encountered during request processing, the child element of the SOAP body in the response will be a SOAP fault.
All SOAP requests should be sent using HTTP POST to /service/soap
.
The framework automatically generates a basic service description which accessible through /service?jsondesc
. This description can help to build generic execution clients.
Request objects should take the following form ('InputIdentifierA' data could be a value, array, object):
{ "ProcessIdentifierRequest": {
"InputIdentifierA": 0.523,
"InputIdentifierB": {
"DataReference": { "href": "http://some.url/somedata.xml", "mimeType": "text/xml" }
}
} }
Response objects will take the following form:
{ "ProcessIdentifierResponse": {
"OutputIdentifierA": 12.094
} }
If any errors are encountered during request processing, an exception object is returned.
{ "ServiceException": {
"message": "something bad happened",
"detail": "here's more detail on why it happened"
} }
All JSON requests should be sent with HTTP POST to /service/json
.
To allow the user to focus on the functionality of the process, encoding is automatically selected depending on the class of the input or output. This automatic selection can be controlled by implementing custom encoding classes. The framework has built-in support for GeoJSON, UncertML and the UncertWeb profiles of GML and O&M.
The following geometry classes in the JTS Topology Suite will be encoded as GML (UncertWeb profile) or GeoJSON:
- Point
- LineString
- Polygon
- MultiPoint
- MultiPolygon
- MultiLineString
Plus from the UncertWeb GML profile API:
- RectifiedGrid
All classes in the UncertWeb O&M profile API are supported for both XML and JSON encoding.
All classes in the UncertML API version 2.0 are supported for both XML and JSON encoding.
The following primitive wrapper classes are supported for both XML and JSON encoding:
- String
- Double
It is possible to use primitive wrapper classes in arrays when creating a DataDescription
for an input or output:
new DataDescription(Double[].class);
This has slightly different semantics to data with maximum occurrences set to a value greater than 1. For example, in XML, the list element will be used rather than multiple named elements.
<!-- new DataDescription(Double[].class); -->
<ps:SomeProcessRequest xmlns:ps="http://www.uncertweb.org/ProcessingService">
<ps:SomeInput>1 2 3</ps:SomeInput>
</ps:SomeProcessRequest>
<!-- new DataDescription(Double.class, 1, Integer.MAX_VALUE) -->
<ps:SomeProcessRequest xmlns:ps="http://www.uncertweb.org/ProcessingService">
<ps:SomeInput>1</ps:SomeInput>
<ps:SomeInput>2</ps:SomeInput>
<ps:SomeInput>3</ps:SomeInput>
</ps:SomeProcessRequest>
A custom XML encoding class can be created by extending AbstractXMLEncoding
. This class defines a number of methods that enable the framework to select the correct encoding class, handle data, and generate schema for process requests and responses. The first of these are:
getNamespace
should return the namespace for the generated elements.getSchemaLocation
should return the location of an XML schema document for the encoding.isSupportedClass
should return whether the givenClass
is supported.
As the name of a Java class may not necessarily be the same as the encoded element, the getIncludeForClass
should describe how a given Class
is mapped to the schema for this encoding. An example for how the JTS Point class maps to a GML Point element:
public Include getIncludeForClass(Class<?> clazz) {
if (clazz.equals(Point.class)) {
// this will generate an element with ref="gml:Point"
// given that xmlns:gml="http://www.opengis.net/gml/3.2"
return new IncludeRef("Point");
}
}
Finally, the methods for actually dealing with data encoding:
parse
should return an instance of the givenClass
parsed from the JDOMElement
encode
method should return anElement
representing the encodedObject
.
If you don't want to use JDOM, you can implement the alternative parse
and encode
methods which handle the streams directly.
Once created, add the fully qualified name of your encoding class to the configuration file (remainder of config ommitted).
{ "encodingClasses": [
"com.example.YourEncodingClass"
] }
The framework uses the Gson library to handle JSON. In some cases, Gson can automatically serialize and deserialize Java objects. When Gson fails to do this automatically (e.g. when a class doesn't have a no-argument constructor), or where more control is required, it is possible to override the default Gson behaviour.
If you wish override the default behaviour, but take advantage of the benefits provided by Gson, you can implement the JsonSerializer
, JsonDeserializer
, and InstanceCreator
interfaces as necessary. Refer to the Gson user guide for details on how to use each of these interfaces. Once created, add the fully qualified name of your implementing classes to the configuration file, where they will be registered when the service starts (remainder of config ommitted).
{ "gsonTypeAdapterClasses": [
{ "com.example.YourClass": [
"com.example.YourGsonSerializer",
"com.example.YourGsonDeserializer",
"com.example.YourGsonInstanceCreator"
] }
] }
The AbstractBinaryEncoding
class can be extended to support binary data within your process. Binary encoding classes will always return a data referenced in a response, never inline. This class has four abstract methods to implement:
encode
should encode the givenObject
to theOutputStream
.parse
should return an instance of the givenClass
parsed from theInputStream
.isSupportedClass
should return whether the givenClass
is supported.isSupportedMimeType
should return whether the given MIME type is supported.
Once created, add the encoding class to the configuration file.
To help describe your processes, metadata can be added. The AbstractProcess
class defines three methods to support this: getMetadata
, getInputMetadata
, and getOutputMetadata
. The former method should return metadata for the process itself, and the latter two should return metadata for each of the inputs and outputs.
Metadata is returned as a list of Metadata
objects. The Metadata
object is essentially a key-value pair. The key can be any string, but here are some examples you may wish to use:
- description
- variable-units-of-measure
- spatial-crss
- spatial-resolutions
An example getInputMetadata
implementation may look as follows:
@Override
public List<Metadata> getInputMetadata(String identifier) {
List<Metadata> metadata = new ArrayList<Metadata>();
if (identifier.equals("FirstInput")) {
metadata.add(new Metadata("description", "A length of something"));
metadata.add(new Metadata("variable-units-of-measure", "m"));
}
return metadata;
}