The Development Cycle

From GCube System
Revision as of 18:11, 13 April 2008 by Fabio.simeoni (Talk | contribs) (The Build Properties)

Jump to: navigation, search

Let us go through a quick tour of development with SampleService.

My First Profile

A good starting point with service development is its configuration. And a good starting point when configuring a service is the definition of its profile. The role and structure of gCube service profiles is described in detail elsewhere. Here we just show one of the smallest and yet perfectly well-formed profile a service may have:

<?xml version="1.0" encoding="UTF-8"?>
<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>
		<Version>1.0</Version>
		<Packages>
			<Main>
				<Name>Main</Name>
				<Version>1.0</Version>
				<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>...</WSDL>
				</PortType>
			</Main>
			<Software>
				<Description>Describes port-type stubs</Description>
				<Name>Stubs</Name>
				<Version>1.0</Version>
				<Files><File>org.acme.sample.stubs.jar</File></Files>
			</Software>
		</Packages>
	</Profile>
</Resource>

By necessity, this profile reveals things yet to come. For the time being, be happy with a conceptual summary:

  • SampleService is a a particular type of gCube Resource, one of type Service. Like all gCube Resources has an ID, which it receives when it is formally registered with the infrastructure. Until then, SampleService does not have a proper identifier. No worries though, a temporary one will be generated by gCF at runtime.
  • The profile of SampleService specifies its Name and specifies its membership to a Class of functionally related gCube services. Classes are not pre-defined, and here we settle on Samples. A free-form Description and a Version complement the preliminary description of SampleService.
  • SampleService is physically comprised of Packages. A Main package identifies the main artefact to emerge from the build of the service, a GARArchive, and provides information about the service port-types, including their WSDLs' (here omitted for mercy). The profile unveils our initial plans for a single stateless port-type, which we name by identifying the relative part of the URI at which it will be accessible on the gHN (more on this soon).
  • Another Software component identifies the secondary build artefact of the service, its own stubs. The Main package depends always upon its stubs and this dependency must be explicitated.

Note: If the notion of build artefact is not obvious to you, bide your time for a little longer or jump ahead.

Save the profile in a file called profile.xml and place it in the etc folder of the service implementation:

|-SampleService
|--etc
|---profile.xml
|--src
|--schema
|
|-Dependencies
|--SampleService

A Tiny JNDI

Next we move to the configuration of the code itself, for which we use JNDI technology. Again, the details and granularity of JNDI configuration are discussed elsewhere. Here we simply notice that, while the JNDI configuration might contain any information which the code needs to consult at runtime, the gCF raises precise requirements against the existence and shape of some of that information. In particular, the configuration should at least contain the following information:

<?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>

</jndiConfig>

Essentially, the configuration must have a service section dedicated to information conceptually associated with the entire implementation, rather than more specific components yet to come. Furthermore, the service section must:

  • specify the name of the service. This is not particularly constrained but must distinguish SampleService from any other which may be deployed on the same gHN. As we already do with Java and URI namespaces, we can approach uniqueness through a hierarchical structure, here of /-separated strings. In our case, the name acme/sample will suffice as a unique identifier.
  • include an environment element which specifies the absolute path to the configuration folder of SampleService. Contrary to expectations, this is not our etc folder under the service location. It is in fact another configuration folder allocated to SampleService at the point of service deployment. This will become clearer later. For the time being, we use the wildcard @config.dir@ to abstract over the precise location of this yet-to-be folder, and leave to gCore the task of resolving it at the point of deployment.

Note: The wildcard @config.dir is surely convenient at this stage. It is in fact necessary for any real gCube service. This is because the service may be dynamically deployed on any gHN and the absolute path of the configuration folder depends on the file system of that gHN. During service development, that location is an absolute unknown.

So far, there is little in our configuration which is specific to SampleService. Indeed, the JNDI above can be considered as boilerplate for any gCube service. We will see shortly how to inject more information into the JNDI configuration, but until that moment this minimal configuration will suffice.

Save the configuration in a file called deploy-jndi-config.xml and place it in the etc folder of the service implementation:

|-SampleService
|--etc
|---profile.xml
|---deploy-jndi-config.xml
|--src
|--schema
|
|-Dependencies
|--SampleService


A PortType Interface

Enough configuration for now. Let us move to the definition of the interface for the first port-type we plan to add to SampleService. The general expectation is that all your interfaces will have this high-level structure:

