Array support

As an extension to the JAIN SLEE specification, Rhino supports CMP field declarations of arrays for the following SLEE-defined types:

  • javax.slee.ActivityContextInterface, and any subclass of this interface

  • javax.slee.SbbLocalObject, and any subclass of this interface

    • If the abstract getter and setter methods for the CMP field are defined in the SBB abstract class (as opposed to a CMP extension interface), the corresponding <cmp-field> declaration in the deployment descriptor may not include an <sbb-alias-ref> element.

  • javax.slee.EventContext

  • javax.slee.profile.ProfileLocalObject, and any subclass of this interface.

Arrays may be declared with any dimension. Array support is automatic wherever the basic type is supported in CMP fields; in other words, no user prompt or directive is necessary.

Serialization enhancements

Rhino introduces a number of enhancements that offer significantly more control and flexibility over how CMP field values are serialized, and much better serialization performance, when compared with standard Java serialization. These include:

FastSerializable

The com.opencloud.util.FastSerializable interface provides a simple alternative to the standard Java java.io.Serializable interface where the exact type of an object to be serialized is known at compile time.

The FastSerializable interface is defined as follows:

package com.opencloud.util;

public interface FastSerializable {
    public void toStream(java.io.DataOutput stream)
        throws java.io.IOException;
}

A class implementing the FastSerializable interface must provide a public constructor that takes either:

  • a single java.io.DataInput argument; or

  • a java.io.DataInput argument and a java.lang.ClassLoader argument.

If both constructors are declared by a given class then the two-argument constructor is used.

Serialization of the object is performed by an invocation of the toStream method. Object state must be written to the DataOutput passed as the input argument. Deserialization is performed by new object instantiation and constructor invocation. Object state can be read from the DataInput argument. The ClassLoader argument, if present, can be used to resolve any application-specific classes stored in the stream.

Rhino recognises and supports the FastSerializable contract on all CMP fields. Arrays of FastSerializable types of any dimension are handled automatically by Rhino, and no special treatment is necessary.

While the FastSerializable contract has some similarity to the java.io.Externalizable contract, there are some differences that warrant discussion:

  • FastSerializable operates in terms of data I/O streams, while Externalizable operates in terms of object I/O streams. The reason for this is that FastSerializable desires to avoid the generally costly serialization overhead that occurs when serializing arbitrary objects. By limiting the stream I/O to basic datatypes, it forces the user to think about the most performance-efficient manner in which the object state can be serialized and deserialized.

  • When FastSerializable types are used in CMP fields, Rhino’s CMP implementation typically determines the type of object to reconstruct during deserialization at code-generation time, not run time; so care must be taken that objects are restored using the correct type. Problems occur, for example, if a CMP field is declared in terms of FastSerializable type Foo, but an object of subclass Bar is stored in the CMP field. The CMP implementation assumes that the CMP field will only store a Foo object, and will instantiate a Foo object when the CMP field is deserialized, leading to deserialization failures. Making classes that implement FastSerializable final is good practice, unless this issue is taken into consideration.

  • No handling of shared references is performed by the implementation. If the same object is encountered twice during serialization, then two copies of the object will be stored and subsequently deserialized.

FastSerialize

The com.opencloud.util.FastSerialize class provides some utility functions that may be useful to application developers implementing their own serialization logic based around FastSerializable.

Example

Below is an example of a FastSerializable type:

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import com.opencloud.util.FastSerializable;

public final class Person implements FastSerializable {
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public Person(DataInput in, ClassLoader classLoader) throws IOException {
        firstName = in.readUTF();
        lastName = in.readUTF();
        age = in.readInt();
    }

    @Override
    public void toStream(DataOutput out) throws IOException {
       out.writeUTF(firstName);
       out.writeUTF(lastName);
       out.writeInt(age);
    }

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    private final String firstName;
    private final String lastName;
    private final int age;
}

Encodable

The com.opencloud.rhino.cmp.Encodable interface provides a similar contract to that of FastSerializable but is specifically targeted at types stored in SBB CMP fields. It provides access to utility functions to encode and decode SLEE-specific datatypes that are not defined as serializable, such as SBB and profile local object references, and an encodable context that can be set by an SBB to pass information to the encoding . decoding methods, such as references to resource adaptor provider objects.

The Encodable interface is defined as follows:

package com.opencloud.rhino.cmp;

import com.opencloud.rhino.cmp.codecs.EncoderUtils;

public interface Encodable {
    public void encode(java.io.DataOutput out, EncoderUtils utils)
        throws java.io.IOException;
}

A class implementing the Encodable interface must provide a public constructor that takes, in this order: a java.io.DataInput argument, a java.lang.ClassLoader argument, and a com.opencloud.rhino.cmp.codecs.DecoderUtils argument.

Serialization of the object is performed by an invocation of the encode method. Object state must be written to the DataOutput passed as the input argument. Deserialization is performed by new object instantiation and constructor invocation. Object state can be read from the DataInput argument. The ClassLoader argument can be used to resolve any application-specific classes stored in the stream.

The constructor and encode methods of an Encodable type are always invoked with the same transaction context used to access or update the CMP field. This is typically only of consequence if an encodable context is used to provide access to other SBB CMP fields.

EncoderUtils / DecoderUtils

SLEE-defined datatypes such as SBB and profile local objects, activity context interface objects, and event context objects are not defined by the SLEE specification as being serializable. The SLEE specification does provide provision for storing objects of these types directly into CMP fields; but the lack of implicit serializability means that, for example, an SBB local object reference cannot be encapsulated within some other object which is stored into CMP, as object serialization will fail when it reaches the unserializable SBB local object reference.

