Introduction

This is a walkthrough of a typical Surinam construction as an example which should give you enough of an understanding to get started building your own Hyper-Dynamic Software (HDS).

You've downloaded and unzipped the binary distribution; what happens next? This is a good place to start since it will introduce you to the primary objects in the framework which will give you some overall context, which should make the JavaDoc more meaningful. By the time you have skimmed through the examples, you will be ready to design and implement software with some level of confidence.

Remember, Surinam tries to err on the side of formality and specificity. Clarity and specificity over ad-hoc methodology makes it easier to understand for those of you who are new to the paradigm because you know what the roles are and where the responsibilities lie. While some will see this as requiring too much "up front" work, Surinam espouses that you should be doing this already and if you are not, then you are engaging in some sort of software non-deterministic "meandering" without a real plan and with unspecified goals. To that end, be extra careful about your assumptions, for some people much will be familiar and for others, the paradigm might be very different from what you are used to. Also, Surinam is a work in progress and not all of the larger vision has been implemented.

The example that follows is loosely based on example three of the project which takes the form of a Servlet that uses a single ServiceBlock object. Inside the block we will deploy an EntryPoint which we will use to make invocations inside the block (since normally we cannot see services deployed inside the block... we need a way in). We will use the Dependency Injection (DI) support to initialize the services with messages that will make it easier to identify the call chain as we build a variety of results at runtime.

Just to be clear, examples are for demonstration and instructional purposes only, it is assumed that developers understand techniques for properly accounting for issues of robustness and thread-safety. Some of these concerns are ignored in the example code to keep the code clean and readable. For example, we store the reference to the ServiceBlock in a servlet variable; blocks are thread safe but may not represent the best design for your application, especially as we pull internal references out of the block and hold them. The facts are that to broad a topic to be comfortable about making general statements about what might be appropriate in all cases.

Overview

The basics of the Surinam framework boil down to one primary object, the ServiceBlock (SB). You instantiate an implementation, deploy services to it and make invocations on it to do work. The lifecycle of the SB can be anything you want but normally it will have a lifecycle that is similar to that of your application. The management API of an SB is thread-safe to make it possible to perform runtime activities without shutting anything down.

Inside the SB, there are two primary structures that work cooperatively to provide most of the internal functionality of the SB, the ServiceDirectory and the BlueprintManager. Like any good SOA, you need a directory service to find registered services to invoke from outside the SB (known as Entry Points) and a way for services to find and consume other services inside the block. The reason for Entry Points is that service implementations have rather thankless jobs, they live and work in anonymity and total isolation inside of individual class loaders; also, all interaction happens via the Surinam Invocation Routing framework. The ServiceDirectory holds all the bindings of Provider Implementations to Service Contracts, which when connected up together, create a Service Graph. Note that for more programmatic power, the ServiceDirectory has an Administrative API and for more detail on Service Graphs and how they are reshaped, see the Surinam Manual.

And finally, for those of you just dipping your toes in the water, both the BlueprintManager and ServiceDirectories have the ability to deliver HTML representations of themselves via a method call, which is sometimes the best way to see what is going on under the covers. You gain access to these primary objects via the ServiceBlock's administrative API.

Initialization

We will leverage Servlet initialization as a good place to instantiate and configure our ServiceBlock, so we use the lifecycle callback.

public void init(ServletConfig servletConfig) throws ServletException

We will need to pass the Servlet class loader to the ServiceBlock since we will want to establish EntryPoints that are visible both inside and outside the SB (inside being deployed services and outside being in the Servlet). To do this we get the Class object first which will make it easier to step through with a line-level debugger and then get the class loader.

Class thisClass = this.getClass();

ClassLoader thisLoader = thisClass.getClassLoader();

Next, we create the Service Block implementation we will use for the example and assign it to a variable that we store in the Servlet.

serviceBlock = new ServiceBlockImpl(thisLoader);

This second line here is just a convenience; Surinam uses interfaces to help crystallize roles and responsibility and so where the ServiceBlock Interface is a general purpose API, the admin interface exists to expose a category of methods that fill a different role. The fact that it may or may not be the same instance is irrelevant. ServiceBlockAdmin serviceBlockAdmin = (ServiceBlockAdmin) serviceBlock;

We will need a way to find EntryPoints and services in the block so we acquire the intuitively-named 'ServiceFinder'. Finders are tied to ServiceBlocks which conveniently have a factory method to create Finder objects.

ServiceFinder serviceFinder = serviceBlock.getServiceFinder();