<?xml version="1.0" encoding="UTF-8"?>
<definitions 
               xmlns:tns="..YOUR NAMESPACE.." targetNamespace="...YOUR NAMESPACE..." name="..SOME_NAME... "
               xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >

	<types>
	   <xsd:schema targetNamespace=".....">
			<!-- REQUEST & RESPONSE TYPE DEFINITIONS -->
	    </xsd:schema>
	</types>
	
	<!-- MESSAGE DEFINITIONS  -->

	<portType name="...PORT_TYPE NAME...portType">
		<!--OPERATIONS DEFINITIONS -->
	</portType>

</definitions>

All pretty standard WSDL, of course. And yet notice what follows:

  • we dedicate the WSDL to a single port-type (even though multiple port-types are admitted by the WSDL specs). This simplifies our building configuration and suggests than any form of sharing across port-types (type definition, messages, faults, etc.) be achieved through imports/includes.
  • the name of the WSDL definitions does not play much of a role during development. Use anything that looks appropriate. Given that there is a one-to-one correspondence between WSDLs and port-types use, any informal name you may wish to give to the port-type.
  • the name of the portType also refers to the port-type, though this has a very important role during the build of the service. In particular, it must end in portType.

The WSDL of our first port-type illustrate these conventions:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns:tns="http://acme.org/sample" name="Stateless" targetNamespace="http://acme.org/sample"
    xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"  >

	<types>
		<xsd:schema targetNamespace="http://acme.org/sample">

			<xsd:element name="about" type="xsd:string"/>
			<xsd:element name="aboutResponse" type="xsd:string"/>

		</xsd:schema>
	</types>

	<message name="aboutInputMessage">
		<part name="request" element="tns:about"/>
	</message>
	<message name="aboutOutputMessage">
		<part name="response" element="tns:aboutResponse"/>
	</message>

	<portType name="StatelessPortType">
		    
             <operation name="about">
			<input message="tns:aboutInputMessage"/>
			<output message="tns:aboutOutputMessage"/>			
		</operation>

	</portType>
</definitions>

As you can see:

  • we have used the namespace http://acme.org/sample for our port-type.
  • we have settled on the name Stateless for it, and used it twice. The first time to name the whole interface definition and a second time in the name StatelessPortType of the portType definition.
  • we have defined a single operation about for Stateless, which takes and returns a string. The idea is to expect the name of the caller as input and produce some information about the port-type as output.

Hard to imagine a simpler port-type, really. Save the interface in a file Stateless.wsdl in the schema folder under the service location:

|-SampleService
|--etc
|---profile.xml
|---deploy-jndi-config.xml
|
|--src
|
|--schema
|---Stateless.wsdl
|
|-Dependencies
|--SampleService

Note: The file in which you save the interface must have the same name as the port-type, here Stateless.

A Simple Implementation

So far, the development process has been dominated by simple but numerous conventions upon the use of not necessarily simple but equally numerous formats. Frankly, all very unexciting and error-prone. Welcome (or welcome back) to the grim reality of web service development. gCF cannot take much of this pain away, unfortunately. In fact, gCF has only added to those conventions with no visible returns. Delving into the implementation of SampleService hopefully will give you the first glimpse of rewards to come.

A natural place to start the implementation is right at its centre, with a service context. Among other things, a service context manages configuration, scoping, security, and lifetime on behalf of the rest of the code. In gCF, you can have your own wih a few lines of code:

package org.acme.sample;
import ...

public class ServiceContext extends GCUBEServiceContext {
		
	/** {@inheritDoc} */
	protected String getJNDIName() {
                  return "acme/sample";
        }
	
}

As you can see, it is enough to:

  • inherit your service context from GCUBEServiceContext.
  • let the inherited code know how to act on your behalf by implementing getJNDIName() and returning the name of the JNDI section dedicated to the service ( remember?).

Note: This is a first instance of the template pattern: you complete the interface by exposing service-specific information which the inherited code uses to customise the work it carries out for you. It is simple, and it's used a lot within gCF.

The rest of the implementation could use the ServiceContext by instantiating it. For efficiency and exchange of data, however, it is more appealing to force the use of a single ServiceContext throughout the implementation. A simple way of doing this is as follows:

/** Single context instance, created eagerly */
private static ServiceContext singleton = new ServiceContext();
		
/** Prevents accidental creation of more instances */
private ServiceContext(){};

/** Returns cached instance */
public static ServiceContext getContext() {return singleton;}
  • a single context object is created the first time the class is loaded (singleton field declaration).
  • no other such object can be created by clients (private constructor).
  • the object is available on demand (getContext()).

Note: This is an instance of the singleton pattern and gCF cannot 'force' it as a contract upon your code without taking away too much of its simplicity. It just recommends it.

We will go back to the service context in the rest of the Primer, as its feature will be needed. Even If we didn't, it would already carry out important work during service startup, as you will see later on. More information on the role and features of a service context in gCF is here.


