Chapter 24. How to write an activity

24.1. Tutorial examples
24.2. Notes on writing a server-side activity
24.2.1. Some definitions
24.2.2. Choosing a base class
24.2.3. Concrete ActivityInput classes
24.2.4. Exception handling and activity termination
24.2.5. Interacting with resources
24.2.6. Interacting with sessions
24.2.7. Obtaining the security context
24.2.8. Executing tasks in the activity
24.2.9. Creating new data resources - resource factory activities
24.2.10. Managing resources
24.2.11. Configuring an activity
24.3. Notes on writing client toolkit activities
24.3.1. Base classes
24.3.2. Inputs
24.3.3. Multiple inputs
24.3.4. Outputs
24.3.5. Multiple outputs
24.3.6. How does an activity get access to the data in a request status?
24.3.7. Validation of inputs and outputs
24.4. Using activities

The ability to develop new activities for OGSA-DAI is an important extensibility point. Many projects will find themselves developing a few application specific activities to complement those shipped with OGSA-DAI. This chapter describes how to write new activities. We begin with some a collection of examples and advise that readers planning to develop their own activities work through these example first. After the examples we include some notes and guidelines relating to activity development that may go beyond the level of detail provided in the examples.

24.1. Tutorial examples

This section presents a number of example activities. It is recommended that activitiy developers work through some of these examples before writing their own activities.

  1. Hello World activity A basic activity that has no inputs an one output.

  2. Hello Name activity. A basic activity that has one input an one output.

  3. List Reduction activity. An activity that processes a values in a list and reduces then to a single output value.

  4. List Processing activity. An activity that processes a list provided as an input and outputs a list.

  5. Tuple Processing activity. An activity that processes tuples as obtained from an SQLQuery activity.

[Tip]Tip

The list of activity examples here will hopefully continue to grow. Please check the on-line documentation for the latest examples.

24.2. Notes on writing a server-side activity

This section contains useful notes and information relating to the development of server side activities. It is indented to provide additional information that is possibly not covered in detail in the tutorial examples.

24.2.1. Some definitions

This section presents some definitions that are useful to understand before discussing activities in more detail.

Activity inputs receive streams of blocks that are simply Java objects. Special list begin and list end markers are often inserted into the data streams to group several blocks together. The term logical value is used to refer to either:

  • Any single block that is not a list marker. For example, a single instance of java.lang.Integer, java.lang.String or uk.org.ogsadai.tuple.Tuple.

  • A sequence of blocks that begins with a list start marker and ends with matched list end marker. This sequence of blocks may itself contain other nested matched pairs of list begin and list end markers.

An activity's inputs are said to be matched if the activity expects each input to receive the same number of logical values. An example of a matched activity is ObtainFromFTP. This activity has three inputs: filename, host and the optional passiveMode. The activity expects the same number of logical values to be passed to the filename and host inputs and also to the passiveMode input if it is specified. The activities released with OGSA-DAI have been carefully designed to ensure they are matched.

An activity is said to be iterative if the activity performs the same processing repeatedly over different sets of inputs. An example of an iterative activity is ObtainFromFTP. This activity repeatedly reads one value from the filename input and one value from the host inputs, obtains the file from the FTP server specified by the host input and writes the data to the activity's output. The activity will continue to repeat this operation until it receives no more input data. The activities released with OGSA-DAI have been carefully designed to ensure they are iterative.

An activity input is said to be a multiple input if the activity can have multiple occurrences of that input. For example, consider the ListConcatenate activity. This activity concatenates multiple lists into a single list. The activity has a multiple input called input and an output called output. Figure 24.1, “Activity with multiple inputs of the same name.” shows an instance of this activity with three occurrences of the input input. The activity reads data from each of these inputs much like any other input. The ListContatenate activity processes each input in turn reading a list from each and writing the values to the output. The use of a multiple input allows the activity to concatenate as many inputs as the client specifies in the workflow.

Activity with multiple inputs.
ListContantenate activity with multiple input inputs.

Figure 24.1. Activity with multiple inputs of the same name.


An activity output is said to be a multiple output if the activity can have multiple occurrences of that output. For example, consider the TupleSplit activity. This activity reads a tuple list and distributes the tuples in the list evenly between a number of outputs. The activity has an input called data and an multiple output called result. Figure 24.2, “Activity with multiple outputs of the same name.” shows an instance of this activity with multiple occurrences of the result output. The activity writes the tuples it receives as input to the outputs in a round robin manner. The use of a multiple output allows the activity to distribute the tuples over as much outputs as the client specifies in the workflow.