Here, we will use the administrative interface to get inside the Block so we can access its internals. In this case, we are interested in the Block's ServiceDirectory. Accessing this object is entirely optional, but we will use the fact that the Directory knows how to render an HTML representation of its internal state... this is simply a view to expose inner workings for the example and for development purposes.

svcDirectory = serviceBlockAdmin.getServiceDirectory();

Also, we will acquire and hold the Block's Blueprint Manager (BPM). We can make good use of the fact that the BPM also has methods that render its state as HTML but more importantly, it can also render a valid ActionDocument. Action Documents are worth spending time reading up on since much of the complexity of managing ServiceBlocks goes away when using Action Documents. They are the keystone for greatly simplified service block management, eliminating the need to do it programmatically. It's worth repeating that the difference between managing a Service Block programmatically and using ActionDocuments is significant, and the latter is highly recommended.

blueprintManager = serviceBlockAdmin.getBlueprintMgr();

The key to using ActionDocuments is the ServiceBlockCommander. A commander essentially wraps a Service Block with an API that hides much of the complexity of managing a Block. Note that the constructor takes a Service Block that it will wrap since it is central to the commander, both literally and figuratively.

cmdr = new ServiceBlockCommander(serviceBlock);

As mentioned before, we will be using injection to initialize our services. We can now simplify things by injecting the property information right from the ActionDocument. In the case of injecting a unique message into each service, Service 1 simply prints the message, service 2 prints its message and then calls service 1 and service 3 prints its message and calls service 2. The three snippets below were pulled from the three separate action docs in example three.

<property name="msg" value="Hello Injected World!" />
<property name="msg" value="I want to say... " />
<property name="msg" value="At this point, " />

As previously mentioned, the Commander greatly simplifies Service Block management. Instead of the several dozens of lines of code were required in the first two Surinam examples to set up the block and register entry points, contracts and implementations; now we have one line for each. Note we are using the hard-coded Action Document that was previously converted to an ActionSequence and simply 'applied' to the block.

cmdr.apply(sequence0);

Once we have applied some Action Document that will reshape the graph to the desired initial configuration, we still need a way to make invocations on at least one of the services in the block; this service is called an 'Entry Point.' We want to acquire the entry point ahead of time so we can cache it , we also need to initialize the block to something useful; obviously if there is no registered entry point in the block, we cannot 'find' something that doesn't exist.

Surinam guarantees that all registered Contracts will resolve to an implementation. Upon registration,
all Contracts automatically have a default implementation bound to it. This default implementation simply
throws the ServiceUnavailableException which is a core case that every service should be written to handle
gracefully. It is a sign of robust software to be able to handle failures gracefully. So, simply registering
a Service Contract alone with no implementation will (correctly) cause failures unless the exception is
properly handled in your code. This approach goes a long way toward creating your "Application Gestalt."

Below is the text of an ActionDocument. We can see that for the Service Block, we have defined a single Entry Point for the contract "EntryPointContract_1_0." Then we bind our implementation to that Service Point... effectively replacing the default placeholder implementation with our own. Note that each Service Contract and Service Implementation can have its own class path and we use that to allow the service classes to be loaded.

<?xml version="1.0"?>
<action xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="file://../admin/action.xsd">
    <service-block reset="true">
        <!-- Let's start with our primary entry point and a resource broker. -->
        <!-- Then we register the entry point. -->
        <entry-point class="com.codemonster.surinam.examples.EntryPointContract_1_0"/>

        <!-- Next, we register the first Service Contracts. -->
        <contract class="com.codemonster.surinam.examples.Service_001_Version_1_0">
            <classpath>
                <path-segment>/service-contracts-1.0.jar</path-segment>
            </classpath>
        </contract>


        <!-- For Schema 1, we assign the only service implementation to the only service point. This
         service is referred to as a "terminal service" as it calls no other services. -->
        <implementation class="com.indio.svcs.example3.Service_001_Impl">
            <binding>com.codemonster.surinam.examples.Service_001_Version_1_0</binding>
            <classpath>
                <path-segment>/indio-action-services-1.0.jar</path-segment>
            </classpath>
            <property name="msg" value="Hello Injected World!" />
        </implementation>

        <!-- For Schema 1, we assign an entry point implementation that will be constantly
        changing at runtime. -->
        <implementation class="com.national.services.example3.Example3EntryPoint1Impl">
            <binding>com.codemonster.surinam.examples.EntryPointContract_1_0</binding>
            <classpath>
                <path-segment>/national-software-1.0.jar</path-segment>
            </classpath>
        </implementation>
    </service-block>
</action>

