CarrierWave: Overview

Introduction

CarrierWave, in part, is an implementation of the Value Object or Transfer Object EJB patterns. It provides a means to identify first class business objects on the server, tools to parse those objects and generate type equivalent client side objects, and a runtime interface to allow clients to select, modify, delete, and invoke server side object graphs described (optionally) by graph plans.

For example:

Instead of adding a new method to an EJB Session Bean to retrieve the data associated with a Person object, stored in a PersonStruct value object. CarrierWave can select the remote Person object and any required child object values with the following code: 

ClientSession clientSession = ClientSession.createClientSession( null );
QueryClient queryClient = clientSession.getQueryClient();
GraphPlan graphPlan = new GraphPlan( 1, true ); // the root plus its immediate children as references (Icons)
PersonFinderImage finder = new PersonFinderImage( "firstname", "lastname" );
PersonImage person = (PersonImage) queryClient.selectImageGraphWith( graphPlan, finder );

The code above accomplishes the following:

Where depth is the number of edges that should be included in the resulting graph closure. Thus a depth of 0 returns only the target object. A depth of 1 returns the target object and its immediate children. When isFuzzy is true, the leaf nodes of the graph are returned as references (Icon instances), not as value objects (Image instances).

One of the benefits to this approach is that the caller can decide how much information to retrieve, in this case by specifying the depth. The converse of this would be to add a new getPersonByName() finder method to the EJB Home interface. When client and server development teams are working independently, this is a major benefit allowing server developers to create parameterized finder objects, and client developers who have more flexibility in using them.

CarrierWave consists of the following components: 

Image/Imageable Object Model

Server side business objects are "Imageable", and client side objects are "Images" of the server "Imageable" types. 

Graphs of Imageable objects are owned (identified and managed) by the server container, the server container is the business object namespace. Graphs of Image objects are owned by their parent ImageGraph object, and there can be many ImageGraph instances in existence in a single client, each with a unique set of Image instances.

      

"Imageable" business object types implement the Imageable interface. "Image" types are generated (by the code generator) as subclasses the Image class. That is, for every business object Imageable class, there is a client side Image class.

Imageable types have both logic and state. Image types reflect only the state and semantics of its prototype Imageable type. The only logic in an Image type is applied to maintaining the integrity of the ImageGraph and its owned set of Image instances. That is, the sole purpose of an Image is to facilitate the transfer of data between the server and client and back to the server, where the data consists of literal values like int, long, Integer, and String types, and relationships between Imageable types.

Image instances retrieved from the server share the identity of the Imageable instance it was sourced from. The identity is encapsulated in an Icon object. This Icon object can be used to identify a particular Image instance (in any ImageGraph) or a particular Imageable instance (in the server namespace).

Image instances created on the client have no identity in relation to the server container, or to any other ImageGraph other than the one they are associated with. When the locally created Image instance is sent to the server (via a "modify" call), a new Imageable instance will be created and sourced with the values and relationships the Image instance was initialized with on the client.

The code generator creates one Image subclass for every Imageable type it finds in the server source path. Yes, currently source code is required at build time in order to generate the client Image types. Note that the Imageable type hierarchy is mirrored in the Image subclass type hierarchy.

To make the meta-model a little more expressive, the Imageable interface is not alone. The ImageableIdentifiable interface identifies business object types that have identity, that they are not dependent types. In terms of the persistence provider, they have a unique id. Imageable types by default are not identifiable, and should implement the ImageableIdentifiable interface if the business object has an identity. Note that ImageableIdentifiable objects are created and destroyed via delegation to the persistence interface (PersistenceRoot) so they can be given identity and registered/de-registered from the persistence provider.

Discussed below are finder types, the server side implementation of a finder implements either ImageableObjectFinder or ImageableCollectionFinder interfaces (both sub interfaces of ImageableFinder). The client side Image types will implement ObjectFinder or CollectionFinder, which are accepted on the QueryClient finder select methods.

Also, the ability to "invoke" an object graph is delegated to server classes that implement ImageableAction and its invokeAction() method. The equivalent Image type implements the Action interface and acts solely as a "parameter" object. During an "invoke" call, the action Image is sent to the server, an Imageable version of the action Image is created (it is not identifiable, so it is not persisted) with the passed parameters, and then invoked. The return value (or value graph) is returned to the client.

