JGT Tutorial
Probably the best way to learn how to use JGT is to look at the example code.  Its provided as a suite of JUnit tests, located in the directory tests/net/sourceforge/jgt/examples.  The tutorial will dicuss the code in these tests.

To get started, here is a very simple example that does nothing more than copy a property from one object to another:
public void testSimple_ForwardMapping() throws Exception {
        //Create two different object graphs
        PersonA roota = new PersonA();
        PersonB rootb = new PersonB();
        roota.setName("Tom");
        rootb.setName("Jerry");

        /**
         * Create configuration data for the transformation.  We state we want to use the "SimpleRuleSet"
         * transformation rule set.  This rule set must be implemented as an XML file
         * called "SimpleRuleSet.jelly"
         */
        TransformationContext context = new TransformationContext("SimpleRuleSet");

        GraphTransformer.getInstance().transformGraphs(roota,rootb,context);
        assertEquals("Tom",rootb.getName());
        assertEquals("Tom",roota.getName());
    }

Here is the SimpleRuleSet.jelly XML rule set:

<?xml version="1.0"?>
<j:jelly trim="false" xmlns:j="jelly:core"
    xmlns:graph="jelly:net.sourceforge.jgt.jelly.JGTTagLibrary">

    <graph:mapping name="names">
        <graph:set name="name"/>
    </graph:mapping>
</j:jelly>

JGT works on two graph, graph A and graph B.  Each graph is passed to JGT as a single root node.  So the first thing the example above does is to configure two very simple graphs and get a reference to the root node for each (roota and rootb).  

JGT also works on a set of transformation rules that define how to map from graph A to graph B and back again.  This is what the TransformationContext object is for.  In its simplest form, it takes nothing more than the name of a rule set that you have defined somewhere else.  In this example we want to use SimpleRuleSet.

The interface to JGT is GraphTransformer.getInstance().transformGraphs().  It takes the root node to graph A, the root node to graph B and the tranformation context.  Internally, it attempts to locate a rule set file called SimpleRuleSet.jelly as a resource on the classpath.  If it fails to locate this, you will get an illegal argument exception.  If it succeeds to locate the resource, it will perform the transformation rules in that file to the two graphs provided.

SimpleRuleSet.jelly defines the rule set required to copy a property from one graph to another.  The first two lines and the last two lines are required by all JGT rule sets.  The graph:mapping bit is where the JGT rule begins.

JGT rule sets are sectioned into named groups.  graph:mapping is what defines the sectioning.  The reason for this is that sections in a rule set can be independently activated/deactivated to allow fine grain specification of transformation requirements, which are likely to be different across use cases.  So, this mapping line above says "there is a mapping called names.  You can specify the mappings you want to execute as a list of names to TransformationContext.setMappings().  The default is for all mappings to execute, which is what will happen in this example.

graph:set, as written above, states that there is a property with the same name on each graph and I want to synchronize their values.  The direction of synchronization is either forward (graph a to graph b) or reverse (graph b to graph a).  This value is specified in the context object (setForwardMapping() and setReverseMapping()) and defaults to forward.  graph:set can also deal with properties with different names on each graph, as shown later.

So, the bottom line after all this work is the equivalent of this java code.

        PersonA roota = new PersonA();
        PersonB rootb = new PersonB();
        roota.setName("Tom");
        rootb.setName("Jerry");

if ( ! roota.getName().equals(rootb.getName())) {
rootb.setName(roota.getName());
}

Hardly very exciting, so lets move on.  Or you can check out a more complete example here.


Child Nodes

A graph of objects usually involves more than a single node.  person.getManager().getName() for example.  How do I deal with this kind of Java code:

if ( ! rootb.getManager().getName().equals(roota.getManager().getName())){
rootb.getManager().setName(roota.getManager().getName());
}

This kind of code becomes even more bothersome when you have to deal with potential nulls.  It could be written as

<graph:set name="manager.name"/>

This is fine if you want to do only a single operation on the manager child node.  Typically though you want to do more than one operation and repeating the "manager" string each time is a pain.  So, JGT allows you set the context of a mapping i.e. the nodes of graph and graph that we are dealing with for this particular mapping:

<graph:mapping name="manager.stuff" nodeA="manager" nodeB="manager">
<graph:set name="name"/>
<graph:set name="employeeNumber"/>
</graph:mapping>

So, the two graph:set statements above will operate in the context of person.manager.


Conditionals

Sometimes you want to continue with the body of a mapping only if particular values on graph A are different to equivalent values on graph B.

1: <graph:compare a="language.id" b="language.languageCode">
2: <graph:onchange>
        3: <graph:link to="languageData" keys="id,language.id"/>
</graph:onchange>
4: <graph:onnull>
5: <graph:prune targetProperty="true"/>
</graph:onnull>
</graph:compare>