The EncoderUtils object passed to the encode method provides access to methods that can serialize these SLEE-defined datatypes, allowing classes that implement the Encodable contract to encapsulate objects of these datatypes and still be storable into CMP fields. The corresponding DecoderUtils object passed to the decoding constructor provides access to methods that can deserialize these datatypes, allowing correct object reconstruction during deserialization.

Rhino recognises and supports the Encodable contract on all CMP fields; however the utility methods provided by SleeDatatypeEncoder and SleeDatatypeDecoder only function under certain conditions:

  • Encode and decode of all SLEE-defined datatypes is supported for SBB and SBB Part CMP fields.

  • Encode and decode of EventContext objects is supported for Activity Context Interface attributes.

  • Encode and decode of SLEE-defined datatypes is unsupported in any other case, and invoked methods will throw a java.lang.UnsupportedOperationException.

Arrays of Encodable types of any dimension are handled automatically by Rhino, and no special treatment is necessary.

Unlike FastSerializable types, CMP fields that store Encodable types may, at runtime, store a subclass of the declared CMP field type without issue. For example, if a CMP field is declared in terms of Encodable type Foo, an object of subclass Bar may be stored in the CMP field and it will serialize and deserialize as expected. Deserialization is, however, more efficient if the type of the stored object is the same as the CMP field type, as reflection must be used to reconstruct a stored object if the type of the stored object differs from the expected type.

Like FastSerializable, no handling of shared references is performed by the implementation. If the same object is encountered twice during serialization, then two copies of the object will be stored and subsequently deserialized.

Example

Below is an example of an Encodable type:

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import javax.slee.ActivityContextInterface;
import com.opencloud.rhino.cmp.Encodable;
import com.opencloud.rhino.cmp.codecs.DecoderUtils;
import com.opencloud.rhino.cmp.codecs.EncoderUtils;
import com.opencloud.rhino.cmp.codecs.SleeDatatypeDecoder;
import com.opencloud.rhino.cmp.codecs.SleeDatatypeEncoder;

public final class Relay implements Encodable {
    public Relay(IncomingSbbLocalObject incoming, OutgoingSbbLocalObject outgoing, ActivityContextInterface aci) {
        this.incoming = incoming;
        this.outgoing = outgoing;
        this.aci = aci;
    }

    public Relay(DataInput in, ClassLoader classLoader, DecoderUtils utils) throws IOException {
        SleeDatatypeDecoder decoder = utils.getSleeDatatypeDecoder();
        incoming = decoder.decodeSbbLocalObject(in);
        outgoing = decoder.decodeSbbLocalObject(in);
        aci = decoder.decodeActivityContextInterface(in);
        messageCount = in.readInt();
    }

    @Override
    public void encode(DataOutput out, EncoderUtils utils) throws IOException {
       SleeDatatypeEncoder encoder = utils.getSleeDatatypeEncoder();
       encoder.encodeSbbLocalObject(incoming, out);
       encoder.encodeSbbLocalObject(outgoing, out);
       encoder.encodeActivityContextInterface(aci, out);
       out.writeInt(messageCount);
    }

    public IncomingSbbLocalObject getIncomingSbb() { return incoming; }
    public OutgoingSbbLocalObject getOutgoingSbb() { return outgoing; }
    public ActivityContextInterface getActivityContextInterface() { return aci; }

    public void incMessageCount() { messageCount++; }
    public int getMessageCount() { return messageCount; }

    private final IncomingSbbLocalObject incoming;
    private final OutgoingSbbLocalObject outgoing;
    private final ActivityContextInterface aci;
    private int messageCount;
}

Datatype codecs

The Encodable contract provides an in-line mechanism for object serialization. That is, code for serialization and deserialization forms part of the class itself. There may be times, however, when it is desired or necessary for the serialization code to be separated from the class being serialized. For example, the serialization logic may have common components that can be shared between multiple classes, or the source code for the class being serialized may not be available to be enhanced to support the FastSerializable or Encodable serialization contracts.

To support these situations, Rhino allows a datatype codec to be defined and associated with either a CMP field or a serializable class directly. The datatype codec specifies how objects of the target type are serialized and deserialized, essentially providing a third-person perspective to the Encodable contract.

A datatype codec must implement the com.opencloud.rhino.cmp.codecs.DatatypeCodec interface. This interface is defined as follows:

package com.opencloud.rhino.cmp.codecs;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public interface DatatypeCodec<T> {
    public void encode(T value, DataOutput out, EncoderUtils utils)
        throws IOException;
    public T decode(DataInput in, ClassLoader classLoader, DecoderUtils utils)
        throws IOException;
}

A datatype codec class must be public (and static, if an inner class) and implement a public no-argument constructor. The encode method functions identically to the encode method defined by the Encodable interface, but takes the object to be serialized as an additional argument. The decode method functions identically to the deserialization constructor required by the Encodable contract, but returns the deserialized object as the method return result. The generic type parameter T identifies the type of object that the datatype codec is capable of serializing and deserializing. Generally, a datatype codec does not need to concern itself with encoding or decoding null values, as Rhino will only invoke the codec for non-null values. There is one exception to this rule, discussed in the Datatype codecs for collections section below.

The encode and decode methods are always invoked with the same transaction context used to access or update the CMP field. This is typically only of consequence if an encodable context is used to provide access to other SBB CMP fields.

A datatype codec is associated with the corresponding datatype using the @DatatypeCodecType annotation. This annotation requires the datatype codec class to be specified as an argument. The annotation can be used either directly on the target class to be serialized, or attached to a CMP field getter or setter method. If attached to a CMP field getter or setter method of an array type, then the datatype codec need only be defined in terms of the base array component type. The codec will be invoked for each non-null array element encountered during serialization or deserialization.

