Skip to main content

Parts, Pipelines, and Declarative programming: An architectural model

Posted by satyak on February 6, 2004 at 8:33 AM PST

Introduction

In this article I want to explain an architectural model that I use often called "parts and pipelines" using an example. This model is especially useful in declrative programming. This article will start by introducing an example where a web page will update some content on the server side. The quoted example will be analyzed to explain parts, pipelines, and declarative programming. To further understand the details of a Part the source code for a "HelloWorldPart" is presented followed by the source code of 2 real world parts.

"Part" introduced

I often do the following in Aspire/J2EE when I submit a form from a web browser. Let me say the url for this form submission look like http://host/webapp/servlet/updateservlet?request_name=some-request&arg1=10&arg2=14. For such a URL my config file looks like:

(1) request.some-request.classname=com.mucomp.mypkg.MyPart

(2) request.some-request.redirectURL=\
/webapp/servlet/DisplayServlet?url=abc&arg1=19&arg2=45

(3) request.some-request.failureRedirectURL=\
/webapp/servlet/DisplayServlet?url=abc&arg1=19&arg2=45&error=general

(4) request.some-request.failureRedirectURL.CONDITION1=\
/webapp/servlet/DisplayServlet?url=abc&arg1=19&arg2=45&error=specific-condition

In line 1 I have specified a java class as a target for the incoming request. This class will be rensposible for executing some java logic. If that logic is successful then the user will be redirected to the specified "redirectURL" in line 2. If the class throws an exception with "CONDITIONS1" then the user will be redirected to specified "failureRedirectURL.CONDITION1" in line 4. For all other failures the user will be redirected to "failureRedirectURL" in line 3.

The focus of this article is the "MyPart" class. The signature of MyPart class is such that it has one well known method that accepts a Map as an input and returns any type of object. The system is clever enough to instantiate MyPart either as a singleton or a multi-instance. Such a decissioni is determined by what interfaces are implemented by the MyPart class.

At this stage of the game, MyPart looks very similar to an action class in model 2 architecture or even resembles the idea of a servlet. But this idea of a "Part" that is declaratively specified becomes pungent when the Part becomes reusable. Let me give an example of this by rewriting line 1 with a reusable part.

A Reusable Part

request.some-request.classname=com.mycomp.mypkg.SQLPart
request.some-request.db=MyDataBase
request.some-request.query_type=update
request.some-request.stmt=\
insert into table1 (column1, column2) values ({arg1},{arg2})

In this example I am using a reusable part called "SQLPart". This part is reusable because it can execute any general purpose SQL statement based on some inputs in the config file and some inputs in the input request. Because this part is reusable whereever SQL needs to be executed, such a part is known to exhibit what is known in the industry as a "Horizontal" architecture. Meaning, a general purpose component or part. So the Part architecture allows you to write these reusable horizontal parts.

Part pipelines introduced

We can extend the utility of this architecture by introducing some specialized Parts called Pipeline parts. These are essentially meta parts that allow other parts to run in a compositional pattern. This should become clear by looking at the following example.

request.some-request.classname=com.mucomp.mypkg.PipelinePart
request.some-request.request.1=part1
request.some-request.request.2=part2

The target of our request now is designated as "PipelinePart". As far as the system is concerned, there is no difference between a regular SQLPart and the PipelinePart. But the inputs to the PipelinePart are different. It takes the names of other parts to run in a sequence. So in the example "part1" and "part2" are names of two parts that will be run in a sequence. But this requires that we define what "part1" and "part2" are. Let me go ahead and define these two.

request.part1.classname=com.mycomp.mypkg.SQLPart
request.part1.db=databasename
request.part1.query_type=update
request.part1.stmt=(insert into table1)

request.part2.classname=com.mycomp.mypkg.MailPart
request.part2.mailserver=your-mail-server
request.part2.subject=some text {arg1} other text
request.part2.body=some body {arg2} etc

This idea of a part pipeline allows you to string together multiple tasks (or parts) so that the main task is a collection of subtasks. Depending on the sophistication of the pipeline implementation you can implement transactional properties to these pipelines. The base implementation in Aspire/J2EE allows for single data base transactional commits so that a very common case is covered. If you were to use the full blown JTA and write your own PipelinePart you may be able to accomplish the same transactional benefits of a traditional session bean.

How to write your own part

Here is the general idea of writing a part followed by an example. You would start out by extending an abstract class called "AFactoryPart". This abstract class allows you to participate in the invocation process and deal with the return values in a general sense. This abstract class has only one method that you need to implement. Let us take a look at an example part.

public class HelloWorldPart extends AFactoryPart
{
//Contract from the base class
Object executeRequestForPart(String requestName, Map inputArgs)
throws RequestExecutionException
{
//requestName points to the line in the config file
//that invoked this Part. This allows you read
//additional configuration

//inputArgs are the input hashtable that is coming in

//update a database
//return a message
return "Hello World";
}
}

Being an abstract class, the "AFactoryPart" does a couple of things for you. It ensures that what Aspire/J2EE passes to your part is an object of type "Map". The second interesting thing it does is this: It places the "Hello World" message on the incoming hash table. This makes the return value avaialbe to the parts that are downstream in the pipeline and also eventually to the caller based on some conventions. Well, if it places the "Hello world" in the input Map, what "key" is it using? This is where the configuration kicks in. Here is how you would have used this part in the pipeline.

request.helloworld.classname=HelloWorldPart
request.helloworld.resultName=arg5