Activity with multiple outputs.
TupleSplit activity with multiple result outputs.

Figure 24.2. Activity with multiple outputs of the same name.


24.2.2. Choosing a base class

A server side activity implementation class must implement the uk.org.ogsadai.activity.Activity interface. Typically this is done by extending one of four abstract base classes shipped with OGSA-DAI. These classes are:

  • uk.org.ogsadai.activity.MatchedIterativeActivity

  • uk.org.ogsadai.activity.MatchedIterativeMultipleInputActivity

  • uk.org.ogsadai.activity.IterativeActivity

  • uk.org.ogsadai.activity.ActivityBase

[Note]Note

The first three abstract classes are extended the ActivityBase class which is responsible for providing access to input and output pipes, activity contracts and simple validation functionality. They only aim at providing a more convenient way to fragment and manipulate the activity processing.

The MatchedIterativeActivity base class is the most commonly used. It should be used in most cases when the activity is iterative and has matched inputs. This base class handles much of the functionality general to these types of activities including the detection and error handling of unmatched input data.

If an activity is both iterative and has matched inputs but at least one of these inputs is a multiple input then the activity should extend the MatchedIterativeMultipleInputActivity base class. This class is very similar to MatchedIterativeActivity but includes extra functionality for multiple inputs.

If the activity is iterative but has only a single input and you do not wish the base class to perform any error detection or processing on that input then the activity should extend IterativeActivity. An example of this is the Tee activity which is most easily implemented by iterating and processing each input object in turn rather than dealing with logical values as it would be required to do it extended MatchedIterativeActivity.

If an activity is not iterative or it has no inputs then none of the standard patterns apply and hence the activity should extend the ActivityBase class. An example of this is the GetAvailableTables activity that has no inputs.

24.2.3. Concrete ActivityInput classes

The MatchedIterativeActivity and MatchedIterativeMultipleInputActivity classes make use of the ActivityInput interface to specify the properties of an activity's inputs. There are a number of very useful classes that implement this interface. These are listed here:

  • TypedActivityInput: for inputs that contain Java objects of a specified type. The constructor takes the input name and the expected type:

    new TypedActivityInput("inputName", String.class)

    The object passed to the processIteration method for the input will be of the specified type.

  • TypedOptionalActivityInput: for optional inputs that, if provided, contain Java objects of a specified type. If the input is not provided in the workflow the processIteration method will be passed the specified default value. The constructor takes the input name, the expected type and the default value:

    new TypedOptionalActivityInput("inputName", String.class, "defaultValue")

    The object passed to the processIteration method for the input will be of the specified type.
  • TypedListActivityInput: for inputs that contain lists of Java objects of a specified type. The constructor takes the input name and the type of objects in the list:

    new TypedListActivityInput("inputName", String.class)

    The object passed to the processIteration method for the input will be of type uk.org.ogsadai.activity.io.ListIterator. This iterator can be used to obtain objects of the specified type.

  • TupleListActivityInput: for inputs that contain tuple lists. The constructor takes the input name:

    new TupleListActivityInput("inputName")

    The object passed to the processIteration method for the input will be of type uk.org.ogsadai.activity.io.TupleListIterator.

  • TypedOptionalListActivityInput: for optional inputs that, if provided, contain lists of Java objects of a specified type. The constructor takes the input name, the type of objects in the list and a default list:

    new TypesOptionalListActivityInput("inputName", String.class, new ArrayList())

    The object passed to the processIteration method for the input will be of type uk.org.ogsadai.activity.io.ListIterator. This iterator can be used to obtain objects of the specified type.

  • ReaderActivityInput: for activities that wish to process data via a Java Reader object. Data streams that can be processed via a Reader are:

    • Blob

    • Clob

    • [ byte[] ]

    • [ char[] ]

    For Blob and [ byte[] ] that deal with binary data the data is converted to character data using the UTF-8 encoding. The constructor takes the input name:

    new ReaderActivityInput("inputName")

    The object passed to the processIteration method for the input will be of type Reader. If an error occurs in an activity that uses this input then it is important not to close the Reader. Closing the Reader may close the BlockReader it wraps. If an error has occured and the activity throws the appropriate exception then the activity framework will close all the inputs and outputs in such a way that the error is correctly propagated through the workflow.

  • InputStreamActivityInput: for activities that wish to process data via a Java InputStream object. Data streams that can be processed via a Reader are:

    • Blob

    • Clob

    • [ byte[] ]

    • [ char[] ]

    For Clob and [ char[] ] that deal with character data the data is converted to binary data using the UTF-8 encoding. The constructor takes the input name:

    new InputStreamActivityInput("inputName")

    The object passed to the processIteration method for the input will be of type InputStream. If an error occurs in an activity that uses this input then it is important not to close the InputStream. Closing the InputStream may close the BlockReader it wraps. If an error has occured and the activity throws the appropriate exception then the activity framework will close all the inputs and outputs in such a way that the error is correctly propagated through the workflow.