Datatype codecs are supported on all CMP fields where Encodable types are supported, and have the same conditions of use.

The @DatatypeCodecType annotation may not be used on a CMP field getter or setter method where the CMP field is one of the following types, or is an array of any dimension of one of the following types:

  • javax.slee.ActivityContextInterface, and any subclass of this interface

  • javax.slee.SbbLocalObject, and any subclass of this interface

  • javax.slee.EventContext

  • javax.slee.profile.ProfileLocalObject, and any subclass of this interface.

Examples

Below is an example of a datatype codec handling the serialization of class type Customer:

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import com.opencloud.rhino.cmp.codecs.DatatypeCodec;
import com.opencloud.rhino.cmp.codecs.DecoderUtils;
import com.opencloud.rhino.cmp.codecs.EncoderUtils;

public class CustomerCodec implements DatatypeCodec<Customer> {
    @Override
    public void encode(Customer value, DataOutput out, EncoderUtils utils) throws IOException {
        ...
    }

    @Override
    public Customer decode(DataInput in, ClassLoader classLoader, DecoderUtils utils) throws IOException {
        return ...
    }
}

The datatype codec can be associated directly with the Customer datatype by annotating the class itself as shown below:

@DatatypeCodecType(CustomerCodec.class)
public class Customer ...

Alternatively, a CMP field storing a Customer object can be annotated to declare the datatype codec to be used for that CMP field, as shown below:

public abstract class MySbb implements Sbb {
    ...

    @DatatypeCodecType(CustomerCodec.class)
    public abstract void setCustomer(Customer customer);
    public abstract Customer getCustomer();
}

The datatype codec can be equally used on a CMP field storing an array of Customer objects, as shown below:

public abstract class MySbb implements Sbb {
    ...

    @DatatypeCodecType(CustomerCodec.class)
    public abstract void setCustomers(Customer[] customers);
    public abstract Customer[] getCustomers();
}

Encodable context

There are a number of use cases where serialization and/or deserialization of a CMP object requires access to resources that the Encodable and DatatypeCodec encode and decode methods may not natively have access to. For example, deserialization of a stored network message may require access to the owning resource adaptor provider object to reconstruct the correct object graph. To solve this problem, Rhino defines the concept of an encodable context. An encodable context can provide access to the resources that the encode and decode methods need in order to fulfill their function.

Encodable context definition

The encodable context required for any given Encodable or DatatypeCodec type may be defined as either a class or interface, though it is strongly recommended that the context be defined as an interface. The reason for this is to allow the provider of the context to more easily combine contexts required by multiple Encodable or DatatypeCodec types used in different CMP fields into a single implementation object. An encodable context may provide read access to other CMP states, but should not provide write access to any CMP state. SLEE behaviour is undefined if arbitrary CMP fields are modified during the encode or decode of another CMP field. Otherwise, there are no specific requirements or restrictions about what an encodable context may provide access to.

Below is an example of an encodable context interface:

import org.jainslee.resources.diameter.ro.RoProviderFactory;

public interface RoProviderContext {
    public RoProviderFactory getRoProviderFactory();
    public Tracer getTracer();
    public int getSomeContextValue();
}

An Encodable or DatatypeCodec object obtains an encodable context object from the EncoderUtils argument passed to the encode method, or from the DecoderUtils argument passed to the deserialization constructor (for Encodables) or decode method (for DatatypeCodecs).

Encodable context provider

If an Encodable or DatatypeCodec type is a consumer of an encodable context, then there must be a corresponding provider of the context. Only SBBs currently support providing an encodable context object for use by CMP fields declared by itself or any dependent SBB part. An SBB sets the encodable context using the setEncodableContext method on its com.opencloud.rhino.slee.RhinoSbbContext object (a Rhino extension of javax.slee.SbbContext):

package com.opencloud.rhino.slee;

public interface RhinoSbbContext extends SbbContext {
    public <T> void setEncodableContext(T context)
        throws SLEEException;

    ...
}

The encodable context object set by the SBB must implement all encodable context types expected by the Encodable or DatatypeCodec types that it uses, so that each Encodable or DatatypeCodec type may typecast the object to the encodable context type that it expects. An encodable context object is scoped to an SBB object; therefore the recommended place to initialise the encodable context is in the SBB’s setSbbContext method.

Below is an example implementation of the RoProviderContext encodable context interface shown above:

import javax.naming.InitialContext;
import javax.slee.Sbb;
import javax.slee.SbbContext;
import com.opencloud.rhino.slee.RhinoSbbContext;
import org.jainslee.resources.diameter.ro.RoProviderFactory;

public abstract class MySbb implements Sbb {
    public void setSbbContext(SbbContext context) {
        RhinoSbbContext rhinoContext = (RhinoSbbContext)context;

        final RoProviderFactory roProviderFactory = new InitialContext().lookup(...);
        final Tracer encodableContextTracer = context.getTracer(...);
        rhinoContext.setEncodableContext(new RoProviderContext() {
            @Override
            public RoProviderFactory getRoProviderFactory() {
                // return provider factory from JNDI
                return roProviderFactory;
            }
            @Override
            public Tracer getTracer() {
                // return tracer for encode/decode methods to use
                return encodableContextTracer;
            }
            @Override
            public int getSomeContextValue() {
                // return value from SBB CMP
                return MySbb.this.getSomeContextValue();
            }
        });

        ....
    }

    // cmp field declaration
    public abstract void setSomeContextValue(int value);
    public abstract int getSomeContextValue();

    ...
}

Since other persistent entities, such as profiles and activity context interface objects, do not currently support the provision of an encodable context object, Encodable or DatatypeCodec types that require an encodable context cannot be used with these persistent entities. The getEncodableContext method defined in the EncoderUtils and DecoderUtils interfaces will always return null for these types of persistent entities.

