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.
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.
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:
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:
inMethod
flag is set.inMethod
flag to true
.false
.
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.
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:
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.
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.
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.
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.
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.