All these classes are in the uk.org.ogsadai.activity.io namespace.

24.2.4. Exception handling and activity termination

When processing activities can throw three types of exception. These are:

  • ActivityUserException: thrown when an error occurs that is due to the data received by the activity. For example, the data is the wrong type or an SQL query is badly formed.

  • ActivityProcessingException: thrown when an error occurs during processing that was not due to the activity input. For example, if the activity cannot connect a required database.

  • ActivityTerminatedException: thrown when an activity has been terminated.

The BlockReader and BlockWriter interfaces can throw exceptions when an activity reads and writes data. These exceptions are:

  • PipeClosedException

  • PipeIOException

  • PipeTerminatedException

The PipeClosedException exception is thrown by the write method of BlockReader if the consuming activity at the other end of the pipe has closed the pipe indicating the activity does not wish to receive any more data. In this situations most activities simply stop producing the data.

The PipeIOException exception is thrown if there is an internal problem during reading from, or writing to, a pipe. Typically this exception is simply mapped to an ActivityPipeProcessingException exception as shown in the example activities.

The PipeTerminatedException exception is thrown if the activity has been terminated. Activities are typically notified that the are to terminate by receiving this exception. The activity should stop processing as soon as possible and throw the ActivityTerminatedException exception.

24.2.5. Interacting with resources

If an activity to interact with an OGSA-DAI resource then it must implement the uk.org.ogsadai.activity.extension.ResourceActivity activity extension interface.

A client using the activity in a workflow will specify the ID of the resource with which the activity is to interact. On encountering any activity that implements ResourceActivity the activity framework will provide the activity with the resource via the ResourceActivity interface's setTargetResourceAccessor(ResourceAccessor resourceAccessor) method.

This provides the activity with an resource accessor corresponding to the resource at which the activity is targeted. A resource accessor is a resource configured with a security context related to the client who submitted the original request. This is one way in which OGSA-DAI ensures that clients can only interact with resources in the ways that OGSA-DAI administrators allow (see Section 30.6.3, “Creating resource accessors” for information on resource accessors from a data resource developers perspective).

Each OGSA-DAI data resource implementation class has an associated sub-class of resource accessor. For example:

  • uk.org.ogsadai.resource.dataresource.file.FileDataResource has uk.org.ogsadai.resource.dataresource.file.FileAccessProvider
  • uk.org.ogsadai.resource.dataresource.group.ResourceGroupDataResource has uk.org.ogsadai.resource.dataresource.group.ResourceGroupProvider
  • uk.org.ogsadai.resource.dataresource.jdbc.JDBCDataResource has uk.org.ogsadai.resource.dataresource.jdbc.JDBCConnectionProvider
  • uk.org.ogsadai.resource.dataresource.xmldb.XMLDBDataResource has uk.org.ogsadai.resource.dataresource.xmldb.XMLDBCollectionProvider

Activities that implement ResourceActivity must also support a method getTargetResourceAccessorClass(). This allows them to inform the activity framework of the types of resource with which they may be used by providing the name of the resource accessor interface they expect. The activity framework can then validate that the resource specified by a client is compatible with the activity and so, for example, allow SQLQuery to be targeted at a relational resource but not at an XMLDB data resource, for example. Activities return the name of a sub-interface of ResourceAccessor corresponding to the resource accessor supported by the data resources with which they can interact.

As an example SQLQueryActivity implements the methods of ResourceActivity as follows:

public Class getTargetResourceAccessorClass()
{
    return JDBCConnectionProvider.class;
}

and

public void setTargetResourceAccessor(final ResourceAccessor targetResource)
{
    mResource = (JDBCConnectionProvider) targetResource;
}

24.2.6. Interacting with sessions

If the activity needs to read/write objects from session resources then the activity must implement the uk.org.ogsadai.activity.extension.SessionActivity activity extension interface.

On encountering any activity that implements the SessionActivity interface the activity framework will provide the activity with a reference to the session specified along with that activity in the request.

If the specified session exists then the activity framework invokes the SessionActivity.setSession method that gives the activity the specified session resource. This method will be called before any of the processing methods are called. Typically the method will simply store the session resource in a local variable for use later. For example:

public void setSession(SessionResource session)
{
    mSession = session;
}

See uk.org.ogsadai.activity.delivery.ObtainFromSession.

24.2.7. Obtaining the security context

If the activity needs to access the security context then the activity must implement the uk.org.ogsadai.activity.extension.SecureActivity activity extension interface. This interface contains the setSecurityContext method that gives the activity the security context. This method will be called before any of the processing methods are called. Typically the method will simply store the security context in a local variable for use later. For example:

public void setSecurityContext(SecurityContext context)
{
    mSecurityContext = context;           
}

See uk.org.ogsadai.activity.delivery.DeliverToGFTP.

24.2.8. Executing tasks in the activity

When the activity is to execute callable tasks - e.g. sub-workflows the uk.org.ogsadai.activity.extension.TaskProcessingActivity interface needs to be implemented. In this case, the Task Processing Extension will have to be set by setting the task factory and task processing service, e.g.

public void setTaskProcessingExtension(
    final AuthorizingTaskFactory taskFactory, 
    final TaskProcessingService processingService)
{
	mTaskFactory = taskFactory;
    mTaskProcessingService = processingService;
}

See uk.org.ogsadai.activity.sql.SQLBag.

24.2.9. Creating new data resources - resource factory activities

Depending upon an application-specific requirement there might be a need or a desire to write an activity that can create application-specific data resources. This then allows clients to request the creation of these resources in their workflows.

OGSA-DAI ships with example resource creation activities including those to create data sources and data sinks (CreateDataSource and CreateDataSink) and, of more direct relevance to this section, CreateResourceGroup which creates OGSA-DAI resource group data resources.

When the activity needs to create new data resources the following steps need to be done.

24.2.9.1. Write a data resource

First you need to write a data resource implementation class if you have not already done so or do not have this implementation class already (e.g. as provided by a third party).

See Chapter 30, How to write a data resource for detailed instructions on writing a data resource.

For examples you can look at our resource group source code in

src/core/resource/uk/org/ogsadai/resource/dataresource/group/

24.2.9.2. Deploy a data resource template

You will then need to deploy a data resource template. A data resource template is the same as a data resource configuration file except it resides in a different part of the server configuration files. Section 16.1.5, “Deploying a resource template” describes how to deploy a resource template with the file format being described in Section 17.2.5, “Resource template files”.

The name you give your template file will be used in your resource creation activity so take note of it e.g.

org.some.com.MY_RESOURCE_TEMPLATE

For an example you can look at our resource group template in

deploy/server-config/resourceTemplates/uk.org.ogsadai.RESOURCE_GROUP_TEMPLATE 

24.2.9.3. Write the resource factory activity

You now write the activity that creates the resource. You will find it useful if your activity implements both the following extension interfaces:

uk.org.ogsadai.activity.extension.ResourceFactoryActivity
uk.org.ogsadai.activity.extension.ResourceManagerActivity

These tell the activity framework that this activity needs access to the resource manager (uk.org.ogsadai.resource.ResourceManager) - which manages the resources currently within OGSA-DAI - and a resource factory (uk.org.ogsadai.resource.ResourceFactory) - a set of utilities to assist in resource creation. For example:

public void setResourceManager(ResourceManager resourceManager)
{
    mResourceManager = resourceManager;        
}

public void setResourceFactory(ResourceFactory resourceFactory)
{
    mResourceFactory = resourceFactory;        
}

A resource manager can be useful for auto-generating a unique ID for your new resource if you want your activity to support this option. Otherwise the client must provide the resource ID as an input to your activity.