Codecs for Java collection types

Rhino natively supports FastSerializable and Encodable types, and types using a DatatypeCodec, on array-type CMP fields. Each non-null array element is individually serialized and deserialized as appropriate. However there are times when using a Java Collections Framework type, such as List or Set in a CMP field, is preferable to using an array; but retaining the serialization benefits provided by the element type is desired.

To answer this, Rhino provides a set of base classes that provides a framework for efficient serialization of Encodable, List, Set, and Map types, and additional datatype codec annotations to simplify the common use cases.

Encodable collections

Rhino provides three base abstract classes to support efficiently serialized collections:

Each of these classes wraps an implementation of the corresponding collection type, and implements the Encodable contract to manage the serialization and deserialization of that collection. To use any of these as a CMP field type, the application developer must implement a concrete class extending from the relevant base class, taking into account the following rules and considerations.

Constructors

A concrete subclass must provide a public constructor satisfying the Encodable contract which delegates to the equivalent protected constructor in the base class. The subclass should also provide at least one general user constructor that delegates to one of the base class public constructors, such as the no-argument constructor. A subclass of EncodableList may also need to provide a constructor suitable for use with the implementation of the abstract newInstance method.

Implementation of writer / reader methods

A concrete subclass must provide the implementation of the abstract writer and reader methods defined by the base class. These methods are responsible for the encoding and decoding of individual collection elements, map keys, or map values, as appropriate.

Implementation of EncodableList newInstance method

The java.util.List interface includes a method, subList, which returns a view of a portion of the source list. The EncodableList requires that a subclass implements the newInstance method it defines to facilitate the implementation of this method. The newInstance method should return a new instance of the concrete class which wraps the list provided by the method argument. Delegating to the EncodableList constructor defined with the same arguments as the newInstance method is the recommended approach.

Managing null elements, keys, and values

The default implementation of EncodableList and EncodableSet assumes that null elements will not occur in the collection. The default implementation of EncodableMap assumes that null keys will not occur in the map, but that null values might. The consequence of this is that if a null element occurs in a list or set, or null key occurs in a map, then during serialization the corresponding writer method will be asked to encode the null object. While this is not problematic, it means that the writer and reader methods need to perform additional work to handle the presence of null objects in the stream they write to or read from.

To simplify the code required of the writer and reader methods when null objects are expected by a given datatype, a subclass may change the default behaviour of the base class by overriding the manageNullElements method in EncodableList and EncodableSet, or the manageNullKeys and manageNullValues methods in EncodableMap. If these methods return true, then the base class will check for null objects of the corresponding type and handle them internally, only invoking the writer and reader methods for non-null objects. If null objects are never expected, or not supported by the underlying backing store (or the writer and reader methods will handle null objects), then these methods may return false, resulting in a slightly smaller serialization data stream that doesn’t include the extra information required for null checks.

Below is an example of subclass of EncodableList that stores a list of strings with possible null elements:

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.List;
import com.opencloud.rhino.cmp.codecs.DecoderUtils;
import com.opencloud.rhino.cmp.codecs.EncoderUtils;
import com.opencloud.rhino.cmp.codecs.SimpleDatatypeCodecs;
import com.opencloud.rhino.util.EncodableList;

public class StringList extends EncodableList<String> {
    // general user constructor
    // since we implicitly delegate to the default superclass constructor,
    // the underlying collection will be an ArrayList
    public StringList() {}

    // constructor required by the Encodable contract
    public StringList(DataInput in, ClassLoader cl, DecoderUtils utils) throws IOException {
        super(in, cl, utils);
    }

    // constructor used by newInstance()
    protected StringList(BackingStore backingStore, List<String> list) {
        super(backingStore, list);
    }

    @Override
    protected StringList newInstance(BackingStore backingStore, List<String> list) {
        return new StringList(backingStore, list);
    }

    @Override
    protected void writeElement(String element, DataOutput out, EncoderUtils utils) throws IOException {
        CODEC.encode(element, out, utils);
    }

    @Override
    protected String readElement(DataInput in, ClassLoader cl, DecoderUtils utils) throws IOException {
        return CODEC.decode(in, cl, utils);
    }

    @Override
    protected boolean manageNullElements() {
        // null elements are possible
        // tell the superclass to deal with them so we don't have to
        return true;
    }


    // use predefined datatype codec for encode and decode
    private static final SimpleDatatypeCodecs.StringCodec CODEC = new SimpleDatatypeCodecs.StringCodec();
}

Below is an example of a CMP field declared in terms of this datatype:

public abstract void setNames(StringList names);
public abstract StringList getNames();

Datatype codecs for collections

In many use cases, the implementation of an Encodable collection type would look strikingly similar to the example shown in the previous section. Most of the implementation code is boilerplate, even more so if the writer and reader methods simply delegate to another datatype codec class. To simplify application development, Rhino provides an alternative through the use of annotations on the CMP field getter or setter method:

  • An EncodableList implementation can be substituted with a @ListCodecType annotation.

  • An EncodableSet implementation can be substituted with a @SetCodecType annotation.

  • An EncodableMap implementation can be substituted with a @MapCodecType annotation.