To convert an XML document (like the one above) into an ActionDocument it must pass validation against the action schema (action.xsd) which can be found in the downloaded package. Once you have acquired an XML document you need to convert the string into its object form, referred to as an ActionSequence. The line below is take from example three's Example3ActionDocs class which takes hard-coded XML strings (EXAMPLE3_SCHEMA0 in this case) and converts them to sequence objects that can be 'applied' to a Service Block via the Commander wrapper. This is where the sequence in the apply statement shown earlier comes from.

public static ActionSequence sequence0 = ActionDocumentConverter.toSequence(EXAMPLE3_SCHEMA0);

Getting back to the initialization of our servlet...

The last thing we will need is the Entry Point that we will use to make invocations on the services that live and run inside the Block. We will acquire the Entry Point here and hang on to it since the invocation routing handles finding the actual implementation behind the scenes and is not normally exposed to the developer. Just as we got the Finder from the Service Block, the Finder also works a like a factory in that it is building a proxy for the service such that you will be able to cast the object and then treat it like the real thing. The only things you need to worry about are normal issues like processing failures or the service simply being unavailable.

 There is value in acquiring the service programmatically for situations where you would like to let
 business logic determine which service you might want to have handle the processing. However, it is
 worth pointing out here that for less dynamic service references, where you already know which service
 you wish to consume, Surinam supports Dependency Injection to automate this... eliminating the step where
 you explicitly acquire the Service; annotation and declarative injection are both supported.

entryPoint = (EntryPointContract_1_0) serviceFinder.getService("com.codemonster.surinam.web.contracts.EntryPointContract_1_0");

It is purely convention that we are using the fully-qualified class name as the Service Contract name since it dramatically reduces the likely-hood that there will be naming conflicts for Contracts; beyond this, there is nothing magical behind it.

Request Handling

Once the ServiceBlock is properly configured, the invocation is what comes next. To keep the example simple and to stress that it is the invocation routing and the service calls that are doing all the interesting work, when the Servlet gets a request to execute the call chain, the code is reduced to this:

result = entryPoint.process();

As you probably figured, the result is just a variable to help capture the output to externalize the internal process in a way that makes it possible to easily see in a web page. That is all there is to invocation.

Service Block Reshaping for Hyper-Dynamic behavior

To make altering the state of the ServiceBlock easy and efficient, we assigned ids to different activities like capturing and restoring the current block state as well as applying different action docs on command. There are three Action Documents provided with each one progressively raising the complexity of the services registered and the interrelationships between them. With each reshaping of the Service Graph, the call chain grows in length, altering the message that it creates; it should go without saying that once you can do that, you can do almost anything.

Looking inside the doPost method we see the switch fragment:

 switch (schemaId) {
    /* Capture the state to the session. */
    case -1: {
        /* We push the current state to the session. */
        session.setAttribute(UNDO_KEY, blueprintManager.renderActionDocument());
        break;
    }

    /* Restore the state from the session. */
    case -2: {
        String lastActionDoc = (String) session.getAttribute(UNDO_KEY);

        /* Before we apply an undo, we need to make sure that it contains something. */
        if (lastActionDoc != null) {
            System.out.println("lastActionDoc = " + lastActionDoc);

            /* We have implemented an UNDO function by caching a particular ActionDoc as provided
            by the BlueprintManager and re-applying it when asked. */

            ActionSequence undoSequence = ActionDocumentConverter.toSequence(lastActionDoc);
            cmdr.apply(undoSequence);
        }
        break;
    }

    /* Otherwise we apply the ActionDoc that has been converted to a sequence. */
    case 0:
        /* Applying an Action Doc/Sequence that initializes the SB to a starting state. */
        cmdr.apply(sequence0);
        break;

    case 1:
        /* Applying an Action Doc/Sequence for the first modified state. */
        cmdr.apply(sequence1);
        break;

    case 2:
        /* Applying an Action Doc/Sequence for the second modified state that extends the first state. */
        cmdr.apply(sequence2);

        /* We don't bother with the broker here since we expect that it has already been set up. */
        break;

    case 3:
        /* Applying an Action Doc/Sequence for the third modified state that extends the second state. */
        cmdr.apply(sequence3);

        /* no broker here either since we expect that graph 2 is pre-existing. */
        break;

    default:
        /* If the schema is unknown, we ignore it. */
}

Summary

This concludes the walkthrough. We have discussed how to register and initialize Services, acquire Finders from a ServiceBlock, to build sequences by processing documents and how to apply them to Service Blocks. From here, feel free to study the example code directly and to check out the JavaDoc. Additionally, if you have not yet done so, checking out the demos and reading the Surinam Manual should be considered a 'must' as it may give you additional insights. Good Luck.