Adding State
Contents
- 1 Adding State
- 1.1 WS-Resources and The Implied Resource Pattern
- 1.2 Extending the Profile
- 1.3 More Port-Type Interfaces
- 1.4 Stateful Resources
- 1.5 Home Sweet Home
- 1.6 The Factory Port-Type
- 1.7 The Stateful Port-Type
- 1.8 A Quick Test
- 1.9 Delving into the implementation
- 1.10 JNDI configuration
- 1.11 Building & Deploying
- 1.12 A Test Client
- 1.13 Final structure of the service location
Adding State
The service we defined in the first part of this tutorial was stateless: its responses to client requests depended solely on the requests. This is all well, but in practice services may need to maintain some form of state that pre-exists, persists, and - most importantly - changes as a result of client invocations. Many gCube services are indeed stateful.
In this second part of the tutorial we will learn how to add state to our SampleService
. In the spirit of a dumb service, the idea is to keep track of the number of visits of any client that has previously 'logged on' with the service.
The identity of logged clients and the number of their subsequent visits will then form the state of our augmented SampleService
. In particular, we will add two new port-types:
- a
Factory
port-type that allows clients to log on and thus creates state within the service . In particular, we plan a single operationlogon()
for this port-type. - a
Stateful
port-type that allows clients to visit the service and thus consults and updates the state of the service. In particular, we plan a single operationaboutSF()
for this port-type which will behave exactly like the operationabout()
in theStateless
port-type whilst keep track of the client visits.
(It will be clear soon why we prefer two port-types with a single operation each rather than a single port-type with two operations.)
For both port-types, we need to repeat the steps already shown for the Stateless
port-type: add the port-types descriptions to the service profile, define the WSDL interface of each port-type, and provide the Java implementation of the two port-types. Additional steps will then be required for state management.
WS-Resources and The Implied Resource Pattern
The overall state of our SampleService
will be comprised of many 'pieces', one per client that logs on and then visits. We refer to these pieces somewhat more technically as stateful resources.
How should we go about creating and accessing stateful resources? One approach could be as follows:
- take some credentials from clients when they invoke the
logon()
operation on theFactory
port-type. A simple name would surely do for our purposes. - create stateful resources as Java objects that contain the count of client visits, and identify such resources with the name of the associated client.
- when a client comes back to visit the service and invokes the
aboutSF()
operation on theStateful
port-type, ask it to provide his name so that we can identify the corresponding stateful resource and update the count of its visits.
In this approach, clients need to explicitly identify the state they wish to target. Unfortunately, identifiers are service-specific: here we need a name, elsewhere we may need something else. The use that we make of identifiers is also specific: here we hinted at one parameter in the aboutSF()
operation, elsewhere could be two or three parameters in one or more operations. This variability makes it impossible to build generic clients that can transparently access state across different services.
You may find this observation rather strange: what could a client do that does not require specific knowledge of the target service? Well, it turns out that if we find a general way to access stateful resources, then we can build enough conventions on how we describe them to enable a range of very useful and yet generic clients. For example, we can define clients that can query and change the stateful resources of any service that complies with the conventions. We can define clients that can uniformly destroy stateful resources, either immediately or based or some renewable expiry time. We can even define clients that allow others to subscribe for changes to the stateful resources. These are all key features in gCube, and we shall be directly concerned with some of them in this very Primer.
What we need to promote generic clients is then:
- a uniform pattern to identify and access stateful resources which does not change from service to service.
- a standard that codifies this pattern and builds useful conventions on top of it.
The Web Services Resource Framework (WSRF) is precisely one such standard, and gCube adopts it. When it comes to identifying and accessing stateful resources, WSRF says: forget passing identifiers explicitly in operations such as aboutSF()
, which vary from port-type to port-type and from service to service; let us pass them instead implicitly, as part of the address of the port-type that exposes those operations. An invocation of aboutSF()
would thus be addressed to the Stateful
port-type, but the address would include also the identifier of the stateful resource that is the target of the request. The port-type implementation would then extract such identifier and use it to locally access the stateful resource. No need to explicitly parameterise aboutSF()
with it. This is the access pattern that WSRF calls the implied resource pattern.
If you think about it, this annotated address - or more appropriately, this qualified endpoint reference - identifies a pair (port-type,stateful resource). This pairs WSRF calls a WS-Resource. We can then think of the qualified endpoint reference as the endpoint reference of the WS-resource itself. Similarly, we can think of the operations available at that endpoint as the operations of the WS-Resource. In this sense, the port-type becomes the uniform interface of potentially many WS-Resources.
With the implied resource pattern and the corresponding terminology, we can now describe our new port-types as follows:
- a
Factory
port-type that allows users to create WS-Resources. In particular, we plan a single operation for this port-type,logon()
, which takes the name of the client and returns the endpoint reference of a WS-Resource 'dedicated' to the client. - a
Stateful
port-type that defines the interface of the WS-Resources. We plan a single operation for this port-type too,aboutSF()
, which takes nothing and returns nothing but it is invoked with the endpoint reference of WS-Resources.
A client may then invoke logon()
on the Factory
port-type and then use the resulting WS-Resource endpoint reference to invoke aboutSF()
on it. As we shall see, a client may also discover and use the endpoint of a WS-Resource of interest without having previously created. A couple of things to notice:
The client does not need to know about the stateful resource identifier embedded in the endpoint reference returned by the Factory
, or otherwise 'found'. The endpoint reference identifies a WS-Resource but the inner structure of this WS-Resource as a pair (port-type,stateful resource) remains opaque to the client.
Placing logon()
and aboutSF()
in different port-types makes sense. The first creates WS-Resources while the second defines their operations. Informally, we say that the Factory
is, like Stateless
, a stateless port-type for it does not serve as the interface of WS-Resources. For the opposite reason, we say that Stateful
is a stateful port-type.
Extending the Profile
We now enrich the service profile to reflect the existence of two new port-types.
<Resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ID></ID> <Type>Service</Type> <Profile> <Description>A very simple gCube Service</Description> <Class>Samples</Class> <Name>SampleService</Name> <Packages> <Main> <Description>Describes port-types</Description> <Name>Main</Name> <Dependencies> <Dependency> <Service> <Class>Samples</Class> <Name>SampleService</Name> </Service> <Package>Stubs</Package> <Version>1.0</Version> <Scope level="GHN"/> <Optional>false</Optional> </Dependency> </Dependencies> <GARArchive>org.acme.sample.gar</GARArchive> <PortType> <Name>acme/sample/stateless</Name> <WSDL/> </PortType> <PortType> <Name>acme/sample/stateful</Name> <WSDL/> </PortType> <PortType> <Name>acme/sample/factory</Name> <WSDL/> </PortType> </Main> <Software> <Description>Describes port-type stubs</Description> <Name>Stubs</Name> <Files><File>org.acme.sample.stubs.jar</File></Files> </Software> </Packages> </Profile> </Resource>
Not much to comment about here, we just added two new PortType
elements in the description of the Main
package.
More Port-Type Interfaces
The WSDL that describes the interface of the Factory
port-type holds few surprises at this stage:
<definitions name="Factory" targetNamespace="http://acme.org/sample" xmlns:tns="http://acme.org/sample" xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:corefaults="http://gcube-system.org/namespaces/common/core/faults" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" > <import namespace="http://gcube-system.org/namespaces/common/core/faults" location="../gcube/common/core/faults/GCUBEFaults.wsdl"/> <types> <xsd:schema targetNamespace="http://acme.org/sample"> <xsd:import namespace="http://schemas.xmlsoap.org/ws/2004/03/addressing" schemaLocation="../ws/addressing/WS-Addressing.xsd" /> <xsd:element name="logon" type="xsd:string" /> <xsd:element name="logonResponse" type="wsa:EndpointReferenceType"/> </xsd:schema> </types> <message name="logonInputMessage"> <part name="request" element="tns:logon"/> </message> <message name="logonOutputMessage"> <part name="response" element="tns:logonResponse"/> </message> <portType name="FactoryPortType"> <operation name="logon"> <input message="tns:logonInputMessage"/> <output message="tns:logonOutputMessage"/> <fault name="fault" message="corefaults:GCUBEFaultMessage"></fault> <fault name="fault" message="corefaults:GCUBEUnrecoverableFaultMessage"></fault> </operation> </portType> </definitions>
Just notice the following:
We use the EndpointReferenceType
type to describe endpoint references of WS-Resources. This type is defined by the WS-Addressing standard to describe endpoint references (qualified or not), and we need to import its definition from a file that ships with gCore (WS-Addressing.xsd
); note in particular the import
directive and how it is resolved relatively to the location of the interface after service deployment (remember?).
At this stage, there is little point in looking into the schema definition of this type. gCore will offer Java objects to model and serialise qualified endpoint references, in accordance with the schema definition. We just notice here that the stateful resource identifier that qualifies an endpoint reference of a WS-Resource is an XML document with an arbitrary payload. In jargon, we speak of this element as the key of the stateful resource. In gCF, in particular, the payload of resource keys is always a plain string, such as the 'name' of clients we expect to use in the design of our Sample
service.
The WSDL for the Stateful
port-type is also straightforward, at least for the time being.
<definitions name="Stateful" targetNamespace="http://acme.org/sample" xmlns:tns="http://acme.org/sample" xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:coretypes="http://gcube-system.org/namespaces/common/core/types" xmlns:corefaults="http://gcube-system.org/namespaces/common/core/faults"> <import namespace="http://gcube-system.org/namespaces/common/core/faults" location="../gcube/common/core/faults/GCUBEFaults.wsdl"/> <types> <xsd:schema targetNamespace="http://acme.org/sample"> <xsd:import namespace="http://gcube-system.org/namespaces/common/core/types" schemaLocation="../gcube/common/core/types/GCUBETypes.xsd"/> <xsd:element name="aboutSF" type="coretypes:VOID" /> <xsd:element name="aboutSFResponse" type="xsd:string" /> </xsd:schema> </types> <message name="aboutSFInputMessage"> <part name="request" element="tns:aboutSF"/> </message> <message name="aboutSFOutputMessage"> <part name="response" element="tns:aboutSFResponse"/> </message> <portType name="StatefulPortType"> <operation name="aboutSF"> <input message="tns:aboutSFInputMessage"/> <output message="tns:aboutSFOutputMessage"/> <fault name="fault" message="corefaults:GCUBEFaultMessage"></fault> </operation> </portType> </definitions>
Just notice the following:
The operation aboutSF()
takes an instance of type VOID
as a way to say that it takes nothing of interest. In particular, notice how the operation does not need any explicit information to identify the stateful resource that corresponds to the requesting client; as discussed earlier the identifier will be implicitly carried by the request, in accordance with the implied resource access pattern. As to the type VOID
, this is imported from a schema document that ships with gCore (GCUBETypes.xsd
), again relatively to the location of the interface after service deployment. The schema defines VOID
and other common types, for your convenience and to promote uniform conventions in gCube.
Now store the new interfaces in the schema folder under the service location. Remember that file names and port-types must coincide:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl [new] |---Stateful.wsdl [new] | | |--build.xml | |-Dependencies |--SampleService
Stateful Resources
Time to model stateful resources then. Inspiringly, we shall do this with objects of a Resource
class. The minimal requirements on this class are indeed minimal:
it must extend a class provided by gCF, GCUBEWSResource
.
it must implement initialise()
, the only abstract method of GCUBEWSResource
.
Here's a glorious Java class that does just that:
package org.acme.sample.stateful.Resource; import ... public class Resource extends GCUBEWSResource { /** Client visits.*/ private int visits; /** Client name. */ private String name /**{@inheritDoc}*/ public void initialise(Object... args) throws Exception { if (args == null || args.length>1) throw new IllegalArgumentException(); this.setName((String) args[0]); } public String getName() {return name;} public void setName(String name) {this.name=name;} public synchronized int getVisits() {return visits;} protected synchronized void addVisit() {this.visits++;} }
As you can see, there is little that you have to do to extend GCUBEWSResource
. gCF will invoke initialise()
in the process of creating a new WS-Resource. The single parameter is an array of zero or more parameters that might be required to initialise a stateful resource (here modelled as an optional parameter for the convenience of clients that have no parameters to pass). Here we expect the name of the client associated with the resource (the client whose visits the resource will track). We perform some checks on the input parameters (there must be exactly one), and then use it to initialise the resource in the obvious way.
initialise()
works a bit like the main()
method of standard a Java application, except that it takes Object
s rather than String
s. In the latter case, the parameters are typically specified on the command line, here you will pass them in from some other part of the code, where the decision to create a WS-Resource is first made. Remember that according to our plans we will do it from the implementation of the Factory
port-type and in response to explicit client requests.
Resource
inherits far more state and behaviour than it declares. We will unveil a small part of this heirloom as we go along. For now, we only notice that the stateful resource inherits an identifier and that the inherited method getID()
is available to show it,. The precise nature of this identifier depends on how the WS-Resource is created, and we will discuss it later when implementing the Factory
port-type.
Even the simplest of stateful resources must be ready for concurrent access. Although not exactly likely for our Sample
service, many clients may target the same WS-Resource at the same time. Concurrent client visits will be assigned different threads by gCore and different threads might concurrently access the same stateful resource through the addVisit()
and getVisits()
methods. If we did not synchronise access to either of these methods our counter may then become inconsistent. Concurrency if of course a main concern when implementing a service and concurrency it is no something gCF can entirely abstract away for you.
Save Resource
in accordance with the suggested package:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------Resource.java [new] |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
Home Sweet Home
According to our plans, the Factory
port-type will create Resource
s and the Stateful
port-type will find them, use them, and change them.
Find them from where, exactly? We could hold a repository of Resource
s in the implementation of the Stateful
port-type, but a cleaner and more general approach is to have a dedicated manager of Resource
s that can be accessed from multiple port-types and for different purposes. In gCF, managers of stateful resources are called resource homes.
Writing a simple resource home is as simple as writing a simple stateful resource. However, we cannot do it as incrementally. Along with the resource home we have to introduce a context for the associated port-type, Stateful
in our case. Only so will gCF be able to link all the pieces required for state management on our behalf. In particular:
Stateful port-types must have an associated context in gCF.
Let us start from the home anyway:
package org.acme.sample.stateful.Home; import ... public class Home extends GCUBEWSHome { /** {@inheritDoc} */ public GCUBEStatefulPortTypeContext getPortTypeContext() {return StatefulContext.getContext();} }
Notice the requirements:
The home implementationmust extend a class provided by gCF, GCUBEWSHome
.
The home implementation must implement getPortTypeContext()
, the only abstract method of GCUBEWSHome
.
Here we return the singleton instance of StatefulContext
, where StatefulContext
is defined as follows:
package org.acme.sample.stateful.StatefulContext; import ... public class StatefulContext extends GCUBEStatefulPortTypeContext { /** Singleton instance. */ private static GCUBEStatefulPortTypeContext cache = new StatefulContext(); /**Creates an instance, privately. */ private StatefulContext(){} /** Returns the singleton context. /* @return the context.*/ public static GCUBEStatefulPortTypeContext getContext() {return cache;} /** {@inheritDoc} */ public String getJNDIName() {return "acme/sample/stateful";} /** {@inheritDoc} */ public String getNamespace() {return "http://acme.org/sample";} /** {@inheritDoc} */ public GCUBEServiceContext getServiceContext() {return ServiceContext.getContext();} }
There is not much we have not already seen here:
- we follow the usual singleton pattern by returning always a single, eagerly created instance.
- we follow the usual template pattern to indicate the JNDI configuration entry-point for the associated port-type (
getJNDIName()
), the namespace in which the interface of the port-type was declared (getNamespace()
), and the context of the associated service (getServiceContext()
).
The only novel requirement is the following:
The contexts of stateful port-types must extend the GCUBEStatefulPortTypeContext
, rather than the more general GCUBEPortTypeContext
. The latter gCF class will provide our context with the additional behaviour which is required by the underlying assumption of state.
Now gCF can link our home to our port-type. However, gCF will also need to do the opposite, i.e. identify the home from the port-type context. In fact, when Sample
will start up, gCF will look into the configuration of the port-type first and will need to find in it enough information to instantiate the home. We must then dedicate a new section of the JNDI file to the configuration of this port-type, as we have done earlier. In addition, we have to point to the associated resource home from there:
<service name="acme/sample/stateful"> <resource name="home" type="org.acme.sample.stateful.Home"> <resourceParams> <parameter> <name>factory</name> <value>org.globus.wsrf.jndi.BeanFactory</value> </parameter> <parameter> <name>resourceClass</name> <value>org.acme.sample.stateful.ResourceMinimal</value> </parameter> </resourceParams> </resource> </service>
Notice the following:
The JNDI section contains a distinguished resource
element that configures the resource home associated with the port-type. The terminology is a bit unfortunate here: this resource
is a 'JNDI resource', nothing to do with a stateful resource! Along with the environment
elements you have seen so far, it is one of the two modelling primitives that we can use to structure information within JNDI fields. Hopefully, this will not be too confusing.
The resource
element must have a name
attribute with value home
. Only so, this JNDI resource can be identified as the configuration of the resource home associated with the port-type. Then it must have a type
that specifies the fully qualified name of the resource home implementation, org.acme.sample.stateful.Home
in our case;
The resource
element must be configured with at least a small number of resourceParams
. The first resourceParam
specifies a factory
that can create an instance of the resource home we are configuring. The value of this resourceParam
is normally always the same, a pre-defined bean factory that ships with gCore. This factory expects the resource home implementation to expose setters for all the other resourceParam
s that occur under resourceParams
. The GCUBEWSHome
derived by our Home
guarantees that this is the case for all the parameters that are pre-defined in gCore (if you were to add ad-hoc configuration parameters in your resource home, then your home implementation would have to include setters for these too). In most cases, you will stick with this factory implementation. We will surely do in this Primer!
The resourceParams
must include a resourceParam
called resourceClass
that indicates the fully qualified name of the class that implements the stateful resources managed by the resource home. This allows your home to create your stateful resource objects reflectively. Here we specify org.acme.sample.stateful.Resource
of course but we do not need to worry about using it directly. The GCUBEWSHome
we inherited from gCF will do it for us at the right time.
In conclusion, we have added two more pieces to the implementation of Sample
, a Home
to manage Resource
s and a StatefulContext
for the Stateful
. We have also added JNDI configuration for the Stateful
port-type, particularly information about the associated Home
. The implementation of Sample
now look as follows:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml [changed] |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------Resource.java |-------Home.java [new] |-------StatefulContext.java [new] |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
The Factory Port-Type
With Resource
, ResourceHome
, and StatefulPortTypeContext
our back-end for state management is in place. What we are left with is now to make co-ordinate use of these classes in the implementation of the Factory
and Stateful
port-types.
Starting with the Factory
port-type:
package org.acme.sample.stateful; import... public class Factory extends GCUBEPortType { GCUBELog logger = new GCUBELog(this); /** {@inheritDoc} */ protected ServiceContext getServiceContext() {return ServiceContext.getContext();} public EndpointReferenceType logon(String name) throws GCUBEFault { try { GCUBEStatefulPortTypeContext ptcxt = StatefulContext.getContext(); GCUBEWSHome home = ptcxt.getWSHome(); GCUBEWSResourceKey key = ptcxt.makeKey(name); GCUBEWSResource resource = home.create(key,name); return resource.getEPR(); } catch (Exception e) { logger.error("unable to logon", e); throw new GCUBEUnrecoverableException(e).toFault(); } } }
As we have already seen for the Stateless
port-type, our Factory
implementation extends GCUBEPortType
and implements the getServiceContext()
method.
As to the method logon()
, notice what follows:
In accordance with its interface, logon()
takes a String
and returns an EndpointReferenceType
, which is a Java model for the WS-Addressing's schema type mentioned in the WSDL of the port-type. EndpointReferenceType
Java type ships with gCore.
We obtain the singleton instance of the StatefulContext
port-type and uses it to retrieve the associated resource home. The method getHome()
is defined in GCUBEStatefulPortType
and our StatefulContext
has simply inherited it. The JNDI configuration of the port-type has provided enough information to gCF to implement that method on our behalf. Note also that since our Home
does not add any behaviour to the generic GCUBEWSHome
(nor would we need it here), we do not need to cast the return value of getWSHome()
to Home
but can work directly with the supertype GCUBEWSHome
.
We ask the home to create a stateful resource by invoking the method create()
on it. This method is predefined in GCUBEWSHome
(and thus in our Home
that inherits from it). It appears to return a generic GCUBEWSResource
but there is a more specific Resource
underneath. The home has looked into the JNDI configuration of the port-type to know which class to instantiate reflectively (remember the resourceClass
configuration parameter?). In any case, we do not need to do anything specific with this resource, so we can again leave it at that without needing to cast down to the more specific Resource
.
We invoke create()
with the identifier that we wish to give to the stateful resource and the parameters required to initialise it, here only the name of the client. This identifier is based on the name
parameter provided by the client but it's actually a GCUBEWSresourceKey
wrapper that we can ask the StatefulContext
to produce for us. The reason of wrapping our identifier into a 'key' is because we need to return it to the client and thus it needs to serialise on the wire in accordance with WS-Addressing requirements for endpoint reference qualifications. Notice that we have already introduced the notion of a WS-Resource key, check it out.
Finally, we invoke the method getEPR()
on our resource. The method is predefined in GCUBEWSResource
and returns the endpoint reference of the WS-Resource that encapulates our resource. Then we simply return the endpoint reference to the client.
A couple of extra commonents on create()
:
create()
can accept an arbitrary number of resource initialisation parameters. If more were required, we would pass them all after the key, comma-separating them (or as an array). As we have seen earlier these parameters will end up into the initialise()
method of the Resource
class. Similarly, we could pass zero initialisation parameters (i.e. only the key) if the resource did not need any at all to initialise.
the create()
could have also not taken a key at all (only parameters or absolutely nothing). In this case, the home would have automatically generated a key for the resource. The decision of whether to specify an identifier or not is our own to make. If we specify one, then the home will avoid creating a fresh resource if there is already one with that identifier. So, we specify a key any time we wish to reuse resources across 'semantically equivalent' requests. If this is not the required behaviour, i.e. we do not want reuse of stateful resources, then we do not pass a key to create()
. This depends of course on the semantics of our service; if reuse is a sensible option we should strive for it, as creating stateful resources might be in principle arbitrarily expensive processes. Here, rather artificially, we assume that the name is an unambiguous identifiers for clients (...) and decide that if we receive many requests with the same name then we will associate all of them with the same WS-Resource.
The Stateful Port-Type
Finally we come to the consumption of WS-Resources and thus to the implementation of the Stateful
:
package org.acme.sample.stateful; import ... public class Stateful extends GCUBEPortType { ... /** {@inheritDoc} */ protected ServiceContext getServiceContext() {return ServiceContext.getContext();} public String aboutSF(VOID voidType) throws GCUBEFault { ServiceContext sctx = ServiceContext.getContext(); try { Resource resource = this.getResource() resource.addVisit(); String name = resource.getName(); StringBuilder output = new StringBuilder(); ...build output as in Stateless.about()... output.append("\nThis is your invocation N." + resource.getVisits() + "\n"); return output.toString(); } catch (GCUBEException e) {throw e.toFault();} catch (Exception e) {throw sctx.getDefaultException(e).toFault();} } private Resource getResource() throws ResourceException { return (Resource) StatefulContext.getContext().getWSHome().find(); } ...
There are essentially two things to notice here:
the method aboutSF()
behaves like about()
in the implementation of the Stateless
port-type (check any of the versions we have looked at before). The significant difference is that now we are acting in the context of a WS-Resource and thus upon an underlying stateful resource. We know by now that this resource will be implicitly identified in the endpoint reference use to call this WS-Resource, and we delegate the task to retrieve it to a private helper method getResource()
. Assuming we get one back, we increment its count of client visits and proceed with satisfying the client request. In particular, we append the current count of visits at the end of the message that we return to the client.
in getResource()
, we delegate in turn the task to retrieve the stateful resource to the resource home, which we obtain from the context of the Stateful
port-type. We then ask the home to find the required resource, by invoking a method find()
that our Home
inherits from GCUBEWSHome
. As the invocation specifies no parameters, the home resolves it by automatically extracting the endpoint reference with which the current call was made by the client. It will then look into that reference and extract the stateful resource key and use the key to lookup the required resource. As this operation is a general one defined by GCUBEWSHome
, its return type is as generic as GCUBEWSResource
and, since we plan to invoke Resource
-specific methods, we need to down cast it before we can return a specific Resource
.
in the process of implied resource access, a few things could go wrong. The call may have been made with an unqualified endpoint reference, i.e. might have been addressed to the port-type rather than a WS-Resource that encapsulates the port-type. Even if the endpoint reference is qualified with a key, the home may still fail to find a resource with that key. This could be because one such resource never existed (the endpoint reference was somehow incorrectly built), or because it existed but it has somehow been removed. We will return later to this possibility.
With our two new port-type implementations, the service implementation ought to look as follows:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------Resource.java |-------Home.java |-------StatefulContext.java |-------Factory.java [new] |-------Stateful.java [new] |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
A Quick Test
Ok, it's time to make sure that all the pieces developed so far come together nicely. For this purpose, we will write a couple of simple test clients. The first client, CreateResource
, will invoke the logon()
operation of the Factory
port-type to create a WS_Resource. The second client, StatefulTest
will invoke the method aboutSF()
of that WS-Resource. The two clients will exchange the endpoint reference of the WS-Resource that the first creates and the second consumes. The exchange will be based on the file system: CreateResource
will serialise on file the endpoint reference of the WS-Resource it creates; StatefulTest
will deserialise the endpoint reference from that file and use it.
Starting with CreateResource
:
package org.acme.sample.tests; import ... public class CreateResource { static GCUBEClientLog logger = new GCUBEClientLog(CreateResource.class); public static void main(String[] args) throws Exception { logger.info("creating WS-Resource..."); try { EndpointReferenceType factoryEPR = new EndpointReferenceType(new AttributedURI(args[0])); FactoryPortType stub = new FactoryServiceAddressingLocator().getFactoryPortTypePort(endpoint); stub=GCUBERemotePortTypeContext.getProxy(stub,GCUBEScope.getScope(args[1])); EndpointReferenceType resourceEpr = stub.logon(args[2]); logger.trace("created resource at endpoint "+wsEpr); FileWriter writer = new FileWriter("/tmp/resource.epr"); ObjectSerializer.serialize(writer,resourceEpr,new QName("http:/org.acme","statefulEPR")); writer.close(); } } }
The first part of the client has been already discussed when we tested the Stateless
port-type, including logging practices. We expect three
arguments: the endpoint reference of a running Factory
, a scope in which to make the call which is compatible with the targeted Factory
, and a name for the client.
We then invoke the logon()
operation and get back a endpoint reference to a WS-Resource. To store it, we invoke the serialize()
method of a utility class that ships with gCore, ObjectSerializer
, passing the a FileWriter
initialised with a file on the file system, the WS-Resource endpoint reference to serialise, and a QName
of our own invention needed to wrap the endpoint reference in well-formed XML.
Next is instead the second client, StatefulTest
:
package org.acme.sample.tests; import ... public class StatefulTest { static GCUBEClientLog logger = new GCUBEClientLog(StatefulTest.class); public static void main(String[] args) throws Exception { logger.info("visiting WS-Resource..."); try { FileReader reader = new FileReader("/tmp/resource.epr"); EndpointReferenceType resourceEPR= (EndpointReferenceType) ObjectDeserializer.deserialize(newInputSource(reader),EndpointReferenceType.class); reader.close(); StatefulPortType stub = new StatefulServiceAddressingLocator().getStatefulPortTypePort(resourceEPR); stub = GCUBERemotePortTypeContext.getProxy(stub, GCUBEScope.getScope(args[0])); logger.trace(stub.aboutSF(new VOID())); } } }
Here, we deserialise the resource following a process inverse to its serialisation (just take this code as boiler plate, really). The rest ought to be familiar by now, including the expectation of a scope as a test parameter.
With the two test clients, the service implementation ought to look as follows: look as follows:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml [changed] |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------Resource.java |-------Home.java |-------StatefulContext.java |-------Factory.java |-------Stateful.java |------tests |-------StatelessTest.java |-------CreateResource.java [new] |-------StatefulTest.java [new] | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies >
Delving into the implementation
The implementation of the stateful part of the SampleService is a bit more complex than the stateless implementation and requires to code:
- the stateful context,
- the stateful resource,
- the resource home,
- and the two new port-types.
All the new classes implemented in this part will be placed in the new org.acme.sample.stateful
package.
Important note: since we are leaving the stateless port-type as part of our example, we do not need to extend again the GCUBEStartupPortType (in charge of the initialisation of the GCUBEServiceContext) in one of the two new port-types. Instead, if we would start from scratch a complete stateful service, it is mandatory that one port-type extends the GCUBEStartupPortType (ideally, the Factory port-type, but it's up to developer.
The Stateful Context
We begin by implementing the Stateful Context. This context models the configuration of the acme/sample/stateful port-type. It transparently adds a lot of facilities to access the statefulness of the port-type.
package org.acme.sample.stateful; import org.acme.sample.ServiceContext; import org.gcube.common.core.contexts.GCUBEServiceContext; import org.gcube.common.core.contexts.GCUBEStatefulPortTypeContext; public class StatefulContext extends GCUBEStatefulPortTypeContext { public static String FREQUENT_USER_LIMIT_JNDI_NAME = "frequentUserLimit"; private static GCUBEStatefulPortTypeContext cache = new StatefulContext(); @Override public String getJNDIName() {return "acme/sample/stateful";} @Override public String getNamespace() {return "http://acme.org/sample";} @Override public GCUBEServiceContext getServiceContext() { return ServiceContext.getContext(); } public static GCUBEStatefulPortTypeContext getPortTypeContext() { return cache; } }
Notes:
- the class extends the
GCUBEStatefulPortTypeContext
- the class adopts the singleton pattern as suggested for any context
- the
getJNDIName()
method returns the JNDI name of the port-type - the
getServiceContext()
method connects the port-type to the service context - the
getNamespace()
method returns the namespace of the port-type as defined in the WSDL interface
The Resource
The Resource class is the core of the statefulness. It models the state of a port-type and the way in which it can be accessed and modified. Our Resource will maintain the state in 2 properties:
- the name of the user
- a counter about how many times it accesses the resource
package org.acme.sample.stateful; import org.gcube.common.core.state.GCUBEWSResource; import org.globus.wsrf.ResourceException; public class Resource extends GCUBEWSResource { protected static final String RP_NAME = "Name"; protected static final String RP_VISITS = "Visits"; protected static String[] RPNames = { RP_NAME, RP_VISITS }; @Override protected void initialise(Object... args) throws ResourceException { if (args.length!=1) throw new IllegalArgumentException(); try { String name = (String) args[0]; this.setName(name); this.setVisits(0); } catch (Exception e) { throw new ResourceException(e); } } /** * {@inheritDoc} */ public String[] getPropertyNames() { return RPNames; } /** * Returns the name of the RP stored on this Resource. * * @return the name. */ public String getName() { return (String) this.getResourcePropertySet().get(RP_NAME).get(0); } /** * Returns the number of visits for the user * @return */ public Integer getVisits() { return (Integer) this.getResourcePropertySet().get(RP_VISITS).get(0); } /** * Sets the name of the property. * * @throws Exception */ protected synchronized void setName(String name) throws Exception { this.getResourcePropertySet().get(RP_NAME).clear(); this.getResourcePropertySet().get(RP_NAME).add(name); } /** * Sets the name of the property. * * @throws Exception */ protected synchronized void setVisits(Integer visits) throws Exception { this.getResourcePropertySet().get(RP_VISITS).clear(); this.getResourcePropertySet().get(RP_VISITS).add(visits); } }
Notes:
- the class overrides the
initialise()
method to perform a custom initialisation of the properties - the two properties are not maintained as private members of the class, they are transparently managed by the superclass in a
GCUBEWSResourcePropertySet
instance. In order to do that the class:- declares the name of the two properties
- overrides the
getPropertyNames()
method to return the property names
- the getters and setters implementation of the properties reflects the
GCUBEWSResourcePropertySet
management of such properties
The Resource Home
The implementation of the Resource Home is very simple, since most of the work is performed by the superclass behind the scene. All it is needed is to override a method to connect the Home to the port-type context.
package org.acme.sample.stateful; import org.gcube.common.core.contexts.GCUBEStatefulPortTypeContext; import org.gcube.common.core.state.GCUBEWSHome; public class Home extends GCUBEWSHome { @Override public GCUBEStatefulPortTypeContext getPortTypeContext() { return StatefulContext.getPortTypeContext(); } }
Notes:
- the class extends the
GCUBEWSHome
class - the
getPortTypeContext()
method connects the class to the stateful port-type
The Service instance port-type
The Service
class provides an implementation of the Service instance port-type defined in the WSDL interface.
package org.acme.sample.stateful; import org.acme.sample.ServiceContext; import org.gcube.common.core.contexts.GCUBEStatefulPortTypeContext; import org.gcube.common.core.contexts.GHNContext; import org.gcube.common.core.faults.GCUBEFault; import org.gcube.common.core.faults.GCUBEUnrecoverableException; import org.gcube.common.core.types.VOID; import org.globus.wsrf.ResourceException; public class Service { public String aboutSF(VOID voidType) throws GCUBEFault { StringBuilder output = new StringBuilder(); GHNContext nctx = GHNContext.getContext(); ServiceContext sctx = ServiceContext.getContext(); GCUBEStatefulPortTypeContext pctx = StatefulContext.getPortTypeContext(); try { final Resource resource = this.getResource(); resource.setVisits(resource.getVisits() + 1); output.append("Hello " + resource.getName()).append(", you have invoked porttype "). append(pctx.getName() + " \nof service " + sctx.getName()). append(", \nwhich you found at "). append(pctx.getEPR() + "in the gCube infrastructure " + nctx.getGHN().getInfrastructure()). append( " \nand you are in the Scope " + sctx.getScope()). append(" \nThis is your invocation N." + resource.getVisits() + "\n"); if (resource.getVisits() >= (Integer)pctx.getProperty(StatefulContext.FREQUENT_USER_LIMIT_JNDI_NAME, true)) { output.append("welcome in the frequent user club!"); } } catch (Exception e) { throw new GCUBEUnrecoverableException(e).toFault(); } return output.toString(); } /** * * @return the stateful resource * @throws ResourceException if no resource was found in the current context */ private Resource getResource() throws ResourceException { return (Resource) StatefulContext.getPortTypeContext().getWSHome().find(); } }
Notes:
- the class implements the aboutSF operation in the
aboutSF()
method - the method retrieves various contexts to use when producing the output string
- the most interesting part of the class is the
getResource()
method demonstrating the way to access from the stateful context the actualResource
instance
The Factory port-type
The Factory
class provides an implementation of the Factory port-type defined in the WSDL interface.
package org.acme.sample.stateful; import org.apache.axis.message.addressing.EndpointReferenceType; import org.gcube.common.core.contexts.GCUBEStatefulPortTypeContext; import org.gcube.common.core.faults.GCUBEFault; import org.gcube.common.core.faults.GCUBEUnrecoverableException; public class Factory { public EndpointReferenceType logon(String name) throws GCUBEFault { //create/reuse the resource try { GCUBEStatefulPortTypeContext ptcxt = StatefulContext.getPortTypeContext(); return ptcxt.getWSHome().create(ptcxt.makeKey(name), name).getEPR(); } catch (Exception e) { throw new GCUBEUnrecoverableException(e).toFault(); } } }
Notes:
- the class implements the logon operation in the
logon
method - the method retrieves the Stateful context and from it retrieves also the
Home
instance - by invoking the
create
method, the stateful resource is created with the given key (first input parameter) and with the given list of parameters (only thename
variable here). The list is then passed to theinitialise()
method of theResource
instance - the
makeKey()
method of theStatefulContext
instance creates a newGCUBEWSResourceKey
- the
getEPR()
method of theResource
instance return the EndPointReference to use to access the new state from clients
Service folder tree after the implementation
All the java files described has to be placed in the src/org/acme/sample/stateful folder under the service location:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------StatefulContext.java |-------Resource.java |-------Home.java |-------Service.java |-------Factory.java |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
JNDI configuration
The deploy-jndi-config.xml file must be updated in order to add the configuration of the stateful port-type. The stateful configuration has to declare the "home" resource that instructs gCore about how to create a new stateful resource.
This configuration is automatically loaded by the gCore in the StatefulContext
instance from which the configuration can be accessed programmatically.
<?xml version="1.0" encoding="UTF-8"?> <jndiConfig xmlns="http://wsrf.globus.org/jndi/config"> <service name="acme/sample"> <environment name="configDir" value="@config.dir@" type="java.lang.String" override="false" /> </service> <service name="acme/sample/stateful"> <resource name="home" type="org.acme.sample.stateful.Home"> <resourceParams> <parameter> <name>factory</name> <value>org.globus.wsrf.jndi.BeanFactory</value> </parameter> <parameter> <name>resourceClass</name> <value>org.acme.sample.stateful.Resource</value> </parameter> </resourceParams> </resource> <environment name="frequentUserLimit" value="3" type="java.lang.Integer" override="false" /> </service> </jndiConfig>
The following is worth noticing:
- the JNDI still included the mandatory
service
element entirely dedicated to the SampleService as a whole - the
service
element named acme/sample/stateful groups the configuration of our stateful port-type; here it only includes the Home resource and a custom property. In the section How to publish the state we will see other configuration elements to add in this service element allowing to publish the service state - the
frequentUserLimit
environment element shows an example of custom property that can be programmatically read within the service code
Save the new configuration in the file called deploy-jndi-config.xml and place it in the etc folder under the service location:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------StatefulContext.java |-------Resource.java |-------Home.java |-------Service.java |-------Factory.java |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
Building & Deploying
Towards Deployment: the WSDD descriptor
The two new port-types have to be declared in the deployment descriptor as follows.
<?xml version="1.0" encoding="UTF-8"?> <deployment name="defaultServerConfig" xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <service name="acme/sample/stateless" provider="Handler" use="literal" style="document"> <parameter name="className" value="org.acme.sample.stateless.Stateless"/> <wsdlFile>share/schema/org.acme.sample/Stateless_service.wsdl</wsdlFile> <parameter name="allowedMethods" value="*"/> <parameter name="handlerClass" value="org.globus.axis.providers.RPCProvider"/> <parameter name="scope" value="Application"/> <parameter name="loadOnStartup" value="true"/> <parameter name="securityDescriptor" value="@config.dir@/security_descriptor.xml"/> </service> <service name="acme/sample/stateful" provider="Handler" use="literal" style="document"> <parameter name="className" value="org.acme.sample.stateful.Service"/> <wsdlFile>share/schema/org.acme.sample/Stateful_service.wsdl</wsdlFile> <parameter name="allowedMethods" value="*"/> <parameter name="handlerClass" value="org.globus.axis.providers.RPCProvider"/> <parameter name="scope" value="Application"/> <parameter name="providers" value="GCUBEProvider"/> <parameter name="loadOnStartup" value="true"/> <parameter name="securityDescriptor" value="@config.dir@/security_descriptor.xml"/> </service> <service name="acme/sample/factory" provider="Handler" use="literal" style="document"> <parameter name="className" value="org.acme.sample.stateful.Factory"/> <wsdlFile>share/schema/org.acme.sample/Factory_service.wsdl</wsdlFile> <parameter name="allowedMethods" value="*"/> <parameter name="handlerClass" value="org.globus.axis.providers.RPCProvider"/> <parameter name="scope" value="Application"/> <parameter name="loadOnStartup" value="true"/> <parameter name="securityDescriptor" value="@config.dir@/security_descriptor.xml"/> </service> </deployment>
The deploy-server.wsdd file has to be updated with the above content in the etc folder under the service location:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------StatefulContext.java |-------Resource.java |-------Home.java |-------Service.java |-------Factory.java |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
Configuring the build process
The build properties configuration file has now to declare the two new port-types to build.
package = org.acme.sample lib.dir = Dependencies/SampleService wsdl.1 = Stateless wsdl.2 = Stateful wsdl.3 = Factory namespace.1=http://acme.org/sample
The build.properties file has to be updated with the above content in the etc folder under the service location:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------StatefulContext.java |-------Resource.java |-------Home.java |-------Service.java |-------Factory.java |------tests |-------StatelessTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService
Building the stubs and the service
The building process does not change. The same steps seen in the Stateless part of the tutorial have to be performed for building the stubs and the service.
(Un)Deploying the service
The (un)deploying process does not change. The same steps seen in the Stateless part of the tutorial have to be performed for deploying and undeploying the service.
A Test Client
package org.acme.sample.tests; import org.acme.sample.stubs.FactoryPortType; import org.acme.sample.stubs.StatefulPortType; import org.acme.sample.stubs.service.FactoryServiceAddressingLocator; import org.acme.sample.stubs.service.StatefulServiceAddressingLocator; import org.apache.axis.message.addressing.AttributedURI; import org.apache.axis.message.addressing.EndpointReferenceType; import org.gcube.common.core.scope.GCUBEScope; import org.gcube.common.core.types.VOID; import org.gcube.common.core.utils.logging.GCUBEClientLog; import org.gcube.common.core.contexts.GCUBERemotePortTypeContext; public class StatefulTest { static GCUBEClientLog logger = new GCUBEClientLog(StatefulTest.class); public static void main(String[] args) throws Exception { logger.debug("Stateful client is running..."); EndpointReferenceType factory_endpoint = new EndpointReferenceType(); factory_endpoint.setAddress(new AttributedURI(args[0])); FactoryPortType factoryPT = new FactoryServiceAddressingLocator().getFactoryPortTypePort(factory_endpoint); //we proxy the factory PT with the desired scope factoryPT = GCUBERemotePortTypeContext.getProxy(factoryPT, GCUBEScope.getScope(args[2])); EndpointReferenceType service_endpoint = factoryPT.logon(args[1]); StatefulPortType statefulPT = new StatefulServiceAddressingLocator().getStatefulPortTypePort(service_endpoint); // then we have to proxy also the stateful PT... statefulPT = GCUBERemotePortTypeContext.getProxy(statefulPT, GCUBEScope.getScope(args[2])); System.out.println(statefulPT.aboutSF(new VOID())); //... but only once, if we use it again, there is no need to proxy again for subsequent uses System.out.println(statefulPT.aboutSF(new VOID())); } }
Save the file StatefulTest.java under the src/org/acme/test folder under the service location.
Final structure of the service location
At the end of this section, the service location tree should look as follows:
|-SampleService |--etc |---profile.xml |---deploy-jndi-config.xml |---deploy-server.wsdd |---build.properties | |--src |---org |----acme |-----sample |------ServiceContext.java |------stateless |-------Stateless.java |------stateful |-------StatefulContext.java |-------Resource.java |-------Home.java |-------Service.java |-------Factory.java |------tests |-------StatelessTest.java |-------StatefulTest.java | |--schema |---Stateless.wsdl |---Factory.wsdl |---Stateful.wsdl | | |--build.xml | |-Dependencies |--SampleService