Next, the implementation of the port-type interface. This is also straightforward:

package org.acme.sample.stateless;
import ...

public class Stateless extends GCUBEStartupPortType {

	/** {@inheritDoc} */
	protected GCUBEServiceContext getServiceContext() {return ServiceContext.getContext();}
	
	public String about(String name) {		
		return ("Hello " + name + ", you have invoked service ")+this.getServiceContext().getName() + " ("+this.getServiceContext().getServiceClass() + ")";				
	}
}

but do please note:

  • the implementation extends GCUBEStartupPortType, which initialises the service context when the port-type is acivated in turn at container startup. One service port-type must always assume the role of 'fire-starter', in case you have many. We will refer to it as the startup port-type from now onwards.
  • the name and class of the service as defined in the service profile can be obtained from the service context, as can any piece of information in the profile if it proves useful for dynamic introspection.

By now the service location should look as follows:

|-SampleService
|--etc
|---profile.xml
|---deploy-jndi-config.xml
|
|--src
|---org
|----acme
|-----sample
|------ServiceContext.java
|------stateless
|-------Stateless.java
|
|--schema
|---Stateless.wsdl
|
|-Dependencies
|--SampleService

Building & Deploying

By now, the definition of the service is completed: configuration, interfaces, and implementation components are all in place. The next step is to build artefacts from the service definition and deploy them within the gHN. There are two main artefacts we may wish to build and deploy:

  • a library of service stubs, i.e. Java code automatically generated from port-type interfaces which hides away the low-level details of communication between the service and its clients. We wish to build stubs separately from the service so that we can more easily distribute them to clients.
  • a packaging of configuration, interface, and implementation components from the service definition which is suitable for deployment in the gHN.

It should not be too surprising by now that both building and deployment require dedicated configuration.

The Deployment Descriptor

We configure deployment so as to tell the gHN how to manage a Running Instance of the service. In particular, we need to the gHN how to instantiate the port-type implementations and how to recognise and correctly dispatch calls to them. Here is the so-called deployment descriptor for SampleService:

<?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/SampleService/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>
    	  
</deployment>

The deployment descriptor ought to include a service section for each port-type. The following is worth noticing:

  • the name attribute tells the gHN how to distinguish calls to the port-type from calls to other port-types. It is the relative part of the URI at which the port-type is accessible, its endpoint. In particular, the port-type endpoint is formed by concatenating the value of the name parameter with the endpoint of the gHN. Given that the endpoint of the gHN is:
http:// <hostname>:<port>/wsrf/services

then the endpoint of our stateless port-type is:

http://<hostname>:<port>/wsrf/services/acme/sample/stateless
  • the className parameter names the class which implements the port-type. This is used by the gHN to create an instance of that class and forward to it all the calls for the port-type that it receives.
  • the wsdlFile parameter specifies the location of the port-type interface after deployment. We will be able to explain its value later on.
  • the scope parameter tells the gHN how many port-type instances it ought to create in order to serve requests. A value of Application indicates that we wish all requests to be handled by single object of the implementation class. Alternatively, a value of Request" indicates that we want a new port-type instance for each request.
  • the loadOnStartup parameter indicates that the gHN should create the port-type instance when it starts up, rather than when it receives the first request for it.

Now, notice the following important points:

Save the deployment descriptor in a file called deploy-server.wsdd under the etc folder of the service location:

|-SampleService
|--etc
|---profile.xml
|---deploy-jndi-config.xml
|---deploy-server.wsdd
|
|--src
|---org
|----acme
|-----sample
|------ServiceContext.java
|------stateless
|-------Stateless.java
|
|--schema
|---Stateless.wsdl
|
|-Dependencies
|--SampleService

The Build Properties

We use Ant for building artefacts and thus rely on a buildfile to define the build process. There is one which ships with gCore and is tailored to the structure of the service location recommended above ($GLOBUS_LOCATION/share/gcore_tools/build-sample.xml). Just copy it to your service location and rename it to build.xml. This buildfile should cater for the build of any gCube service and has been carefully optimised for maximum usability and generality. Stick to it unless you know what you are doing,

The standard buildfile requires configuration. First of all, it needs to know the absolute path to the location from which you will build the artefacts, the build location. This is expected as the value of the environment variable BUILD_LOCATION. For convenience, we use the location of the Eclipse workspace as our build location:

export BUILD_LOCATION = ...workspace location...

Second, the buildfile requires you to specify a small set of build properties:

 package = org.acme.sample
 lib.dir = Dependencies/SampleService 
 wsdl.1 = Stateless
 namespace.1=http://acme.org/sample

