The Handler Framework
One way to characterise the overall behaviour of a service is in terms of the tasks that the service is designed to carry out. Interacting with a target service in the process of satisfying a client request is a common example of a task; monitoring some of the local resources at regular intervals is another. In many cases, modelling such tasks with dedicated components improves their maintainability and reusability. In particular, the same task components can be customised and reused in different parts of the service implementation or assembled in some guise to form more complex tasks. In recognition of these benefits, gCF defines a framework for task definition and management that builds upon a standardisation of tasks as gCube handlers.
Contents
Overview
The Handler Framework is for the most part defined by the inheritance hierarchy contained in the package org.gcube.common.core.utils.handlers
:
The high-level roles of package components are summarised below:
GCUBEIHandler
: the interface of all handlers.
GCUBEHandler
: the abstract base implementation ofGCUBEIHandler
.
GCUBEComplexHandler
: an abstract specialisation ofGCUBEHandler
for complex handlers, i.e handlers that manage other handlers.
GCUBEScheduledHandler
: a concrete specialisation ofGCUBEComplexHandler
for handlers that schedule the execution of other handlers.
GCUBESequentialHandler
: a concrete specialisation ofGCUBEComplexHandler
for handlers that chain the execution of other handlers.
GCUBEHandlerConnector
: a concrete specialisation ofGCUBEHandler
for handlers that connect the inputs and outputs of handlers chained by aGCUBEScheduledHandler
.
GCUBEParallelHandler
: a concrete specialisation ofGCUBEComplexHandler
for handlers that parallelise the execution of other handlers.
GCUBEServiceHandler
: an abstract specialisation ofGCUBEHandler
for handlers that engage in interactions with remote service port-types.
GCUBEStatefulServiceHandler
: an abstract specialisation ofGCUBEServiceHandler
for handlers that engage in interactions with remote WS-Resources.
GCUBEStagingServiceHandler
: an abstract specialisation ofGCUBEStatefulServiceHandler
for handlers that can stage the WS-resource with which they need to interact.
The framework also includes the following auxiliary packages:
org.gcube.common.core.utils.handlers.events
: a package of components related to the management of events produced by handlers.
org.gcube.common.core.utils.handlers.lifetime
: a package of components related to the management of the lifetime of handlers.
Basics Concepts
The basic behaviour of all handlers is defined by the GCUBEIHandler
interface. The class GCUBEHandler
provides a base implementation for it which most handlers are expected to directly or indirectly extend.
class MyHandler extends GCUBEHandler { ... }
Note: Implementations of GCUBIHandler
other than GCUBEHandler
are possible but not expected. The existence of an interface for handlers is mainly intended for clients that wish to layer additional expectations on the handlers they process and reuse at the same time the hierarchy of predefined handlers.
The first and foremost method of all handlers is the run()
method, which implements the task associated with the handler. This is in fact the only method of GCUBEIHandler
that GCUBEHandler
does not implement. The following is thus one of the simplest handlers:
class MyHandler extends GCUBEHandler { /** {@inheritDoc} **/ public void run() throws Exception { System.out.println("carrying out a task.."); } } ... MyHandler handler; ... handler.run();
Note: The framework makes no assumption about how run()
is implemented, expect for the fact that it may generate an exception. For some handlers it may block while for others it may return immediately. Again, for some handlers it may be invoked only once while for others it may be invoke an arbitrary number of times.
Note: Clients may wish to revert the effects of executing a handler. GCUBEIHandler
defines a method whereby the intention can be communicated to the handler (undo()
). GCUBEHandler
provides a dummy implementation of the method but its subclasses may override it if they know how to trace back the steps of their execution. Often, this is not easy or even possible.
Most handlers will need inputs and produce outputs. Since run()
does not cater for them, handlers need some form of state in which clients can put inputs and find outputs. Handlers are free to define any form of state and access to state (dedicated getters, setters, and constructors). The two forms of state discussed next, however, are predefined in GCUBEIHandler
to enable generic state management strategies across handlers of different types. Their use is entirely optional.
The Handled Object
The first form of state is an arbitrary object upon or on behalf of which the handler is expected to execute, the handled object. GCUBEIHandler
defines accessors for it and GCUBEHandler
implements them (setHandled(object)
, getHandled()
). GCUBEIHandler
and GCUBEHandler
are in fact parametric in the type of the handled object:
public interface GCUBEHandler<T> { ... public void setHandled(T h); public T getHandled(T h); ... }
Note: The parametrisation allows the framework to be specialised in contexts where handlers are expected to handle objects of a give type, at no loss of static typechecking.
In spite of their name, not all handlers need a handler object of course. MyHandler
above does not for example and derives GCUBEHandler
as a raw type. To avoid compiler's warnings, it could have been defined as:
class MyHandler extends GCUBEHandler<Object> {...}
The following variation of MyHandler
exemplifies the use of handled objects:
class MyHandler extends GCUBEHandler<String> { /** {@inheritDoc} **/ public void run() throws Exception { System.out.println("executing with "+this.getHandled()); } } ... MyHandler handler; ... handler.setHandled("some task"); handler.run();
Note: In practice, the handled object is often an instance of some distinguished component of the service implementation (e.g. a stateful resource). One ore more handlers are then used to implement tasks associated with the instance as if they were separate extensions of its class:
class SomeServiceComponent {.....} ... class SomeComponentTask extends GCUBEHandler<SomeServiceComponent> { /** {@inheritDoc} **/ public void run() throws Exception { ... ... this.getHandled().getSomeProperty()... ... // do something important ..... ... this.getHandled().setSomeOtherProperty(...)... ... } }
Note: For convenience, GCUBEHandler
has a constructor parametric in the handled object to which subclasses may wish to delegate.
class MyHandler extends GCUBEHandler<String> { public MyHandler(String s) {super(s);} .... }
The Blackboard
A less constrained form of state associated with all handlers is in the forms of a map of arbitrary named objects, the blackboard. Again, GCUBEIHandler
defines accessors for it GCUBEHandler
implements them (getBlackboard()
, setBlackboard(map)
, clearBlackboard()
). The names of the objects that handlers expect to find in their state or else put in their state is defined by the convention (i.e. it must be part in the documentation of the handler).
class MyHandler extends GCUBEHandler<Object> { /** {@inheritDoc} **/ public void run() throws Exception { this.getBlackboard().put("output",this.getBlackboard().get("input")+1); } } ... MyHandler handler; ... Map<String,Object> map = new HashMap<String,Object>(); map.put("input",5); handler.setBlackboard(map); handler.run(); System.out.println(handler.getBlackboard().get("output"));
Note: This exemplifies the use of the blackboard but does not justify it. The blackboard is not intended for the exchange of information between handlers and their clients, where the handled object, getters, setters, and constructors fare better. It is primarily intended for the loosely-coupled exchange of information between handlers that are developed independently but under the same conventions. As we discuss below, this exchange may occur when handlers are chained in a GCUBESequentialHandler
.
Miscellaneous Functionality
GCUBEIHandler
defines a number of auxiliary methods that may be useful for the management of handlers and GCUEBHandler
implements them. These include:
getID()
: returns a unique identifier for the handler which may be used to discriminate between instances of the same class;
getName()
/setName(string)
: returns/sets a name for the handler (by default, this is the unqualified name of its class). This is used by the framework primarily for logging purposes;
getSecurityManager()
/setSecurityManager(securitymanager)
: returns/set a security manager for the execution of the handler. This methods allow security information to propagate from clients to handlers;
getScopeManager()
/setScopeManager(scopemanager)
: returns/set a scope manager for the execution of the handler. This methods allow scope information to propagate from clients to handlers;
getLogger()
: returns a logger for the execution of the handler. It is recommended that handlers make use of the predefined loggers rather than define their own logger (this allows the injection of different loggers and the transparent redirection of logs based on context of usage).
Lifetime and Event Management
The lifetime of a handler may be naturally divided in a number of phases, where the handler changes state from phase to phase. For example, the handler may be in a running state, it may have completed successfully, it may have failed, or it may be temporarily suspended.
Some clients may need to know the current state of the handler in order to correctly manage it. There is thus value in modelling its states explicitly. There is also value in modelling its state changes, as lifetime events which interested clients can be notified of and act upon. There is in fact value in notifying clients of arbitrary events that may occur during the execution of the handler, besides lifetime events.
We discuss below the facilities offered by the framework for handler lifetime and event management.
Lifetime Management
The Handler Framework offers the following abstractions for lifetime management.
- The class
State
in packageorg.gcube.common.core.utils.handlers.lifetime
is the abstract root of all the possible states of a handler's lifetime.State.Created
,State.Running
,State.Done
,State.Failed
, andState.Suspended
are predefined subclasses ofState
. Each of these classes has a single instance that handlers and their clients can refer to. For example,Created
is defined as follows:
public abstract class State { ... public static class Created extends State { public static final Created INSTANCE = new Created(); protected Created(){...}; ... } ... }
-
GCUBEHandler
defines a method that clients can invoke to obtain the current state of a handler (cf.getState()
). It also defines a method that handler implementers can invoke to change the state of the handler (cf.setState(state)
).
class MyHandler extends GCUBEHandler<...> { ... /** {@inheritDoc} **/ public void run() throws Exception { this.setState(Running.INSTANCE); ... this.setState(Suspended.INSTANCE); ... this.setState(Running.INSTANCE); ... try { .... catch(Exception e) { .... this.setState(Failed.INSTANCE); } ... this.setState(Done.INSTANCE): } ... }
Note: Created.INSTANCE
is the initial state of every handler derived from GCUBEHandler
.
Note: setState()
enforces precise constraints on the state transitions of a handler, i.e. what states it can assume from the current one. The following state diagram illustrates the legal transitions for predefined states.
The legal transitions conform to standard expectations, except perhaps for the transition from DONE.INSTANCE
to RUNNING.INSTANCE
that shows the possibility of 're-running' a handler that has completed successfully. An attempt to perform an illegal transition results in an IllegalStateException
. The programmatic specification of legal transitions for predefined and user-defined states is discussed below.
- There is no guarantee that handlers will ever invoke
setState()
as they change state. Those that do can flag their commitment by implementing theLifetime
interface, also in packageorg.gcube.common.core.utils.handlers.lifetime
:
public interface Lifetime<T> extends GCUBEIHandler<T> { public State getState(); }
Note: GCUBEHandler
implements Lifetime
even though it does not declare it. In other words, it offers support for state management but does presume that subclasses will make use of it.
Event Management
All handlers are event producers. They accept subscriptions for and fire notifications of events in one more topics of interests, first and foremost lifetime events. The handling of topics, events, subscriptions, and notifications relies of course on the the Event Framework (if you are not already familiar with the Event Framework this is a good time to have a look).
Topics
The class Topic
in package org.gcube.common.core.utils.handlers.events
implements GCUBETopic
as the abstract root of all topics associated with the execution of handlers. Topic.LifetimeTopic
is a predefined subclass of Topic
that 'groups' lifetime events.
public abstract class Topic implements GCUBETopic { ... public static final class LifetimeTopic extends Topic { public static final LifetimeTopic INSTANCE = new LifetimeTopic(); protected LifetimeTopic(){}; } }
Note that, for convenience, LifetimeTopic
has a single instance called LifetimeTopic.INSTANCE
.
Events
The class Event
, also in package org.gcube.common.core.utils.handlers.events
, extends GCUBEEvent
as the abstract root of all events that are about Topic
s, whatever their payload. Event.LifetimeEvent
is an abstract subclass of Event
that models lifetime events. Event.Running
, Event.Suspended
, Event.Done
, and Event.Failed
are four concrete subclasses of LifetimeEvent
:
abstract public class Event<TOPIC extends Topic,PAYLOAD> extends GCUBEEvent<TOPIC,PAYLOAD>{ ... public abstract static class LifetimeEvent extends Event<LifetimeTopic,GCUBEHandler<?>>{} public static class Running extends LifetimeEvent {...} public static class Suspended extends LifetimeEvent {...} public static class Done extends LifetimeEvent {...} public static class Failed extends LifetimeEvent {...} }
Note: LifetimeEvent
s mirror State
s and are associated with the corresponding transitions. The exception is the initial state Created
, which is not associated with a state transition and thus does not have a corresponding lifetime event. We discuss below how lifetime events are raised during state transitions.
Note: LifetimeEvent
s have a GCUBEHandler
as their payload. This is of course the very handler the events are about.
Monitors
Monitor
, also in package org.gcube.common.core.utils.handlers.events
, is a partial implementation for consumers of Event
s about Topic
s. Concrete consumers may extend it and override its callbacks of interest. There are callbacks for each of the predefined lifetime events (onRunning(event)
, onCompletion(event)
, onFailure(event)
,onSuspension(event)
), as well as a catch-all callback for any event (onAnyEvent(event)
):
public abstract class Monitor implements GCUBEConsumer<Topic,Object> { ... protected void onRunning(Running e) {} protected void onCompletion(Done e) {} protected void onFailure(Failed e) {} protected void onSuspension(Suspended e) {} protected void onAnyEvent(Event<?,?> e){} }
Note that all the specific callbacks receive the corresponding event and that their implementation does nothing by default. Note also the generic callback onAnyEvent()
receives an equally generic event. Consumer
will pass to onAnyEvent()
all the events that it receives, including lifetime events (but only after having more specific callbacks return).
Subscriptions
GCUBEIHandler
defines method whereby Monitor
s can subscribe to and unsubscribe from one or more Topic
s (cf. subscribe(monitor, topics*)
, unsubscribe(monitor, topics*)
). The following example shows the common case of a monitor of Done
and Failed
events. The monitor is defined as an anonymous subclass of Monitor
and subscribed for the Lifetime
topic.
Monitor myMonitor = new Monitor() { /** {@inheritDoc} **/ protected void onCompletion(Done e) {...} /** {@inheritDoc} **/ protected void onFailure(Failed e) {...} } ... MyHandler handler; ... handler.subscribe(myMonitor, LifetimeTopic.INSTANCE); handler.run();
Note: subscribe()
and unsubscribe()
accept zero or more topic parameters. When none are specified, the methods assume implicitly a single topic, Lifetime
. In other words subscriptions are by default to lifetime events and the subscription above could have been more simply setup as handler.subscribe(myMonitor)
.
Notifications
GCUBEHandler
defines a protected method that handlers can invoke to report the occurrence Event
s about Topic
s to Monitors
that have previously subscribed for them (cf. notify(topic,event)
).
Handlers developers need to invoke notify()
only for custom events (see below). For the common case of lifetime events, notify()
is invoked transparently by the implementation of setState()
in GCUBEHandler
when the handler transitions to the corresponding states.
Extensions
Developers can define new states or events for their handlers and inject them into the Handler Framework.
Adding States
Defining new states amounts to subclassing State
, which is particularly designed for extension. The relevant methods are those shown below.
public abstract class State { ... abstract protected void addPrevious(List<State> previous); public LifetimeEvent getLifetimeEvent() {return null;} public void onEnter() throws Exception {} public void onExit() throws Exception {} ... }
In particular:
-
addPrevious(state*)
is a callback to subclasses, which may add to the list parameter the states from which a handler may transition to the state. The implementation of this method thus defines the legal transitions for the state.
-
getLifetimeEvent()
returns the lifetime event that should be raised when a handler transitions to the state. By default, the method returnsnull
to signal that the event is private, i.e. subscribers should not be notified of its occurrence. We discuss later how subclasses may override this method to make the state public.
-
onEnter()
andonExit()
are also callbacks to subclasses which may specify arbitrary codeto be invoked by the framework before a handler transitions to and form the state, respectively. By default, they do nothing.
The class below defines a private state to which handlers may transition from the CREATED
state:
abstract class MyState extends State { /** {@inheritDoc} */ protected void addPrevious(List<State> previous) {previous.add(Created.INSTANCE);} /** {@inheritDoc} */ public void onEnter() throws Exception {System.out.println("entering my state");} /** {@inheritDoc} */ public void onExit() throws Exception {System.out.println("exiting my state");} }
Note: As currently defined, MyState
allows the generation of multiple instances, all of which model the same state. For convenience, State
overrides equals()
and hashCode()
to capture this equivalence. It is nonetheless more economic to follow the pattern used for predefined states and define MyState
as a singleton class:
abstract class MyState extends State { public static final MyState INSTANCE = new MyState(); protected MyState() {...} ... }
Note: MyState
is currently a final state. Once handlers transition to it they cannot legally move to any other state, predefined or not. More often developers will want to inject new states between existing ones. To achieve this, it is necessary to subclass also the states that should follow the new one. For example, the following class derives Running
and overrides addPrevious()
to replace Created.INSTANCE
with MyState.INSTANCE
:
abstract class MyRunning extends Running { public static final MyRunning INSTANCE = new MyRunning(); protected MyRunning() {} protected void addPrevious(List<State> previous) { super.addPrevious(previous); previous.remove(Created.INSTANCE); previous.add(MyState.INSTANCE); } }
With MyState
and MyRunning
, the following becomes a set of legal state transitions for handlers:
Here is an handler that performs the new state transitions:
class MyHandler extends GCUBEHandler<...> { ... /** {@inheritDoc} **/ public void run() throws Exception { this.setState(MyState.INSTANCE); ... this.setState(MyRunning.INSTANCE); ... this.setState(Suspended.INSTANCE); ... this.setState(MyRunning.INSTANCE); ... try { .... catch(Exception e) { .... this.setState(Failed.INSTANCE); } ... this.setState(Done.INSTANCE): } ... }
Note: While it is necessary to derive all the existing states that should follow new states, it is not necessary to derive other states in turn. Indeed, it was not necessary to derive Suspended
,Failed
or Done
in the previous example. This is because the Handler Framework tolerates the presence of subclasses when checking the validity of a state transition. For example, it tolerates a transition from Suspended
to MyRunning
even though Suspended
defined Running
as one of its previous states.
Note: It is not possible to change the start state of a handler, as Created
is set directly by the framework when the handler class is instantiated.
Adding Events
Defining new lifetime events amounts to subclassing LifetimeEvent
and defining new generic events amounts to subclassing Topic
and Event
.
The following class defines a new lifetime event for the state MyState
introduced above:
class MyLTEvent extends LifetimeEvent {}
The link between MyLTEvent
and MyState
is established by overriding the method getLifetimeEvent()
in the definition of MyState
(thereby turning MyState
into a public event):
class MyStates extends State { ... /**{@inheritDoc}*/ public LifetimeEvent getLifetimeEvent() {return new MyLTEvent();} }
The following classes define instead a new topic, a new generic event in that topic, and a payload for the new event:
class MyTopic extends Topic { public static final MyTopic INSTANCE = new MyTopic(); protected MyTopic(){}; } class MyPayload {...} class MyEvent extends Event<MyTopic,MyPayload> { MyEvent(MyPayload p) {this.setPayload(p);} //convenience constructor }
Finally, the following class specialises Monitor
for the convenience of subscribers to the new events:
abstract class MyMonitor extends Monitor { /**{@inheritDoc}*/ protected void onAnyEvent(Event<?, ?> e) { if (e instanceof MyTEvent) {onMyEvent((MyEvent) e);} else if (e instanceof MyLTEvent) {onMyLTEvent((MyLTEvent) e);} else super.onAnyEvent(e); } protected void onMyEvent(MyEvent e) {} protected void onMyLTEvent(MyLTEvent e) {} }
Note: MyMonitor
repeats within onAnyEvent()
the pattern followed by the base Monitor
for predefined lifetime events. It recognises the events for which it is designed and dispatches them to callbacks that subscribers may implement in subclasses.
The example below illustrates the use of the extensions in the assumption that MyHandler
is defined as at the end of the previous section:
MyMonitor m = new MyMonitor() { /**{@inheritDoc}*/ protected void onMyEvent(MyEvent e) {System.out.println("Doing something with my event");...} /**{@inheritDoc}*/ protected void onMyLTEvent(MyLTEvent e) {System.out.println("Doing something with my lifetime event");...} }; MyHandler handler = new GCUBEHandler<...>() { ... /**{@inheritDoc}**/ public void run() throws Exception { ... this.setState(MyState.INSTANCE); ... MyPayload p = ... ... this.notify(MyTopic.INSTANCE,new MyEvent(p)); }
Note: Like predefined ones, the new lifetime event (MyLTevent
) is raised transparently by the framework when the handler transition to the associated state (MyState
). The new generic event must instead be notified explicitly.
Note: The subscription of m
specifies the new topic as well as the predefined LifetimeTopic
, as the latter is implicitly used only when no topics are specified.
General-Purpose Handlers
The Handler Framework includes a small number of handlers that do not perform specific tasks but govern how other handlers are executed: in a chain, in parallel, or at regular intervals. It is advantageous to dispense clients with managing such generic aspects of execution semantics. It is even more advantageous to model these facilities as handlers, because this allows clients to combine them in arbitrarily complex ways. For example, it becomes easy for a client to schedule the parallel execution of a number of chains. In essence, these general-purpose handlers constitute a minimal toolkit for programmatic process management.
Complex Handlers
GCUBEComplexHandler
derives GCUBEHandler
as a partial implementation for handlers that manage the execution of one or more other handlers, its components. It offers a small number of methods to add, remove, or inspect components:
-
addHandlers(handler*)
: add one or more handlers to the component list; -
removeHandler(handler)
: removes one component from the component list; -
getHandlers()
: returns the component list;
It addition, GCUBEComplexHandler
defines a convenience constructor that delegates to addHandlers()
and which subclasses can delegate to in turn.
Note the following:
-
GCUBEComplexHandler
retains the type parametrisation ofGCUBEHandler
:
public abstract class GCUBEComplexHandler<T> extends GCUBEHandler<T> {...}
- Components are constrained to handler objects of a subtype of the type parameter. The components of a
GCUBEComplexHandler<Object>
can handle objects of any type, those of aGCUBEComplexHandler<String>
must handleString
s, and those of aGCUBEComplexHandler<Number>
can handleNumber
s as well asInteger
s.
- Components can be instances of different handler classes or different instance of the same handler class. Further, components may handle the very same object, different objects of the same type or, type parametrisation allowing, objects of completely different types.
- Complex handlers may or may not expect or make use of the handled object. The predefined subclasses of
GCUBEComplexHandler
dicussed later do not. In this sense, the type parameter of a complex handler acts primarily as a type constraint on the objects handled by its components.
-
getHandlers()
returns a copy of the component list, so that addition and removal of components must occur throughaddHandlers()
andremoveHandler()
, respectively. Further, all three methods are synchronized. Overall, this promotes the thread-safety of subclasses, that do not need to worry about modifications of the component list as they iterate over it.
-
addHandlers()
and the constructor that delegates to it both take a variable number of parameters. Since these have parametric types, the compiler will generate safety warnings with all their calls. However, clients may safely disable them as the implementation of those methods guarantee soundness. (The interaction of variable-length parameter lists and generic is one of the roughest corners of java typechecking. The conditions under which the two features are unsound are generally rare and certainly do not arise inGCUBEComplexHandler
.)
Finally, note that:
-
GCUBEComplexHandler
overrides the dummy implementation ofundo()
inGCUBEHandler
by delegating it to its components in reverse order of addition.
Sequential Handlers
A GCUBESequentialHandler
derives GCUBEComplexHandler
to execute components in the order in which they have been added. The sequential handler in the following example chains the execution of two components.
class MyHandler1 extends GCUBEHandler<String> { //a dummy handler of strings public MyHandler1(String s) {super(s);} public void run() throws Exception { System.out.println(this.getHandled()+"."); } } class MyHandler2 extends GCUBEHandler<Integer> {// a dummy handler of integers public MyHandler2(int i) {super(i);} public void run() throws Exception { System.out.println(this.getHandled()+1); } } ... GCUBESequentialHandler<Object> chain = new GCUBESequentialHandler<Object>(new MyHandler1("one"),new MyHandler2(1)); chain.run();
Note that chain
has type GCUBESequentialHandler<Object>
and thus accepts components that handle objects of different types, such as MyHandler1
and MyHandler2
.
Note also that MyHandler1
and MyHandler2
execute independently int the context of chain
. This is not normally the case. One loose form of dependency occurs in the presence of failure, in that a component that fails may invalidate the actions of all those that executed previously. In this respect notice that:
- a sequential handler observes the execution errors of its components and invokes
undo()
on all those that have already completed. The invocations occurs in parallel and a warning is logged for each that fails.
Stronger forms of execution dependencies are also common. Typically, the output of a component may serve as inputs of the component that follows it. This is true even for components that have been independently developed but are brought together opportunistically in the context of a sequential handler. Sequential handlers offer two ways to manage these dependencies, one that relies on a conventional use of the blackboard and another that requires dedicated connector components.
Blackboard Exchanges
The first approach relies on the coordinated use of the blackboard. Components are written to find their input and place their outputs in the blackboard under well documented names. During execution, the sequential handler takes care of merging the blackboard of a component with the blackboard of the next component in the list (the merge is 'non-destructive', i.e. the contents of the blackboard of the next component to execute are always preserved). To exemplify:
class MyHandler1 extends GCUBEHandler<String> { ... public void run() throws Exception { System.out.println(this.getHandled()+"."); this.getBlackboard().put("standardname", this.getHandled().length()); } } class MyHandler2 extends GCUBEHandler<Integer> { ... public void run() throws Exception { System.out.println(this.getHandled()+((Integer)this.getBlackboard().get("standardname"))); } }
The blackboard offers maximum flexibility in combining components at runtime, but it is suitable only when the components are drawn from a pool governed by common conventions. However, components are often written with ad-hoc expectations on how they should receive inputs and produce outputs. This is (artificially!) shown in the following example:
class MyHandler1 extends GCUBEHandler<String> { private String s = new String(); ... public getString(return s;) public void run() throws Exception { for (int i=0;i<this.getHandled().length();i++) s += this.getHandled(); } } class MyHandler2 extends GCUBEHandler<Integer> { private int toAdd; ... public void setToAdd(int i) {this.toAdd=i;} public void run() throws Exception { System.out.println(this.getHandled()+this.toAdd); } }
In these cases explicit connectors are required, as shown below.
Connectors
The second approach allows maximum independence of components but it is suitable only when their combination is statically known. In particular, it requires that some components play the dedicated role of connectors, i.e. connect those that precede them with those that follow them. The Handler Framework provides a partial implementation for connectors with the abstract class GCUBEHandlerConnector
:
public abstract class GCUBEHandlerConnector<T, PREVIOUS extends GCUBEIHandler<? extends T>, NEXT extends GCUBEIHandler<? extends T>> extends GCUBEHandler<T> { ... public PREVIOUS getPrevious() {return this.previous;} public NEXT getNext() {return this.next;} public final void run() throws Exception {...} public abstract void connect() throws Exception; }
The class is much easier to use than declare. It has three type parameters:
-
T
is the type parameter of the handled object. AGCUBEHandlerConnector
usually does not expect or make use of a handled object but the type parameter allows the connector to match that of the sequential handler so as to occur as one of its components.
-
PREVIOUS
is the type of the component that precedes the connector in a sequential handler (it therefore handles an object of some subtype ofT
).
-
NEXT
is the type of the component that follows the connector in a sequential handler (it therefore handles an object of some subtype ofT
).
A connector that links MyHandler1
and MyHandler2
could be declared and used as follows:
class MyConnector extends GCUBEHandlerConnector<Object,MyHandler1, MyHandler2> { public abstract void connect() throws Exception {...} } ... GCUBESequentialHandler<Object> chain = new GCUBESequentialHandler<Object>(new MyHandler1("one"),new MyConnector(),new MyHandler2(1)); chain.run();
Notice that the type parameter Object
matches the type parameter of the chain
and MyHandler1
and MyHandler2
are the types of the handlers that need connecting.
As to the implementation of MyConnector
, this concentrates in the method connect
, which is delegated to by the unmodifiable run()
common to all handlers. connect()
can invoke the accessors getPrevious()
and getNext()
to obtain the MyHandler1
that precede it and the MyHandler2
that follow it in chain
. It can then simply connect their specific interfaces:
class MyConnector extends GCUBEHandlerConnector<Object,MyHandler1, MyHandler2> { public abstract void connect() throws Exception { this.getNext().setToAdd(this.getPrevious().getString().length()); } }
Parallel Handlers
A GCUBEParallelHandler
derives GCUBEComplexHandler
to execute components in parallel. The parallel handler in the following example parallelises the execution of two components.
class MyHandler1 extends GCUBEHandler<String> { //a dummy handler of strings public MyHandler1(String s) {super(s);} public void run() throws Exception { System.out.println(this.getHandled()+"."); } } class MyHandler2 extends GCUBEHandler<Integer> {// a dummy handler of integers public MyHandler2(int i) {super(i);} public void run() throws Exception { System.out.println(this.getHandled()+1); } } ... GCUBEParallelHandler<Object> master = new GCUBEParallelHandler<Object>(new MyHandler1("one"),new MyHandler2(1)); master.run();
Note that master
has type GCUBEParallelHandler<Object>
and thus accepts components that handle objects of different types, such as MyHandler1
and MyHandler2
.
A parallel handler always synchronises with its components, i.e. terminates when all of them do. However, it can react to their failures in either one of two ways. The failure modes are the values of the GCUBEParallelHandler.Mode
enumeration:
- in
Mode.LAX
, the parallel handler tolerates component failures. This is default mode. - in
Mode.STRICT
, the parallel handler fails as soon as one of its component does.
- in
Modes can be set at creation time or at any later time before the handler is run (cf. methods setMode(mode)
,getMode()
). In any case, the parallel handler returns the component that have failed with the method getFailedHandlers()
and invokes undo()
on those that have instead completed successfully.
Like sequential handlers, parallel handlers propagate their blackboard to their components, again non-destructively. They also propagate scope and security managers to their components, making sure that credentials and scope set for the current thread is available in the threads in which their components are executed.
Scheduling Handlers
[coming soon]
Handlers for remote Interactions
[coming soon]
Service Handlers & the Client Interface
[coming soon]
Staging Service Handlers
[coming soon]