These annotations have only two minor restrictions:

  1. Element, key, and value encoding and decoding must be implemented using datatype codecs.

  2. Generic type parameters aside, an annotated CMP field must be declared in terms of the base collection interface type, that is: java.util.List, java.util.Set, or java.util.Map, or an array of any dimension of this type.

    • If the CMP field type is scalar, any implementation class of the declared CMP field type may be passed as a parameter to the CMP field setter method, for example: a java.util.ArrayList or java.util.HashSet. However, no assumption can be made about the type of object returned from the CMP field getter method, other than it implements the interface declared as the CMP field type. For example, if the CMP field type is java.util.List, a java.util.ArrayList may be passed to the CMP field setter method; but it cannot be assumed that a java.util.ArrayList will be returned from the CMP field getter method.

    • If the CMP field type is an array, any implementation class of the base component type of the declared CMP field array type may be passed as array elements. However, no assumption can be made about the type of objects returned as array elements from the CMP field getter method, other than they implement the interface declared as the array base component type.

Below is an example of how the StringList CMP field example from the previous section could be simplified by using the @ListCodecType annotation:

@ListCodecType(codec = SimpleDatatypeCodecs.StringCodec.class, manageNullElements = true)
public abstract void setNames(List<String> names);
public abstract List<String> getNames();

The annotations provide a similar level of configurability as can be achieved by manual implementation. For example, the underlying collection type and management of null objects can be specified as annotation parameters.

Datatype codecs and null objects

If a collection datatype codec annotation indicates that null objects should not be managed internally by Rhino, and null objects occur in the corresponding data set, then this is one specific case where the serializing datatype codec is expected to handle and manage the null value itself during serialization and deserialization. For simplicity reasons, it is highly recommended that Rhino be asked to manage null objects when they are expected through appropriate annotation parameters.

Predefined datatype codecs

Rhino provides two sets of predefined datatype codecs:

These predefined codecs can be particularly useful when using datatype codecs for collections that store simple types such as java.lang.Integer or java.lang.String.

Initial values

Often it is necessary, when a new persistent entity (such as an SBB entity) is created, to initialise various CMP fields to a value different than the standard Java default. For example: a CMP field storing a counter may need to be initialised to 1 instead of the default value of 0; a CMP field storing an array may need to be initialised to an empty array rather than default null; or a CMP field storing a list may need to be initialised to an empty list. The standard way to achieve this is to add the necessary CMP setter method invocations to the entity’s create lifecycle method; this is acceptable, but means that the initial value is separated from the CMP field declaration, making the code less obvious than it could be. Also this technique doesn’t work if, for example, an arbitrary CMP extension interface is linked in to the SBB — the SBB needs to know a priori what CMP fields need initialising — which means CMP fields in CMP extension interfaces dynamically added after compilation cannot be initialised this way.

To solve this problem, Rhino introduces an annotation that can be used to specify the initial value for a CMP field.

Initial value fields

The initial value for a CMP field can be declared by annotating either the CMP field getter or setter method with the @com.opencloud.rhino.cmp.InitialValueField annotation. The annotation references, either implicitly or explicitly by parameter, a class field that contains the initial value for the CMP field. This mechanism means that the initial value can be constructed by any legal Java means, and initial values for any arbitrarily complex CMP field type can be declared without issue.

If the annotation does not explicitly name a class field to use as the initial value, then a default name of _initial + the CMP field name, with the first letter capitalised, is assumed. For example, for a CMP field with the name foo, the default initial value field name is _initialFoo.

The initial value class field must:

  • be public, static, and final

  • be visible in the scope of the annotated CMP field accessor method

  • have a type that is assignable to the CMP field.

The @InitialValueField annotation may be used on:

  • an SBB abstract class CMP field getter or setter method

  • an SBB or SBB part CMP extension interface CMP field getter or setter method

  • a profile CMP interface CMP field getter or setter method.

It is illegal to use this annotation on both the getter and setter methods for the same CMP field, or to declare an initial value for the same CMP field multiple times. For example, if a given CMP field is declared both in an SBB abstract class and an SBB CMP extension interface, only one CMP field declaration may be annotated with an initial value declaration. It is legal, however, for example, for two separate SBB CMP extension interfaces to both extend from a common interface that declares a CMP field with an initial value, as the initial value has only been declared once by the parent interface.

The @InitialValueField annotation may not be used on a CMP field getter or setter method where the CMP field is one of the following types, or is an array of any dimension of one of the following types:

  • javax.slee.ActivityContextInterface, and any subclass of this interface

  • javax.slee.SbbLocalObject, and any subclass of this interface

  • javax.slee.EventContext

  • javax.slee.profile.ProfileLocalObject, and any subclass of this interface.

A error occurs at deployment time if any of the above constraints are violated.

SLEE behaviour is undefined if an object referenced by an initial value class field is mutated at any time. A newly created persistent entity containing the CMP field could be initialised with either the original initial value or the modified initial value at the discretion of the SLEE implementation. It is generally advised that initial values be immutable; though in some cases this will not be possible.

Examples

Below is an example where the initial value field is explicitly named by the annotation:

@InitialValueField("aValue")
public abstract void setIntValue(int value);
public abstract int getIntValue();
public static final int aValue = 42;

Below is an example where the initial value field is not explicitly specified, so the default field name is assumed:

@InitialValueField
public abstract void setListValue(List<String> value);
public abstract List<String> getListValue();
public static final List<String> _initialListValue = new ArrayList<String>(5);

Pass-by-reference

The JAIN SLEE 1.1 specification, as written, prescribes that CMP fields have pass-by-value semantics. This means that when an object value is stored in a CMP field, the SLEE will make and store a copy of that value, rather than store the original value. Similarly when a stored value is retrieved from a CMP field, the SLEE will name a copy of the stored value and return the copied value. The effect of this is that the stored object is unaffected by any subsequent changes made by the application to the original or retrieved object — a value retrieved from the CMP field will always have the exact same state as it did when stored in the CMP field.