In the above sample code, the property "resultName" is a reserved word for the abstract part "AFactoryPart". So, this is what "AFactoryPart" does with the above definition. It invokes the execute method and gets a result as an object. It places this object in the Map using the key name identified by "resultName". In the example above at the end of "HelloWorld" execution you will see an entry in the Map called (arg5:Hello World).

Implications of the declrative model

This kind of programming where the programmers compose subtasks declaratively can be said to be declarative programming. There are positives and negatives to this approach. I have over 2 to 3 years experience developing websites using this model. I have collected some insights into the limits of declarative programming. I will document the negatives and potential solutions in a future document.

In here for now I will focus on the benefits I was able to derive from the model. For starters the model opens the doors for less skilled Java programmers as well as non-java programmers. For instance when you use an SQLPart, you are not getting into the details of the JDBC on which this part is based. This is similar to using a library. But making the library look like a "part" is like taking a gem out of a stack of hay and presenting it for the usage. It also provides a clear boundary for experienced programmers to come up with reusable horizontal parts. As these parts would be tested on multiple apps, the application tend to be lot more robust right from the start.

The model is highly suited to web sites with entry level and medium level complexity. For large scale web sites with 100s of pages, the problem is the multiplicity of declarations. One way to solve this is using a GUI to manage this process.

A real "if" part example taken from Aspire source code

The above example gave a taste of how to write a basic part in Aspire. Let me go ahead and list here an actual Part borrowed from Aspire source code.

package com.ai.parts;

import com.ai.application.interfaces.*;
import com.ai.application.utils.AppObjects;
import com.ai.aspire.utils.TransformUtils;
import com.ai.common.*;
import java.io.*;
import java.util.Hashtable;
import java.util.Map;

/**
* Additional property file arguments
* 1. expression=
* 2. if-request
* 3. else-request
*
* Output
* 1.resultName: Absolute filename to which the contents are exported
*
*/

public class IfPart extends AFactoryPart
{
    protected Object executeRequestForPart(String requestName, Map inArgs)
        throws RequestExecutionException
    {
        PrintWriter w = null;
        String aspireFilename = null;
        try
        {
        //mandatory args
            String expression = AppObjects.getValue(requestName + ".expression");
            String ifRequestName = AppObjects.getValue(requestName + ".if");
            String elseRequestName = AppObjects.getValue(requestName + ".else",null);

            boolean r
            = ExpressionEvaluationUtils.evaluateBooleanExpressionUsingDictionary
                                   (expression,new MapDictionary(inArgs));
            if (r == true)
            {
               return AppObjects.getObject(ifRequestName,inArgs);
            }
            else
            {
               if (elseRequestName == null)
                  return new Boolean(true);
               return AppObjects.getObject(elseRequestName,inArgs);
            }
        }
        catch(ConfigException x)
        {
            throw new RequestExecutionException
         ("Error: ConfigException. See the embedded exception for details", x);
        }
        catch(CommonException x)
        {
            throw new RequestExecutionException
         ("Error: Expression evaluation error. See the embedded exception for details", x);
        }
    }
}

This example shows how to read additional arguments from the config file.

Another example: URLExporter

package com.ai.parts;

import com.ai.application.interfaces.*;
import com.ai.application.utils.AppObjects;
import com.ai.aspire.utils.TransformUtils;
import com.ai.common.*;
import java.io.*;
import java.util.Hashtable;
import java.util.Map;
/**
* Takes an Aspire URL name and exports the contents of that URL output to the specified filename
*
* Additional property file arguments
* 1. aspireUrlName=
* 2. outputFilename=
*
* Output
* 1.resultName: Absolute filename to which the contents are exported
*
*/

public class URLExporterPart extends AFactoryPart
{
    protected Object executeRequestForPart(String requestName, Map inArgs)
        throws RequestExecutionException
    {
        PrintWriter w = null;
        String aspireFilename = null;
        try
        {
//mandatory args
            String aspireURLName = AppObjects.getValue(requestName + ".aspireUrlName");
            String filename = AppObjects.getValue(requestName + ".outputFilename");

            String subFilename = SubstitutorUtils.generalSubstitute(filename, inArgs);
            aspireFilename = FileUtils.translateFileName(subFilename);
            w = new PrintWriter(new FileOutputStream(aspireFilename));
            TransformUtils.transform(aspireURLName, (Hashtable)inArgs, w);
            String s = filename;
            return s;
        }
        catch(ConfigException x)
        {
            throw new RequestExecutionException
        ("Error: ConfigException. See the embedded exception for details", x);
        }
        catch(IOException x)
        {
            throw new RequestExecutionException
        ("Error: Error wrting to file:".concat(String.valueOf(String.valueOf(aspireFilename))), x);
        }
        catch(TransformException x)
        {
            throw new RequestExecutionException("Error: TransformException", x);
        }
        finally
        {
            if(w != null)
                w.close();
        }
    }
}

Conclusion

This document is presented with the idea of introducing the concepts. As a result the classnames shown in the examples are fictitious except for the two real parts whose source code I have provided. This article also has demonstrated how easy it is to write simple parts. Nevertheless if your part is responsible for updating data bases it gets a bit tricky. I will post another document very soon describing the process. The main issue there is that of obtaining a connection to be passed to the down stream parts so that they all get commited in the same connections. This is automatic if you were to use Aspire's part pipeline. But if your part is invoked outside of this pipeline you need to be aware of some conneciton details. Again, this will be documented in a separate document.

Further reading

How to do data access in Aspire/J2EE.

How to do updates in Aspire/J2EE.

Aspire documentation pages

Aspire home page

Disclaimer

The industry may have similar models. The idea is presented here not to negate any of those models but merely as one of the models for the architecturally curious mind.

Related Topics >>