Your activity needs to create an instance of your data resource using your template to configure it. For example our implementation of CreateResourceGroup (uk.org.ogsadai.activity.management.CreateResourceGroup) does:

ResourceGroupDataResource resourceGroup = 
  (ResourceGroupDataResource)mResourceFactory.createDataResource(OGSADAIConstants.RESOURCE_GROUP_TEMPLATE); 

So for your activity you'd do:

MyDataResource myResource = 
  (MyDataResource)mResourceFactory.createDataResource(new 
      uk.org.ogsadai.resource.ResourceID("org.some.com.MY_RESOURCE_TEMPLATE"); 

You then need to perform resource-specific setting of the state of the resource. For example for CreateResourceGroup we have:

// Get the resource state.
ResourceGroupDataResourceState state = 
   resourceGroup.getResourceGroupDataResourceState(); 

// Set the ID of the new ResourceGroup...

state.getDataResourceState().setResourceID(groupResourceID); 

// ... and adds the member resources... 

String resourceID = null; 
while ((resourceID = (String)rIDs.nextValue()) != null) 
{ 
     state.addResourceID(new ResourceID(resourceID)); 
} 

You can then use the resource factory to add the new resource to the resource manager. The configuration for the new resource is then persisted (e.g. in a file named after the ID of your new resource in the resource configuration files directory). For example for CreateResourceGroup we have:

mResourceFactory.addResource(groupResourceID, resourceGroup); 

A simple example of the code required to create a resource in the processIteration method for your activity might be as follows.

// Activity input - ID for the new resource.
public static final String INPUT_RESOURCE_ID = "resourceID";
// Activity input - ID for some input from the client used to
// configure the resource. 
public static final String INPUT_CONFIG_ID = "someConfig";
// Activity output - ID for the new resource.
public static final String OUTPUT = "output";
// Resource template ID.
public static final ResourceID TEMPLATE_ID =
    new ResourceID("org.some.com.MY_RESOURCE_TEMPLATE"); 

protected ActivityInput[] getIterationInputs()
{
    // Declare our two inputs - the resource ID and configuration
    // parameter.
    return new ActivityInput[] 
    {
        // Input 0
        new TypedOptionalActivityInput(INPUT_RESOURCE_ID, String.class),
        // Input 1
        new TypedActivityInput(INPUT_CONFIG_ID, String.class)
    };
}

protected void preprocess() 
    throws ActivityUserException, ActivityProcessingException, ActivityTerminatedException
{
    validateOutput(OUTPUT);
    mOutput = getOutput();
}

protected void processIteration(Object[] iterationData)
    throws ActivityUserException, ActivityProcessingException, ActivityTerminatedException
{
    // Get the configuration value.
    String someConfig = (String)iterationData[1];
    ResourceID resourceID;
    if (iterationData[0] == null)
    {
        // No value was given for the optional resource ID input
        // so ask the resurce manager to create a unique resource ID.
        resourceID = mResourceManager.createUniqueID();
    }
    else
    {
        // Use the resource ID given by the client.
        resourceID =  new ResourceID((String)iterationData[0]);
    }
    try
    {  
        // Create a new instance of MyDataResource using a 
        // resource template.
        MyDataResource myDataResource
            (MyDataResource)mResourceFactory.createDataResource(TEMPLATE_ID);

        // Get its state and set its ID,
        MyDataResourceState state = myDataResource.getMyDataResourceState();
        state.getDataResourceState().setResourceID(resourceID);
        // Set the configuration.
        state.setSomeConfig(someConfig);

        // Add the resource to the resource manager using the
        // resource factory.
        mResourceFactory.addResource(resourceID, myDataResource);
        // Output the new resource ID.
        mOutput.write(resourceID.toString());
    }
    catch (PipeClosedException e)
    {
        // No more input wanted
        iterativeStageComplete();
    }
    catch (PipeTerminatedException e)
    {
        throw new ActivityTerminatedException();
    }
    catch (PipeIOException e)
    {
        throw new ActivityPipeProcessingException(e);
    }
    catch (ResourceIDAlreadyAssignedException e)
    {
        // Wrap the resource ID already assigned error as a user exception
        throw new ActivityUserException(e);
    }
    catch (Exception e)
    {
        throw new ActivityProcessingException(e);
    }
}

There is no requirement to use a resource template with a resource creation activity. The alternative is to populate the ResourceState for your data resource within the activity. However, in terms of specifying the activities supported by your new data resource, you have to publish what you expect the IDs of activities available on the OGSA-DAI server to be which makes your activity less portable.

See uk.org.ogsadai.activity.management.CreateResourceGroupActivity.

24.2.10. Managing resources

When the activity needs to interact with the resource manager then the uk.org.ogsadai.activity.extension.ResourceManagerActivity can be implemented. The resource manager needs to be set:

public void setResourceManager(ResourceManager resourceManager)
{
	mResourceManager = resourceManager;        
}

Activities that might want to interact with a resource manager include those that create new data resources (e.g. CreateResourceGroup), destroy existing resources or query the state of the OGSA-DAI server.

See uk.org.ogsadai.activity.management.CreateResourceGroupActivity.

24.2.11. Configuring an activity

If an activity has server-side configuration properties then the uk.org.ogsadai.activity.extension.ConfigurableActivity interface has to be implemented. This allows the configuration from the OGSA-DAI persistence and configuration components to be given to the activity via a method:

public void configureActivity(KeyValueProperties properties);

An example of this interface is uk.org.ogsadai.activity.deliver.DeliverToSMTP activity. In this activity, the SMTP server that the activity will be using should be set by the OGSA-DAI administrator and therefore it will be a configuration parameter of the activity. More details about how an activity is configured can be found in Section 17.2.2, “Activity specification file”.

So if the configuration parameter is dai.smtp.server then the activity can define a key for this property.

private static final Key SMTP_SERVER = new Key("dai.smtp.server"); 

and the method configureActivity(KeyValueProperties properties) will be implemented like:

public void configureActivity(KeyValueProperties properties)
{
	mServerName = (String)properties.get(SMTP_SERVER);        
}

where server name is the actual value of key SMTP_SERVER.

Other possible interfaces can be found under uk.org.ogsadai.activity.extension.

24.3. Notes on writing client toolkit activities

After writing a server-side activity you should typically write a client-side activity class that allows Java applications to add the activity to workflows. Note that the client-side activity implementation does not do any of the work of the activity - this is all done in the server-side activity implementation. The client-side activity implementation simply allows client developers to specify inputs and access outputs.

This section provides some notes and guidelines on how to write client-side activity classes.

24.3.1. Base classes

There are two types of activities in OGSA-DAI, those that target a resource and those that do not. If the activity targets a resource then the client-side activity class must extend BaseResourceActivity. If the activity does not target a resource then the client-side activity class must extend BaseActivity. Both of these classes are in namespace uk.org.ogsadai.client.toolkit.activity.

24.3.2. Inputs

For each non-multiple input that an activity has the client-side activity class should provide a connectXInput method and, depending on the type of objects the input receives, zero or more addX methods, where X is the name of the input.

24.3.2.1. Connect method

The connectXInput method should have the following signature:

public void connectXInput(ActivityOutput output);

The method is typically implemented using as:

public void connectXInput(ActivityOutput output)
{
    mXInput.connect(output);
}

where mXInput where is a private member variable declared as:

private ActivityInput mXInput;

and instantiated within the constructor with code such as:

mXInput = new SimpleActivityInput("X");

or

mXInput = new SimpleActivityInput("X", SimpleActivityOutput.OPTIONAL);

if the input is optional.

24.3.2.2. Add method

If the input expects values that of the primative types supported by OGSA-DAI in client/server communications (types String, char[], byte[], Boolean, Date or a number) the the client-side activity class should provide at least one add method to pass literal values to the activity. If the input is called X then this method should be called addX.

The following classification holds:

  1. if the input is of primitive type (e.g. String) then we provide the addX method and map the input object to the appropriate subclass of DataValue. For example:

    public void addX(final String x)
    {
        mXInput.add(new StringData(x));
    }
  2. if the input is a list of objects of primitive types then we provide two methods:

    1. One that takes an Iterator of objects of the expected type as a parameter and

    2. another one that takes an array of objects of the expected type.

    [Note]Note

    The first one should check that the members of the Iterator are of the expected type. If not then an IllegalArgumentException should be thrown.

    Both of them should add the list markers before and and after the input objects. For example:

    public void addX(final Iterator xValues)
    {
        mXInput.add(ListBegin.VALUE);
        while ( xValues.hasNext() )
        {
          	mXInput.add(new StringData((String)xValues.next()));
        }
        mXInput.add(ListEnd.VALUE);
    }

    and

    public void addX(final String[] xValues)
    {
        mXInput.add(ListBegin.VALUE);
        for ( int i=0; i< xValues.length; i++ )
        {
        	mXInput.add(new StringData(xValues[i]));
        }
        mXInput.add(ListEnd.VALUE);
    }

  3. If the input is a list of char[] then we need to provide an addX method that takes a Reader as input and reads chunks of char[] and adds them to input. For example:

    public void addX(final Reader xReader) throws IOException
    {
        mXInput.add(ListBegin.VALUE);
        char[] chars = new chars[BLOCK_SIZE];
        int charsRead = 0;
        while ((charsRead = xReader.read(chars, 0, BLOCK_SIZE)) != -1)
        {
            if (charsRead != BLOCK_SIZE)
            {
                chars[] charsTmp = new char[charsRead];
                System.arraycopy(chars, 0, charsTmp, 0, charsRead);
                mXInput.add(new CharData(charsTmp));
            }
            else
            {               
                mXInput.add(new CharData(chars));
                chars = new char[BLOCK_SIZE];
            }
        }       
        mXInput.add(ListEnd.VALUE);
    
    }
  4. If the input is a list of byte[] then we need to provide an addX method that takes an InputStream as input and reads chunks of byte[] and adds them to the input. For example,

    public void addX(final InputStream xStream) throws IOException
    {
        mXInput.add(ListBegin.VALUE);
        byte[] bytes = new byte[BLOCK_SIZE];
        int bytesRead = 0;
        while ((bytesRead = xStream.read(bytes, 0, BLOCK_SIZE)) != -1)
        {
            if (bytesRead != BLOCK_SIZE)
            {
                byte[] bytesTmp = new byte[bytesRead];
                System.arraycopy(bytes, 0, bytesTmp, 0, bytesRead);
                mXInput.add(new BinaryData(bytesTmp));
            }
            else
            {               
                mXInput.add(new BinaryData(bytes));
                bytes = new byte[BLOCK_SIZE];
            }
        }       
        mXInput.add(ListEnd.VALUE);
    }

24.3.3. Multiple inputs

If an activity supports multiple inputs with the same name (activity input with multiple occurences) then we consider that every activity input is a group of SingleActivityInput objects. By default an activity input will consist of one SingleActivityInput. Therefore, in the case of multiple inputs we need to specify the number of SingleActivityInput objects. To faciliatate this we need:

  1. A method called setNumberOfXInputs where X is the name of the input. The method allow application developers to specify how may occurrences of the input there should be. For example:

    public void setNumberOfXInputs(int count)
    {
        mXInput.setNumberOfInputs(count);
    }
  2. All addX methods must now specify to which input occurence the literal is to be added. For example,

    public void addX(int index,final String xValue)
    {
        mXInput.add(index,new StringData(xValue));
    }

    The rules about addX methods explained above apply in this case as well, with the only difference being that an extra index argument is needed and corresponding adjustment on the body of the method.

  3. Similarly for the connectXInput method we need the additional index arguement:

    public void connectXInput(int index, SingleActivityOutput output)
    {
        mXInput.connect(index,output);
    }

24.3.4. Outputs

For each output an activity has the client-side activity implementation should provide methods to access the output values if these output values are of a type that can be written to the request status ( String, char[], byte[], Boolean, Date, number objects as well as lists of any of these).

Typically the activity can output a stream of values not just one. Thus for each such output named X we provide two methods:

boolean hasNextX();

and

Y nextXXX();

where Y is the type of the objects corresponding to the output.

The following classification applies to the nextX method:

  1. If the output type is, or can be mapped to, a Java primitive type then we provide a nextX method which returns objects of this type. Example:

    public String nextX()
    {
        mXOutput.getDataValueIterator().nextAsString();
    }
    

    where mXOutput is a private member variable of type ActivityOutputconstructed as:

    mXOutput = new SimpleActivityOutput("X");
    

  2. If the output is list of objects that are, or can be mapped to, primitive types then we provide a two methods: One that returns an DataIterator that can be used to access the objects in the list and another one that returns an array of these objects. Example:

    public DataIterator nextX()
        throws DataStreamErrorException, 
               UnexpectedDataValueException, 
               DataSourceUsageException
    {
        return new DataListIterator(mXOutput.getDataValueIterator(),String.class);
    }

    and

    public String[] nextResultAsArray()
        throws DataStreamErrorException, 
               UnexpectedDataValueException, 
               DataSourceUsageException
    {
       // Code that uses the DataIterator returned by nextX() to build an
       // array.
    }
    

  3. If the output is a list of char[] then the nextX method returns a Reader. Example:

    public Reader nextX()
    {
        return new DataValueReader(mXOutput.getDataValueIterator(),1);
    }
  4. If the output is a list of byte[] then the nextX method returns an InputStream. Example:

    public InputStream nextX()
    {
        return new DataValueInputStream(mXOutput.getDataValueIterator(),1);
    }

Of course and apart from the above rules, when giving the user access to the outputp, one has to have in mind that we need to provide the data to the user in a convinient way based on the type of the output data. On good example of this is the TupleToWebRowSetCharArrays activity that has an output called result that outputs list of char[]. This character data contains XML respresentations of tuple sets so the client-side activity provides the additional convenience method

ResultSet getResultAsResultSet()

that allows users to obtain the data in a convenient object.

24.3.5. Multiple outputs

If an activity supports multiple outputs with the same name (output with multiple occurences) then we consider that every activity output is a group of uk.org.ogsadai.client.toolkit.SingleActivityOutput objects. By default an activity output will consist of one SingleActivityOutput. Therefore in the case of multiple outputs we need to specify the number of SingleActivityOutput objects. To faciliatate this we need, for output X:

  1. A setNumberOfXOutputs method so that the number of occurences can be specified:

    public void setNumberOfXOutputs(int count)
    {
        mXOutput.setNumberOfOutputs(count);
    }
  2. The getXOutput method to have an index parameter to specify the occurence:

    public SingleActivityOutput getXOutput(int index)
    {
        return mXOutput.getSingleActivityOutputs()[index];
    }
    

  3. The hasNextX and nextX to have an additional index parameter to specify the output occurenece. For example:

    public boolean hasNextX(int index) 
        throws DataStreamErrorException, 
               UnexpectedDataValueException, 
    	   DataSourceUsageException,ArrayIndexOutOfBounds
    {
        return mXOutput.getDataValueIterator(index).hasNext();  
    }
    

    public int nextX(int index) 
        throws DataStreamErrorException,
               UnexpectedDataValueException,
               DataSourceUsageException,ArrayIndexOutOfBounds
    {
        return mXOutput.getDataValueIterator(index).nextAsInt();
    }

24.3.6. How does an activity get access to the data in a request status?

If an activity's output is connected to a deliverToRequestStatus activity so that the result comes back to the user then the data values written to that output can be obtained from the ActivityOutput object using the getDataValueIterator method. This is how we get hold of the data stream to implement the hasNextX and nextX methods described above.

If the activity (activity A) is not connected directly to the deliverToRequestStatus activity but there is another activity (activity B) in between that does not alter the data in a way that prevents activity A handling the output then activity A can still obtain the data that was written to he deliverToRequestStatus. To do this the application developer must tell activity A about the activity used to deliver the data to the request status. For example:

ActivityA activityA = new ActivityA();
ActivityB activityB = new ActivityB();
DeliverToRequestStatus deliverToRequestStatus = 
    new DeliverToRequestStatus();
activityB.connectInput(activityA.getOutput());
deliverToRequestStatus.connectInput(activityB.getOutput);
// Tell activity A's output how to get the data from the request status
activityA.getOutput().setDeliverToRequestStatusActivity(
    deliverToRequestStatus);

24.3.7. Validation of inputs and outputs

The base activity class will do most of the activity input and output validation. If however you need to do validation that requires looking at several inputs or outputs at once (for example, if an activity must have one of inputs A or B but never both) then this logic can be coded in the validateIOState method.

Very few activities need this type of validation so typically the method is empty:

protected void validateIOState() throws ActivityIOIllegalStateException
{
}

24.4. Using activities

Configuring OGSA-DAI to use your activities is the same as for OGSA-DAI's own activities. Activity deployment is described in Section 16.1.8, “Deploying an activity”.