Generally speaking, this is desired behaviour, and it makes application code easier to understand. However there are times when pass-by-value semantics get in the way of efficient programming, requiring additional coding to work around the limitations of these semantics. As an example, consider an application that needs to use some unchanging session state across multiple transactions. The correct SLEE technique here is to store the state in a CMP field, then retrieve that state in each transaction as required. Unfortunately the pass-by-value semantics mean that the application incurs the overhead of a stored value copy every time it retrieves the value from the CMP field, even though the stored value is never changed. A typical workaround to avoid this overhead is to:

  1. create an instance variable which caches a reference to the stored value

  2. add alternative getter and setter methods which check the cache first

  3. include code in the relevant lifecycle callback methods to clear any cached reference when appropriate…​

…​all of which is cumbersome, particularly when there are many CMP fields that require this treatment.

To simplify the application developer’s effort in situations like these, Rhino introduces the option to declare pass-by-reference semantics to CMP fields.

Reference scopes

Pass-by-reference may be declared with one of three reference scopes: TRANSACTIONAL, WHILE_READY, or PERMANENT.

The TRANSACTIONAL and WHILE_READY reference scopes operate in similar ways. An intermediate reference cache is used to store the CMP field value during a transaction. If the CMP field is written to, the cache simply stores the reference to the stored value. If the CMP field is read, then the reference stored by the cache is returned. The actual stored value is only written back to persistent storage, and thus a copy is made, when the transaction commits; and that value is only read back from persistent storage if the CMP field is read by the application and the cache has been invalidated. The difference in the two reference scopes lies in when the cache is invalidated. When using the TRANSACTIONAL reference scope, the reference cache is invalidated whenever a bounding transaction completes, either by commit or rollback. When using the WHILE_READY reference scope, the reference cache is invalidated only when the owning container object (such as an SBB object) leaves the READY state and is thus disassociated from the persistent entity, or transaction rollback occurs and the owning container object needs to resynchronise with persistent state.

The PERMANENT reference scope completely removes any pass-by-value semantics. Any value stored in the CMP field is stored by reference only. Unlike other reference scopes, a permanent reference loses all transactional semantics. Any changes made to the referenced object will persist irrespective of whether the current transaction commits or rolls back. As such, permanent references are best suited to objects that are unlikely to change during the lifetime of an application entity, for example: parts of input network messages that must be retained across event handler transaction boundaries, but serialization of those messages is undesirable.

Warning This reference scope should not be used in replicated SBBs, as object references naturally cannot extend beyond a single JVM boundary; the semantics of such a configuration are undefined.

Declaration

Pass-by-reference semantics are declared using the @com.opencloud.rhino.cmp.PassByReference annotation. This annotation may be used on:

  • an SBB abstract class CMP field getter or setter method

  • an SBB or SBB Part CMP extension interface CMP field getter or setter method

  • an SBB abstract class itself

  • an SBB or SBB part CMP extension interface.

If the annotation is used on a class or interface declaration, then its meaning is applied to all CMP fields declared in the class or interface, and also any subclass if the annotation’s inherited attribute is set to true. An individual CMP field may override any pass-by-reference semantics inherited from a class or interface annotation by being annotated itself with a different reference scope. The reference scope DISABLED can also be used in this case to remove any inherited pass-by-reference semantics from the CMP field.

It is illegal for a single CMP field’s getter and setter methods to be annotated with different reference scopes.

A @PassByReference annotation with WHILE_READY or PERMANENT scope may not be used on a CMP field getter or setter method where the CMP field is one of the following types, or is an array of any dimension of one of the following types:

  • javax.slee.ActivityContextInterface, and any subclass of this interface

  • javax.slee.SbbLocalObject, and any subclass of this interface

  • javax.slee.EventContext

  • javax.slee.profile.ProfileLocalObject, and any subclass of this interface.

Additional considerations

  • If pass-by-reference semantics are desired for a CMP field that holds, either directly or indirectly, a reference to a SLEE-defined object that is only valid in the transaction it was materialised (for example an SbbLocalObject, ProfileLocalObject, ActivityContextInterface, or EventContext), then the reference scope must be limited to TRANSACTIONAL to avoid runtime application failures. Note that if a CMP field directly storing one of these types inherits pass-by-reference semantics from a class or interface annotation, Rhino will automatically limit the scope to TRANSACTIONAL for that CMP field. This means it is not necessary to specifically annotate a CMP field directly storing one of these types if the inherited scope is wider — the correct scope will be used automatically.

  • The @PassByReference annotation naturally has no effect on CMP fields that store primitive types. Primitive types are always stored by value.

  • The @PassByReference annotation also has no effect on CMP fields that store types that Rhino understands to be immutable, such as java.lang.Integer (and other primitive type wrappers), java.lang.String, javax.slee.Address, and so on…​ nor any class that implements com.opencloud.util.Immutable. Immutable types are always stored by reference.

Examples

Below is an example of how a CMP field annotation declaring pass-by-reference semantics can be applied using the default (WHILE_READY) reference scope:

@PassByReference
public abstract void setValue(FooValue value);
public abstract FooValue getValue();

Below is an example of how a CMP field annotation declaring pass-by-reference semantics can be applied using the TRANSACTIONAL reference scope:

@PassByReference(scope = PassByReference.Scope.TRANSACTIONAL)
public abstract void setValue(FooValue value);
public abstract FooValue getValue();

Below is an example of how an SBB abstract class annotation declaring pass-by-reference semantics can be used to apply the semantics to all CMP fields, unless otherwise indicated, using the default (WHILE_READY) reference scope. These pass-by-reference semantics will also be inherited by any subclass of this SBB abstract class.

@PassByReference(inherited = true)
public abstract class MySbb implements Sbb {
    ...

