Introduction

Wrappers are a mechanism to implement extensions to the core graph model. The basic idea is that each GraphEntity and GraphEntityClass may be encapsulated by another object of the same type that implements the same outside interface, but modifies the behaviour of the methods (performing additional checks or actions before calling the actual method of the wrapped object, or maybe even completely replacing the call another implementation). Thus, from an architectural point of view, wrappers (or more precisely, extensions using wrappers) build upon the core GM, but are below the level of the application GM.

Realisation

To make wrappers easy to use and easy to implement, they are explicitly supported by the core GM. A GraphPool can be configured to use a wrapper, and all GraphEntities and GraphEntityClasses returned afterwards will be automatically wrapped. Also, when using GraphEntities or GraphEntityClasses as parameters, they will be automatically unwrapped if necessary. The core GM comes with two wrapper implementations: NullWrapper, which is the default, and returns any arguments unchanged, and MultiWrapper, which calls any number of additional wrappers in sequence to allow arbitrary combinations of wrappers.

The Wrapper Hierachy Contract

To avoid undesired and unpredictable side effects, there is a very important, yet simple rule:

A class may only call methods on the same level or the level below in the wrapping hierachy!

If you need an example to see why this is important, just consider that some wrappers use additional graph entities to store their internal data, and hide these from the outside. Now what would happen if the underlying database wants to delete the element and all entities connected to it, but getAllEdges() would hide some of the edges, so they can not be found and deleted? Chaos!

This means that a wrapping class may only call methods implemented by the same wrapper and associated classes (e.g. ExampleWrappingNode), or on the wrapped elements. A Core GM implementation may only call methods on completely unwrapped elements.

There are two cases where this rule is accidentaly and easily violated:

  1. Return values from methods like getEdgesOfGraph() are completely wrapped (as required by the specification), so you have to unwrap them before calling any methods on them!
  2. Method parameters the wrapping classes receive are usually wrapped, too.
How to deal with these problems?
  1. In the Core GM implementation, if you use WrapperBooster correctly (which means using it everywhere, and never using the simple Wrapper!), you can simply call WrapperBooster.deactivateWrapping() before calling such a method. Just remember to reactivate it afterwards!

    For extensions, the UnwrapTool provides a number of convenient methods that perform unwrapping until an instance of a specific class is found, which should uniquely identify a wrapping level.
  2. Here the UnwrapTool is helpful once more. Make it a habit to unwrap any parameter (down to the desired level) at the beginning of each method! When calling methods on the wrapped entity, you can give it the unwrapped parameter (maybe even remove the layer representing the current wrapping level), since at some lower layer the parameter will be unwrapped anyway. In fact, it is a good idea to only supply parameters with the minimal wrapping, since it reduces the ammount of necessary unwrapping further down and also reduces the possibility of violations of this contract by the wrapped layer manifesting themselves as errors.

