Java Kernel - Interception MetaModel

This meta-model exposes the process of invoking an operation in a component’s interface. A meta-interface IMetaInterception is provided which allows the meta-programmer to insert arbitrary code elements called interceptors within component connections, such that an interceptor is executed whenever an operation is invoked across the connection (more specifically, either before, or after, or both before and after, the invocation). Such an interceptor might be useful in auditing the pattern of invocations and their arguments for debugging purposes, or dispatching invocations to an alternative object instance (‘hooking’), or inserting a security or concurrency control check on an invocation. Interception is especially useful in adaptation scenarios; for example, in a media streaming/ mobile computing scenario, an interceptor on a low-level protocol component could be used to monitor and detect the conditions under which a codec should be replaced.

Bindings or connections between Runes components are reified by connector components. In the simplest form of bindings between components, the DefaultConnectorFactory component is used to instantiate connectors without any interception capability. The interception-capable connector provides the same behaviour as all connectors but differs in that it exposes the IMetaInterception meta-interface to allow application developers to add/remove interceptors and inspect the current configuration of interceptors. The operations supported by the IMetaInterception metamodel are as follows:

boolean addInterceptor(Component interceptComp, int index) throws InterceptionException, ComponentException;

boolean removeInterceptor(Component interceptComp)throws InterceptionException, ComponentException;

Component[] getInterceptors();

Using Interception-Capable Connectors

To use an interception-capable connector, the developer must first load and instantiate the InterceptConnectorFactory component via the capsule’s load() and instantiate() methods respectively. Then, when connecting an interface-receptacle pair through the capsule’s connect() method, the developer specifies the instantiated connector factory as one of its arguments. Not specifying any connector factory will result in the default non-interception-capable connector to be used. The interception-capable connector performs type-safety checking when interceptors are registered with it i.e. it checks if the interceptor component exposes a matching interface and receptacle.

The following code snippet shows how to create an interception-capable connector:

ComponentType icfType = capsule.load(new StringPattern("runes.kernel.interceptImpl.InterceptConnectorFactory"));
InterceptConnectorFactory icf = (InterceptConnectorFactory) capsule.instantiate(icfType);

Creating and Adding Interceptors

Interceptors are themselves RUNES components, but must, in addition, inherit from the abstract InterceptorComponent class which specifies some common behaviour for all interceptors. For instance, it contains methods to set/get interception options (pre-, post- and/or around-interception) and to clear or set the lastinterceptor flag. The terminology is explained below.

The pre-interception option specifies whether interception code is to be executed prior to the method call reaching the implementing component. The pre-interception code must be written in the to-be-overridden pre(Object[] args) method.

Post-interception code, on the other hand, is executed when the control flows back to the caller during the invocation. It is specified in the to-be-overridden post(Object[] args) method. The around-interception option specifies that the method call is to be re-routed around the target interface and flow of control returned back to the caller.

Apart from inheriting from InterceptorComponent, it is a requirement that the interceptor implements the same interface as the sink component and hosts a receptacle of this interface type. This will allow it to be inserted between the source and sink components. Interceptors are registered with the interception-capable connector via its addInterceptor() method.

Please note that since the interceptor implements methods of the sink component’s interface, interception is performed on a per-method basis. i.e. each method in the interceptor will specify pre-, post- and around- interception code for each corresponding method in the sink interface.

Chaining of interceptors (as shown in the figure) is enabled by the meta-model- when registering an interceptor, the developer needs to specify the position in the chain at which to insert the new interceptor – using the index parameter in the addInterceptor() method. Changes made to the arguments of the method call are propagated from interceptor to interceptor to the implementing component. The result of the method call is returned unscathed. Such features, however, depend on how the developer implements the interceptor components.

The lastInterceptor flag is used to indicate the last interceptor in the chain. If around-interception is specified, then the lastInterceptor flag is the marker where the re-routing of the method invocation should occur. By setting the lastInterceptor flag appropriately in the interceptors along the chain, the developer can flexibly specify where this re-routing can occur – it need not be at the last interceptor in the chain.

STEP 1: Create the Interceptor

Given a sink interface IAdd (as used in the sample application package runes.sampleApp):

import runes.kernel.Interface;

public interface IAdder extends Interface {
public int add(int x, int y);
}

the corresponding implementation of an interceptor between a sink component implementing IAdd and a source component holding a receptacle of type IAdd is as shown in this source file: InterceptorAdderOne.java.

STEP 2: Register the Interceptor component with the connector

Adding an interceptor to an interception-capable connector is straightforward as shown in the example below:

// Load and instantiate a Calculator component
ComponentType calculatorType = capsule.load(new StringPattern("runes.sampleApp.Calculator"));
Component calculator = capsule.instantiate(calculatorType);

// Load and instantiate an Adder component
ComponentType adderType = capsule.load(new StringPattern("runes.sampleApp.SimpleAdder"));
Component adder = capsule.instantiate(adderType);

// Retrieve interfaces and receptacles for connecting a Calculator to an Adder
Interface adderIf = (Interface) capsule.getAttr(adder, "INTERFACE-runes.sampleApp.IAdder").getValue();
Receptacle calculatorAdderRecpt = (Receptacle) capsule.getAttr( calculator, "RECEPTACLE-runes.sampleApp.IAdder").getValue();

// ----- use an interception-capable connector to connect an Adder to Calculator -------
Connector CalcToAdder_intcp = capsule.connect(adderIf, calculatorAdderRecpt, icf);

// Load and instantiate an interceptor component
ComponentType interceptorAddTwoType = capsule.load(new StringPattern(
"runes.sampleApp.InterceptorAdderOne"));
Component interceptorOne = capsule.instantiate(interceptorAddTwoType);

// set interception options
((InterceptorComponent)interceptorOne).setForPreInterception();
((InterceptorComponent)interceptorOne).setForPostInterception();
((InterceptorComponent)interceptorOne).setAsLastInterceptor();
((InterceptorComponent)interceptorOne).setConnector(CalcToAdder_intcp);

// add interceptor to first position
boolean result = ((IMetaInterception) CalcToAdder_intcp).addInterceptor(interceptorOne, 0);
// ---------------------------------------------------------------------------------------

// get interceptors in binding
Component [] interceptors = ((IMetaInterception) CalcToAdder_intcp).getInterceptors();

// run call to be intercepted
System.out.println("Add Operation: result is "+((Calculator) calculator).add(5, 5));

A sample program demonstrating the use of interceptors is included in the distribution as: runes.sampleApp.SampleIntcpApp.java.