Jura Home | John Winn, December 2003 |
A transform defines an operation on a source object which creates a
target object. Transforms operate recursively on a tree of objects
allowing operations which apply to many levels in the object hierarchy.
Transforms are used throughout Jura whenever hierarchical operations are
required e.g. when expanding out Patterns,
when writing objects to a text file or during compilation.
Transform-related classes are in the jura.transform
package and
allow for new transforms to be created or existing transforms to be extended in
an object-oriented fashion.
Jura transforms can be thought of as a programmatic, object-orientated version of XSLT which has access to the full power and flexibility of the Jura language (and which are compiled and so much more efficient!). Like XSLT, Jura transforms apply only to a tree of objects.
Transforms can be applied where the source object and target object are the same instance, (a self-transform) in which case the object is simply modified by the transform.
The deep copy transform creates a copy of an object and all descendent
objects in the object tree. In Jura, we can define it using a class DeepCopyTransform
which implements the jura.transform.Transform
interface, like so:
Class:DeepCopyTransform{ implements={Transform} Method:transform{ doc={"Copies an object"} Par:t{type=TransformContext} Par:object{type=Object} // t={ copy() transform(ALL) } } }
The TransformContext
object contains all contextual information
about the current state of the transform and provides a range of methods for
transforming objects in the object tree. These include:
copy()
- for copying the object from the current node of the
source tree to the target tree.set(Object obj)
- for setting the object in the target tree.add(Object obj)
- for adding a node in the target tree.The context also provides methods for applying the transform recursively to children in the tree, such as particular named properties, elements contained in collections etc.
A transform is applied using a transformer. Jura provides a
single transformer BeanTransformer
which works off objects in
memory, considering bean properties and collection elements to be children of an
object. As this produces an object graph rather than a tree, links can be marked
as references which will not be followed by the transformer.
Otherwise, the transformer will break the graph whenever it encounters an object
for the second time (which may not give the desired behaviour).
The following Jura code uses a bean transformer to create a deep copy of an object:
BeanTransformer bt = new BeanTransformer{transform=DeepCopyTransform{}} Object copiedObject = bt.transform(myObject)
Currently, transforms are not typed and the result must be cast to an object of the appropriate type. Ideally, a transform should declare a source and target type and it would be checked to ensure that the transform is valid. Such type-safe transforms are not currently available in Jura.
Jura provides two patterns TemplateTransform
(which transforms to a Class
)
and Template
(which transforms to a Method
) that simplify the creation of XSLT-like transforms. The deep copy transform
above can be rewritten using these patterns as:
TemplateTransform:DeepCopyTransform{ Template{ Par:object{type=Object} // copy() transform(ALL) } }
Templates are applied according to the type of their first parameter (like match
in XSLT) and can be named (equivalent to mode
in XSLT). The
following more complex transform demonstrates this. It recursively prints
out all the points of visible polygons in a hierarchy of polygon objects:
TemplateTransform:PrintPoints{ Template{ doc={"The transform only applies to polygons. It simply calls the 'print' template for the polygon."} Par:poly{type=Polygon} // print(SELF) } Template:print{ doc={"Prints the polygon. If the polygon is visible, it calls the 'print' template for all children of the polygon."} Par:poly{type=Polygon} // If{test=poly.isVisible() // print(ALL)} } Template:print{ doc={"Prints a point."} Par:p{type=Point} // System.err.println("Point: "+p.x+","+p.y) } }
The use of these two patterns has changed the way the Jura code for the transform looks, from Java-like source code to XSLT-like source code. This provides an example of how patterns can be applied to modify the basic Jura language so that it is compact for a particular purpose.
If required, a transform can be inserted into the compilation process to allow, for example, debugging code to be inserted in all methods of a particular name. Alternatively, this can be made to happen automatically by using a Pattern and performing the transformation when the pattern is resolved (the transform would then apply only to children of the pattern).