Wether an extension / Core GM implementation confirms to this contract could be unit-tested using a special Wrapper. It would have a boolean inMethod flag (initially set to false shared among all instances of wrapping elements. And all methods defined in the core GM would be implemented as follows:

  1. Fail if inMethod flag is set.
  2. Set an inMethod flag to true.
  3. Perform the method call on the wrapped entity / entity class / attribute.
  4. Reset the flag to false.
Now the idea is of course that every illegal method call from a lower layer would find the flag set to true and thus be detected. In other words, we are forcing the wrapper (which here means "all wrapping classes living at that level in the wrapping hierachy") to be non-reentrant. Given a large enough number of operations with different data, this should provide a good coverage.

Guide for Users

To use an extension implenented as a wrapper, simply call myGraphPool.setWrapper(myWrapper) during the initialization phase. From that moment on, the wrapper will be activated. Some extensions may require additional setup, see the extension's documentation for details. If you need to access additional methods of the wrapping classes, take a look at UnwrapTool.unwrapGraphEntity(Class)Till methods.

Using more than one wrapper at once

There might be situations were you want to use more than one wrapper at once, e.g. if you want to use dragos-ext-autocommit and another extension. As mentioned above, support for this already exists in the form of MultiWrapper. MultiWrapper takes a Wrapper[] array as argument to the constructor. Whenever MultiWrapper is invoked, the first wrapper in the array is called to wrap the original entity / class, all following wrappers each receive the output of the predecessor as argument. The output of the last wrapper in the array is then returned by the MultiWrapper. This means the order of wrappers is important and should be carefully considered. See Best Practices for some more information on this topic.

An Example: Let us assume you want to use dragos-ext-autocommit and dragos-ext-attreval at the same time, which are both implemented as wrappers (without any additional components, e.g. a custom GraphPool encapsulation). As explained in Best Practices, the AutoCommitWrapper must be below AttrEvalWrapper in the hierachy. So the architecture we aim for is as follows:

  • Application Graph Model
  • dragos-ext-attreval
  • dragos-ext-autocommit
  • Core Graph Model

How do we set up the MultiWrapper? As explained above, the array goes from innermost to outermost layer, so the correct order would be:

       // create MultiWrapper
       Wrapper[] myWrappers = new Wrapper[2];
       myWrappers[0] = new AutoCommitWrapper();
       myWrappers[1] = new AttrEvalWrapper();
       MultiWrapper mw = new new MultiWrapper(myWrappers);
       // tell the GraphPool to use this new MultiWrapper
       myGraphPool.setWrapper(mw);
      
From that moment on, any graph entities, graph entity classes and attributes returned by the GraphPool will be wrapped accordingly, and thus support dynamic attributes and the auto-commit feature.

Accessing additional methods

The wrapping classes may define additional methods beyond those in the core interfaces (Node, NodeClass, Edge...).

Accessing one of these additional methods is easy when the wrapper in question is the only one or the last one in the chain created by MultiWrapper: You just have to perform a class cast to the specific class, e.g.

       ExtendedGraph g = (ExtendedGraph) myGraphPool.getTopLevelGraph("ExtendedGraphA");
      
Then you can call all methods defined by the ExtendedGraph class.

However, as soon as you need to access extensions of classes buried deeper in the wrapper layers, this approach does not work anymore. Let us consider CustomNode from the MultiWrapper example above, which is "hidden" by AutoCommitNode. The UnwrapTool.unwrapGraphEntity(Class)Till methods were written just for cases like this one. They remove all layers up to a certain point. To continue the example, we could call:

       CustomNode cn = UnwrapTool.unwrapGraphEntityTill(returnedNode, CustomNode.class);
      
UnwrapTool would remove layer after layer, until it finds an instance of CustomNode (which would happen during the second iteration in this example). Now we could call any new methods defined in CustomNode.

However, there is one thing to keep in mind: After the unwrapping, the outer layers are not active anymore, so in this case, the functionality of dragos-ext-autocommit would not be available when you call methods on cn! Sometimes, this does not cause any problems (e.g. if the new methods in CustomNode handle transactions themselves). Sometimes you can work around this by re-ordering the wrappers in the array given to the MultiWrapper constructor. If all this fails, you may have to modify one of the wrappers and merge the functionality of another wrapper into it, or explore other options.

It is highly recommended to use the UnwrapTool.unwrapGraphEntity(Class)Till methods instead of the direct casting approach described first, even if the wrapper layer in question is the outmost one. If you add another layer in the future, you would have to change all those direct castings to use UnwrapTool, and any casting you miss will not be detected by the compiler, but result in a runtime exception, possibly crashing your application. None of this would happen if you had used UnwrapTool in the first place, and the few more characters of code as well as the performance penalty are neglible in contrast.

Guide for Wrapper Authors

First, you should take a look at dragos-ext-autocommit, which is implemented using wrappers and should be quite easy to understand and use as a starting point for your own work. At the very least, you have to write a class that implements the Wrapper interface, NullWrapper might be a good template here. In most cases, you will also want to write some classes that actually wrap some of the core GM classes. It is highly recommended to keep the class hierachy, e.g. have a WrappingNode that extends WrappingGraphEntity. The wrapping objects will be created and discarded as needed; if you need to store data permanently, set MetaAttributes on the wrapped object.

Before you define any additional method beyond the core interfaces, please consider if it could not be simulated using a (possibly dynamic) MetaAttribute. This makes combining multiple wrappers much more convenient for the user, because he does not need to perform partial unwrapping and because of this he also does not need to put as much consideration into the ordering of the wrappers.

By design, there is one limitations to the wrapper approach for implementing extensions: You can not wrap GraphPool (or Schema) itself. There are several reasons for this: First, wrapping of the GraphPool would have to happen in the GraphPoolFactory, which is a different area of the DRAGOS system, and would thus not fit well with the wrapper approach as it is now. Second, automatic wrapping and unwrapping mostly makes sense when dealing with a large number of objects in many different places. The GraphPool in contrast is usually retrieved only once and cached, and it takes even less lines of code to replace

       GraphPool myGraphPool = GraphPoolFactory.get(dataSourceURL);
      
with something like
       GraphPool myGraphPool = new CustomGraphPoolWrapper(GraphPoolFactory.get(dataSourceURL));
      
than to set up a wrapper with GraphPoolFactory first and then retrieve the automatically wrapped GraphPool.

Guide for PEGS Implementers

If you implement a PEGS storage back-end for the core GM, you will have to perform all the actual work that makes live easier for users and wrapper authors. But do not despair, help is here. There are two things you have to do:

First, make sure that any GraphEntity or GraphEntityClass you return (directly or inside a Collection) is wrapped according to the current configuration. That is not very hard, just call getWrapper().wrapXY(myReturnValue). Be careful not to wrap twice or more when using helper methods!

Second, you have to unwrap any GraphEntity or GraphEntityClass parameter coming your way. UnwrapTool makes this very easy, just call param = UnwrapTool.unwrapXY(param) at the beginning of every method for every possibly affected parameter. After that, proceed as usual.

Proxies - a special type of Wrappers

Note: You can safely ignore this section, but it might be of interest if you are writing a data storage back-end.

During development of the JDO-backed Core GM implementation, we found the need to perform wrapping already at implementation-level (in this case to allow certain method calls like getDataSourceURL() even after an object has been undeclared / deleted). Of course, it made sense to re-use the Wrapper mechanism; but on the other hand, we wanted to hide this from the outside and prevent accidental unwrapping. This is why we introduced proxies, which can be found in the i3.dragos.gm.core.proxies package.

Proxies implement the Core GM interfaces, and ProxyWrapper implements the Wrapper interface, so wrapping works just like for standard wrappers. The methods for unwrapping however are named differently (getProxyGraphEntity(), getProxyGraphEntityClass() and getProxyAttribute()) for the reasons described above.

If you need to employ proxies in your Core GM implementation, you should first wrap any returned objects using proxies (either the supplied ones or subclasses extended for your specific needs) before wrapping using the application level Wrapper specified by GraphPool.setWrapper(Wrapper). WrapperBooster provides (besides other features) facilities for performing both wrapping steps with a single method call.