where:

  • package is the 'root package' of the service implementation, i.e. the first package in the implementation hierarchy which identifies the implementation unambiguously. The buildfile uses this property to guarantee unique names to the artefacts it produces. As we shall see, this guarantees the they can be safely deployed without any clashes with other service implementations. Let us assume that org.acme.sample identifiers uniquely the implementation of SampleService within ACME and, through Java package conventions, pretty much everywhere.
  • lib.dir is the path to the custom dependency folder, relative to the build location. The buildfile needs to know where are your custom dependencies so as to put them on the classpath during compilation.
  • wsdl.1 is the name of the file which containes the WSDL interface of the first port-type. The buildfile uses this property to locate the interface under the service location, process it, and generate stubs from it.
  • namespace.1 is the namespace of the port-type, as specified in its WSDL. The buildfile uses this property to determine the Java package of the stubs generated from the WSDL interface.

Note: If you had many port-types, you would list them all in arbitrary order (wsdl.2,wsdl.3,...).

Note: If you had different namespaces across WSDLs and/or iauxiliary XSDs, you would also list them all in arbitrary order.

Note: By default, all namespaces are mapped to ${package}/stubs. In our case, it would place them under org.gcube.sample.stubs. If you wish to map different namespaces onto different packages, you can do so with optional properties package.<n>. The buildfile will consider its value when generating stubs for interface elements in the namspace namespace.<n>. For example, if you specify the property:

package.1=stateless

then the buildfile would create stubs for our stateless port-type under org.ample.stubs.stateless.

Save the configuration in a build.properties file and place it in 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
|
|--schema
|---Stateless.wsdl
|
|
|--build.xml
|
|-Dependencies
|--SampleService

Building & Deploying Stubs

Build time. From the build location, type:

ant -f SampleService/build.xml stubs

This invokes ANT and tells it to process the buildfile of SampleService, specifically the instructions for building the stub library which are grouped under the target stubs. If all goes according to plans, the buildfile:

  • generates stubs source code from the port-type interfaces, compiles it, and packages it in a org.acme.sample.jar file.
  • deploys the packaged stubs in two locations:
    • ${GLOBUS_LOCATION}/lib, where it will be found as a run-time dependency of the service. Everything in here is on the classpath when the gHN is running.
    • ${BUILD_LOCATION}/Dependencies/SampleService, where it can be found as a compile-time dependency of the service. This is why we specified the custom dependency location in the build properties.

Note: The build process creates a build folder under the build location, creating it if does not exist and clearing it if it already exists. It uses this folder as a scratchpad to organise all the elements required for the build (code, configuration, interfaces)., It copies most elements from the service location, of course, but it complements them with other elements which are cross-service and ship with gCore. In most cases, you should not concern yourself with the contents of the build folder but it is good to know who creates it and why.

Note: Sometimes you might want to generate the stubs or go as far as to package them, but not deploy them. Say you want to make sure they are generated correctly, and perhaps have a peek inside the jar file, but are not ready to replace the stub library previously deployed. In these cases, you can invoke the buildStubs or the jarStubs targets:

ant -f SampleService/build.xml buildStubs   ...generates stubs but does not package them...
ant -f SampleService/build.xml jarStubs       ...generates and packages stubs but does not deploy them... 

Note: When we will build the service, the buildfile will automatically resolve the dependency of SampleService to its own stubs from the custom dependency folder. Eclipse needs instead to be told explicitly. For this, you should add ${BUILD__LOCATION}/Dependencies/SampleService/org.acme.sample.stubs.jar to the build path of your Eclipse project (Project -> Properties -> Java Build Path -> Libraries -> Add Jars...). The same is true for any other custom dependency, of course. For stubs, however, the good news is that we do not have to manually copy the library into the custom dependency folder.

Building & Deploying the Service

From the build location, type:

ant -f SampleService/build.xml

which points implicitly to the target deployService. If all goes according to plans, the buildfile

  • compiles the service implementation and packages it along with service configuration and port-type interfaces in a org.acme.sample.gar file.
  • deploys org.acme.sample.gar in the gHN.


(Un)Deploying the Service

To deploy the SampleService, type in your BUILD_LOCATION folder:

${BUILD_LOCATION}> gcore-deploy-service SAMPLESERVICE/org.acme.sample.gar

This preforms a runtime deployment of the service by:

  • assigning it the ID org.acme.sample
  • creating a new GLOBUS_LOCATION/etc/org.acme.sample and copying there the service configuration files
  • copying the compiled code in the GLOBUS_LOCATION/lib folder
  • creating a new GLOBUS_LOCATION//share/schema/SampleService and copying there all the service interfaces (WSDLs)

To undeploy the SampleService, just type:

gcore-undeploy-service org.acme.sample

Logging it

A Test Client