    // this CMP field will automatically be demoted to use the TRANSACTIONAL reference scope
    public abstract void setSbbObject(SbbLocalObject sbb);
    public abstract SbbLocalObject getSbbObject();

    // this CMP field will revert to pass by value semantics
    @PassByReference(scope = PassByReference.Scope.DISABLED)
    public abstract void setValue(FooValue value);
    public abstract FooValue getValue();
}

CMP extension interfaces

The SLEE specification requires the SBB CMP fields be defined using abstract getter and setter methods. For example, the SBB CMP field named firstName of type java.lang.String must have the following method declarations in the SBB abstract class:

public abstract void setFirstName(String firstName);
public abstract String getFirstName();

Rhino allows SBB CMP fields alternatively to be defined in separate CMP extension interfaces. A CMP extension interface is simply an interface that declares methods related only to CMP fields. One use of a CMP extension interface is to allow an SBB to store additional state that may not be known when the SBB abstract class is developed. For example, the SBB build process may allow additional components to be "plugged in" to the base SBB, each of which may require its own CMP state. The plug-in components can define the CMP state they need using CMP extension interfaces; then these interfaces can be declared in the SBB deployment descriptor when the SBB is packaged.

CMP extension interfaces are also the sole mechanism by which an SBB part component can define CMP fields.

The firstName CMP field example above could be defined in a CMP extension interface as shown below:

public interface MySbbCMPInterface {
    public void setFirstName(String firstName);
    public String getFirstName();

    ...
}

A CMP extension interface may also optionally include has and reset methods for each CMP field.

  • The has method determines if a value for the CMP field is present. Primitive types always have a value, so this method will always return true for a CMP field of a primitive type. For CMP fields storing object types, this method returns true if the CMP field has been assigned a non-null value. While this method is generally for convenience only, a has method can offer the potential for better-performing code when only the presence of a value in the CMP field is required to be known, as the implementation does not necessarily require the stored CMP field value to be deserialized in order to test for a non-null value.

  • The reset method resets the CMP field value to its initial value. The initial value is determined by an @InitialValueField annotation, if present. Otherwise, the Java-defined default initial value for the field type is used; for example: 0 for numeric primitives, null for object references.

Adding the has and reset methods to the MySbbCMPInterface example shown above results in the interface shown below:

public interface MySbbCMPInterface {
    public void setFirstName(String firstName);
    public String getFirstName();

    // returns true if firstName has an assigned non-null value
    // effectively equivalent to: getFirstName() != null
    public boolean hasFirstName();

    // effectively equivalent to: setFirstName(null)
    public void resetFirstName();

    ...
}

Below is an example of a how the reset method is influenced by an @InitialValueField annotation:

public interface MySecondSbbCMPInterface {
    @InitialValueField
    public void setNames(String[] names);
    public String[] getNames();
    public static final String[] _initialNames = new String[0];

    // effectively equivalent to: setNames(_initialNames)
    public void resetNames();

    ...
}

A CMP extension interface must be public, and optionally may extend the com.opencloud.rhino.cmp.CMPFields interface. A single SBB or SBB part may declare as many CMP extension interfaces as desired. An SBB declares its CMP extension interfaces in the oc-sbb-jar.xml extension deployment descriptor, while an SBB part declares its CMP extension interfaces in the oc-sbb-part-jar.xml deployment descriptor. Since a CMP extension interface defines only CMP fields, an SBB deployment descriptor does not need to specify <cmp-field> elements for any CMP field defined only in a CMP extension interface. The CMP fields will be determined by class introspection.

All methods defined in a CMP extension interface are mandatory transactional methods. If they are invoked without a valid transaction context, a javax.slee.TransactionRequiredLocalException will be thrown. In addition, these methods may only be invoked on an SBB or SBB part object that has been assigned to an SBB entity, or is in the process of being assigned to an SBB entity using the sbbCreate method. If the SBB or SBB part object is not assigned to an SBB entity (with the exclusion of the sbbCreate method), a java.lang.IllegalStateException is thrown.

An SBB or SBB part obtains access to the CMP fields defined in CMP extension interfaces using a com.opencloud.rhino.cmp.CMPFields object. The CMPFields object may be typecast to any CMP extension interface declared by the SBB or SBB part, regardless of whether or not the CMP extension interface extends the CMPFields interface, thus exposing the CMP field accessor methods defined by the interface.

The CMP fields defined by an SBB in its SBB abstract class, in any CMP extension interfaces, and in any CMP extension interfaces used by dependent SBB parts, all share the same namespace. As such, if the same CMP field is defined in multiple places, for example in the SBB abstract class and in a CMP extension interface, then it must be declared with the same type. All these CMP accessor methods will refer to the same underlying SBB CMP field.

Arbitrary CMP fields

There are sometimes occasions when an SBB needs to store arbitrary CMP state which cannot be predetermined at development time. For example, arbitrary session state could be created in response to interactions with some other network element. The typical response is to define a CMP field that stores a map and store the session state in the map as key, value pairs. This is an acceptable solution; however, serialization of the stored map value can be a performance hit if not explicitly managed; and even then a change to any mapped value requires serialization of the entire map as it is read from CMP, updated, then rewritten to CMP.

To help in these situations, Rhino allows an SBB or SBB part to optionally support arbitrary CMP fields. Arbitrary CMP fields are simply CMP fields that have not been explicitly defined in the SBB abstract class or any CMP extension interface. Support for arbitrary CMP fields is disabled by default but can be enabled using the arbitrary-cmp-fields-allowed attribute in the oc-sbb-jar.xml SBB extension deployment descriptor or oc-sbb-part-jar.xml SBB part deployment descriptor. Note that if an SBB has a dependency on an SBB part that has enabled support for arbitrary CMP fields, then support for arbitrary CMP fields extends back to the SBB and all its dependent SBB parts. It is not possible for an SBB or SBB part to override a positive setting of this support from some other dependent SBB part (or the SBB itself in the case of an SBB part).