For a more detailed descriptions, see the Object Model PDF file.

Code Generator

The CarrierWave code generator is an Ant taskdef. 

It parses all the given Java source files and looks for classes that implement the Imageable interface. When found, a similarly named Image class is generated in the specified package, and an interface is (optionally, but by default) generated that declares all the setters/getters on the Image sub-class.

The parser by default looks for declared fields on the Imageable classes, and creates comparable fields on the result Image class. The parse also looks for @image javadoc tags in the class and field declarations. The @image tags provide a means to create "virtual" fields on the Image classes. 

For example:

An Imageable business object.

/**
 *
 * @image-field mother example.package.Person
 * @image-read-only mother
 */
public class Person implements ImageableIdentifiable
  {
  String firstName;
  String lastName;
  Person spouse;

  /**
   * @image-contained-type example.package.Person
   */
  List children;

  public void setFirstName( String firstName )
    {
    this.firstName = firstName;
    }

  public String getFirstName()
    {
    return firstName;
    }	

  public void setLastName( String lastName )
    {
    this.lastName = lastName;
    }

  public String getLastName()
    {
    return lastName;
    }	

  public void setSpouse( Person spouse )
    {
    this.spouse = spouse;
    }

  public Person getSpouse()
    {
    return spouse;
    }

  public Person getMother()
    {
    // some code that dynamically finds the mother, possibly via a Finder object
    }

  public void setChildren( List children )
    {
    this.children = children;
    }

  public List getChildren()
    {
    if( children == null )
      children = new ArrayList(); // must never return null

    return children;  
    }
  }
An Image of the above business object.
public class PersonImage extends Image implements example.generated.Person
  {
  String firstName;
  String lastName;
  Icon spouse;
  List children; // really an instance of ImageArrayList
  Icon mother;

  public void setFirstName( String firstName )
   {
  	this.firstName = firstName;
   }

  public String getFirstName()
    {
    return firstName;
    }	

  public void setLastName( String lastName )
    {
    this.lastName = lastName;
    }

  public String getLastName()
    {
    return lastName;
    }	

  public void setSpouse( Person spouse )
    {
    // adds the PersonImage to the current ImageGraph and sets the spouse Icon value
    }

  public Person getSpouse()
    {
    // uses the spouse Icon value and looks up the PersonImage from the current ImageGraph
    }

  public void setSpouseIcon( Icon spouse )
    {
    // sets the spouse Icon value
    }

  public Icon getSpouseIcon()
    {
    // returns the spouse Icon value
    }

  public void setMother( Person mother )
    {
    // adds the PersonImage to the current ImageGraph and sets the mother Icon value
    // note that this relationship is readonly and any relationship changes will be ignored during
    // a "modify"
    }

  public List getChildren()
    {
    // returns a instance of ImageArrayList
    // the ImageArrayList looks up the Image instance from the current ImageGraph on demand
    // or associates additions with the current ImageGraph
    // note there is not setChildren(), you must get the collection and modify it (add/remove/clear/etc)
    }

  public List getChildrenIcons()
    {
    // returns an ArrayList of Icons
    }

  public Person getMother()
    {
    // uses the mother Icon value and looks up the PersonImage from the current ImageGraph
    }

  public void setMotherIcon( Icon mother )
    {
    // sets the mother Icon value
    // note that this relationship is readonly and any relationship changes will be ignored during
    // a "modify"
    }

  public Icon getMotherIcon()
    {
    // returns the mother Icon value
    }
  }

Of course Maps are also supported.

The Imageable class javadoc has more information on the @image tags supported.

Graph Plans

Graph plans are objects that describe the closure of an object graph. They can be initialized with simple meta-data that describes an object graph in generic terms, or they can be initialized with a graph node tree (a tree of GraphNode instances). 

For example,

GraphPlan graphPlan = new GraphPlan( 2, true, true); // depth, ignoreReadOnly, isFuzzy

The above graph plan, when used in a "select" call, will return an object graph that includes two traversed edges from the origin object, but will not traverse read-only edges, and the leaf objects will be returned as references (Icons) instead of as objects (Images).

If you have want to make a "select" call that only returns a Person and their spouse (see Person declaration above), but not the collection of children, you could create a GraphPlan like this.