What this says is:

  1. Perform a comparison of language.id on graph A against language.languageCode on graph B.
  2. If the values are different, 
  3. Create and assign a new instance of an object of type languageData, using id and language.id as parameters
  4. If one of the child nodes was null during comparison
  5. Prune the target property (depends on the direction of mapping).
Sometimes you want to perform action(s) only if the mapping is forward or reverse.  This is required to support mappings that are not naturally symmetrical.

<graph:onmapping mapping="forward">
body
</graph:onmapping>

<graph:onmapping mapping="reverse">
body
</graph:onmapping>

Pruning and Null handling

It is possible to prune the "target node".  The target node is the node that you have currently put in scope using graph:mapping.  If the mapping is forward, the target node is the current node on graph A.  Other wise it is the current node on graph B.

<graph:mapping name="example" nodeA="manager" nodeB="manager">
<graph:prune targetProperty="true"/>
</graph:mapping>

This will set either roota.manager or rootb.manager to null, depending on the direction of the mapping.

Additionally, the graph:compare tag supports a child graph:onnull tag, which will get fired if any of the nodes in any of the comparison keys is null:

<graph:compare a="country.language.id, country.timezone" b="language.languageCode, timezone">
<graph:onnull>
<graph:prune targetProperty="true"/>
</graph:onnull>
</graph:compare>

Assuming a forward mapping (from graph a to graph b), the onnull section will get executed if country, country.language or country.timezone is null.


Creating new instances and handling Maps

You can use graph:link to create new instances and hook them in to the graph as the target node.

<graph:link to="language" keys="roota.id,roota.language.id"/>

This will:

  1. Look up the type of the property language on the current target node.
  2. Attempt to find a Converter instance keyed on this type
  3. Pass the values of the specified to the converter in a collection
  4. Set targetNode.language to the new instance returned by the converter
graph:link can deal with maps:
<graph:link to="language(rootb.languageCode)" keys="roota.id,roota.language.id" type="net.sourceforge.jgt.beans.domain.LanguageData"/>
graph:link can also set its result to be the target node if you plan to do more processing with it after its construction by the converter.
<graph:link to="language" keys="roota.id,roota.language.id" setvar="a"/>
<graph:link to="language" keys="roota.id,roota.language.id" setvar="b">

Synchronizing collections
graph:set can synchronize a collection to have contents that are equivalent based on some relationship to the values in another collection:

<graph:set name="usertypes" compareKeysA="key" compareKeysB="id"
typeA="net.sourceforge.jgt.beans.presentation.UserType"
typeB="net.sourceforge.jgt.beans.domain.DBUserType"/>

This synchronizes the collections called usertypes on node A and node B.  To determine if a value in usertypes on node A is "equivalent" to a value in usertypes on node B, it compares the values of "key" in graph A against the value of "id" in graph B.  If this comparison fails, a new type (either typeA or typeB, depending on the direction of mapping) is created by retrieving a Converter for the target type and passing the source instance to it.

If there is a value in the target collection that is not equivalent to any value in the source, it is removed.


Activating subsets of mappings

Assume you have the following rule set:
<graph:mapping name="summary_data">
body
</graph:mapping>

<graph:mapping name="all_data">
body
</graph:mapping>

And sometimes you want summary_data to execute, other times "all_data" and other times still both of them:

List mappings = new ArrayList();
mappings.add("summary_data");
TransformationContext context = new TransformationContext("RuleSet");
context.setActiveMappings(mappings);
GraphTransformer.getInstance().transformGraphs(roota,rootb,context);

Catching PropertyChangeEvents

PropertyChangeEvents get fired for every change made to the target graph.  Typically this is required to implement cascade deletes, but provides for other uses like, for example, updating caches or indexes etc.

Collection listeners = new ArrayList();
PropertyChangeListener listener = new MyPropertyChangeListener();
listeners.add(listener);

TransformationContext context = new TransformationContext("RuleSet");
context.setPropertyChangeListeners(listeners);
GraphTransformer.getInstance().transformGraphs(roota,rootb,context);

Subroutines or Repeated Mappings
Sometimes you need to map an object in more than one context in exactly the same way.  Address is a good example.  A Person instance might have a work address and a home address, and both are modelled using the same Address class.  The mapping of an Address from one graph to another will be the same regardless of whether its the work address or the home address.  JGT allows you to define Callables, which are sections of mapping code, and call them in different contexts:

<graph:callable name="address">
<graph:set name="address1"/>
<graph:set name="address2"/>
<graph:set name="address3"/>
</graph:callable>

<graph:mapping name="person.home_address" nodeA="homeAddress" nodeB="homeAddress">
<graph:call callable="address"/>
</graph:mapping>

<graph:mapping name="person.work_address" nodeA="workAddress" nodeB="workAddress">
<graph:call callable="address"/>
</graph:mapping>

See CallableExampleJUnitTest.java




SourceForge Logo