Arbitrary CMP fields have only a few basic rules and restrictions:

  • Arbitrary CMP fields must have a non-null name. Any non-null name is valid, including a zero-length name.

  • An arbitrary CMP field cannot have the same name as a CMP field predefined in the SBB abstract class or any CMP extension interface.

  • An arbitrary CMP field is only deemed to exist if it has an assigned non-null value. An existing arbitrary CMP field ceases to exist if assigned a null value.

  • Any non-null value assigned to an arbitrary CMP field must be serializable using standard Java serialization. FastSerializable types are also supported. Encodable types and those annotated with @DatatypeCodec, however, are not currently supported. FastSerializable should be used instead where possible.

  • Arbitrary CMP fields always exhibit pass-by-value semantics as per standard JAIN SLEE-defined CMP field behaviour. Pass-by-reference is not supported for these CMP fields.

Arbitrary CMP fields are accessed and managed using a CMPFields object.

The CMPFields interface

The CMPFields interface provides a means to access CMP fields indirectly by name, access metadata about CMP fields, and obtain information about and determine which CMP fields currently have a value. Arbitrary CMP fields are also managed using a CMPFields object.

The CMPFields interface is shown below:

package com.opencloud.rhino.cmp;

public interface CMPFields {
    public <T> T get(String name)
        throws NullPointerException, UnrecognisedFieldNameException, ClassCastException;
    public <T> void set(String name, T value)
        throws NullPointerException, UnrecognisedFieldNameException, ClassCastException;
    public boolean has(String name)
        throws NullPointerException, UnrecognisedFieldNameException;
    public void reset(String name)
        throws NullPointerException, UnrecognisedFieldNameException;
    public void reset();
    public Class<?> typeOf(String name)
        throws NullPointerException, UnrecognisedFieldNameException;
    public boolean isPredefined(String name)
        throws NullPointerException;
    public Set<String> predefinedNames();
    public Set<String> keySet();
    public Set<Map.Entry<String,Object>> entrySet();
}

The get, set, has, and parameterised reset methods allow any CMP field to be accessed by name parameter. These methods provide an alternative access mechanism for CMP fields predefined in the SBB abstract class or a CMP extension interface. These methods also allow arbitrary CMP fields to be created and managed if support for this feature has been enabled. Predefined and arbitrary CMP fields all share the same namespace. If any of these methods are invoked with a name parameter that matches a predefined CMP field name, then the name refers to the predefined CMP field; otherwise it refers to an arbitrary CMP field. These methods throw an UnrecognisedFieldNameException if the name refers to an arbitrary CMP field, but support for arbitrary CMP fields has not been enabled for the SBB.

When any of these methods are used to access a predefined CMP field, the invocation is equivalent to the corresponding CMP field method declaration. The example below illustrates this point:

public abstract class MySbb implements Sbb {
    public abstract void setFirstName(String firstName);
    public abstract String getFirstName();

    public void someMethod() {
        CMPFields cmpFields = ...

        // set CMP field to "Alice"
        setFirstName("Alice");

        // returns "Alice"
        String firstName = cmpFields.get("firstName");

        // set CMP field to "Bob"
        cmpFields.set("firstName", "Bob");

        // returns "Bob"
        firstName = getFirstName();

        // as the CMP field currently contains a value,
        // this will reset the CMP field to its initial value null
        if (cmpFields.has("firstName") {
            cmpFields.reset("firstName");
        }

        // returns null
        firstName = getFirstName();

        ...
    }

    ...
}

The unparameterised reset method resets all predefined CMP fields to their initial value and removes all arbitrary CMP fields that exist. This method effectively returns the SBB CMP state to as it was when the SBB entity was first created.

The typeOf method returns the Java class type of the named CMP field. For a predefined CMP field the return value is equal to the class used in its CMP field method declarations. For an arbitrary CMP field, this method returns the class of the value stored in the CMP field. The arbitrary CMP field must exist for this method to return a successful result, otherwise an UnrecognisedFieldNameException is thrown.

The isPredefined method returns true if the name corresponds with a predefined CMP field, and false otherwise. The predefinedNames method returns a set containing the names of all predefined CMP fields.

The keySet method returns a set containing the names of all CMP fields that currently have a value. The entrySet method returns a map containing the names and values of all CMP fields that currently have a value. The CMP fields that are deemed to have a value include:

  • any predefined CMP field of a primitive type

  • any predefined CMP field of an object type that currently has a non-null value

  • any arbitrary CMP field that currently exists.

All methods defined in the CMPFields interface are mandatory transactional methods. If they are invoked without a valid transaction context, a javax.slee.TransactionRequiredLocalException will be thrown. In addition, these methods may only be invoked on an SBB or SBB part object that has been assigned to an SBB entity, or is in the process of being assigned to an SBB entity using the sbbCreate method. If the SBB or SBB part object is not assigned to an SBB entity (with the exclusion of the sbbCreate method), a java.lang.IllegalStateException is thrown.

CMPFields object

An SBB obtains a CMPFields object from its com.opencloud.rhino.slee.RhinoSbbContext object (a Rhino extension of javax.slee.SbbContext). An SBB part obtains a CMPFields object from its com.opencloud.rhino.slee.sbbpart.SbbPartContext object.

A CMPFields object may be typecast to any CMP extension interface declared by the SBB or any of its dependent SBB parts, regardless of whether or not the CMP extension interface extends the com.opencloud.rhino.cmp.CMPFields interface.

Previous page Next page