GraphNode originNode = new GraphNode( PersonImage.class );
GraphNode spouseNode = new GraphNode( PersonImage.class, "spouse" );
originNode.addChildNode( spouseNode );
GraphPlan graphPlan = new GraphPlan( originNode );

Or if you want the children, and not the spouse.

GraphNode originNode = new GraphNode( PersonImage.class );
GraphNode childrenNode = new GraphNode( PersonImage.class, "children[]" ); // identifies the whole collection
originNode.addChildNode( childrenNode );
GraphPlan graphPlan = new GraphPlan( originNode );

Or if you want only the first child. 

GraphNode originNode = new GraphNode( PersonImage.class );
GraphNode childNode = new GraphNode( PersonImage.class, "children[0]" ); // identifies the first element
originNode.addChildNode( childNode );
GraphPlan graphPlan = new GraphPlan( originNode );

Note that list ordering may not be guaranteed by the persistence provider (unless its sorted). In those cases, if the children where held in a Map, keyed by their first name (for some reason).

GraphNode originNode = new GraphNode( PersonImage.class );
GraphNode childNode = new GraphNode( PersonImage.class, "children<firstname>" ); // element with the key "firstname"
originNode.addChildNode( childNode );
GraphPlan graphPlan = new GraphPlan( originNode );

The "[]" at the end of an edge says the edge references a List. The "<>" at the end of the edge says the edge references a Map. Any values inside the "[]" or "<>" specified the index or key, respectively.

GraphPlan instances can, and should, be persisted via the repository.

Client Interface

The client side interface consists of three classes; ClientSession, QueryClient, and RepositoryClient.

The ClientSession class act as a factory for creating instances ClientSession instances. The default ClientSession created is the EJBClientSession, this of course requires CarrierWave to be deployed in an EJB server. The LocalClientSession can be used if CarrierWave is to be deployed in a stand alone JSP server. But by simply switching from LocalClientSession to EJBClientSession, you can amortize your JSP client code over numerous machines, all focused on one CarrierWave server. No other client code needs to be changed.

QueryClient provides all the "select", "modify", "delete", and "invoke" graph methods to the server.

RepositoryClient provides a simple interface to the GraphPlan repository, when used.

The javadoc for these classes should provide ample information regarding the functionality they provide.

Graph Manager Classes

The graph manager classes (couldn't think of a snazzy name) are where the core graph management functionality lives. All "select" calls live in the Select class, "modify" calls in the Modify class, "delete" calls in the Delete class, and "invoke" calls in the Invoke class (all in the com.vinculumtech.carrierwave.manager package).

The QueryClient class is simply the client interface to these server side classes. A ClientSession subclass provides the middleware abstraction to the server from the client.

The javadoc for these classes should provide ample information regarding the functionality they provide.

Persistence

CarrierWave is coupled to a persistence provider through implementations of the PersistenceSession class, instantiated and managed by the PersistenceRoot class.

The default (and very simple) InMemoryPersistenceSession class is a quick and dirty implementation that stores all objects in memory, and "commits" any changes to the disk (via serialization) when a read-write transaction is committed. Of course this implementation isn't very robust, but is thread safe. It allows for many concurrent read transactions, and only one write transactions at any given moment.

Finders are supported through the OGNL property navigation syntax. This gives developers the ability to prototype client side queries against the InMemoryPersistenceSession and against any other persistence provider.

The JDO persistence session provides an interface to any JDO implementation and any specified schema files.

Note only lookup and life cycle concerns related with ImageableIdentifiable types are delegated to the PersistenceRoot (and the current PersistenceSession instance).

Repository

The repository is a component of the architecture where graph plans are stored and shared between the clients and server. By default, the repository is in the CarrierWave server process space, but it could live anywhere. The server side interface to the repository is through the GraphPlanRoot class (in the com.vinculumtech.carrierwave.manager package).

The RepositoryClient class is the client interface to the server side GraphPlanRoot class. But there are hooks allowing other repository sources to be added on the client. But this gets into an advanced topic pre-maturely.

Note that when graph plans are used, as little coupling to them as possible should be a primary goal. The repository allows for graph plans to be named (and versioned), and shared. Subsequently, the client should only need to pass the graph plan name to the server, instead of the GraphPlan instance. External tools should be used to create and maintain graph plans, not deployed client applications.

Last Modified: 09/22/2003