Patterning it

A new approach to XML – Part 4: A template pattern to create XML documents

SvenEwald
template

In the concluding part of this series, learn how to create a document using templates, enrich projections, and extend the build in type converter.

The fourth and last part of this series shows you how to create
a document using templates, how projections can be enriched by own
method implementations, and how to extend the build in type
converter.

We
already demonstrated writing projections in the
second part
of this series. Now we are going deeper into
writing projections and are learning that XMLBeam is not limited to
writing values of elements and attributes only. Utilizing the
subprojections shown in the first part it’s possible to write
entire subgraphs into the DOM tree. This can be used to handle
static and dynamic parts of the document separately and thereby
reduce the effort of programming. Therefore we define templates
read as projections, modified with Java code, and then composed to
a new document. We provide an example reduced to its essentials to
demonstrate the principle.

Example 1: Document template "svg_document_template.svg“
<svg>
   <!-- simple document template -->
        <g style="stroke:black; stroke-width:3;"/>
</svg>

The element “<svg>” is going to be the
basis of our document. We will insert more elements in our code
example, so we create more templates with reasonable
defaults.

Example 1: Element template "rect_template.svg“
<rect width="100" height="100" style="fill:blue" />
Example 1: Element template "circle_template.svg“
<circle r="50" style="fill:red" />
Example 1: Element template "ellipse_template.svg“
<ellipse rx="50" ry="100" style="fill:yellow" />

Afterwards we define projection interfaces to be
able to work with these templates. To share common methods, the
projections
Rectangle,
Circle, and Ellipse
inherit projection methods from Shape
via regular Java type inheritance. There is no deeper meaning
in defining projections as inner interfaces, this is just to save
some lines in our example.

Example 1:
@XBDocURL("resource://svg_document_template.svg")
public interface SVG {

    public interface Shape {
        @XBWrite("./@x")
        Shape setX(int x);

        @XBWrite("./@y")
        Shape setY(int y);
    }

    @XBDocURL("resource://rect_template.svg")
    public interface Rect extends Shape {
        @XBWrite("./@width")
        Rect setWidth(int w);

        @XBWrite("./@heigth")
        Shape setHeight(int h);
    }

    @XBDocURL("resource://circle_template.svg")
    public interface Circle extends Shape {
        @XBWrite("./@r")
        Shape setRadius(int r);
    }

    @XBDocURL("resource://ellipse_template.svg")
    public interface Ellipse extends Shape {
        @XBWrite("./@rx")
        Shape setRadiusX(int rx);
        
        @XBWrite("./@ry")
        Shape setRadiusY(int ry);
    }

    @XBWrite("/svg/g/*")
    SVG setShapes(List<Shape> shapes);
}

The method
s
etShapes(List<Shape>) is the key
to our document composing pattern. It takes the projections of
type
Shape as parameter and inserts these
projections as subelements of element “<g>” into the DOM tree
of the projection
SVG. The gimmick is
hidden, because the written objects are no subprojections to
elements, but projections to documents. XMLBeam supports this use
case and inserts copies of the root elements into the DOM tree. The
copies are made, because the ownership of the template elements
changes during the writing operation. The wildcard “*” instructs
XMLBeam to keep the names of the written subelements. An XPath like
“/svn/g/shape” would result in renaming all subelements to
“shape”.

Example 1: Document composition with templates
XBProjector projector = new XBProjector(Flags.TO_STRING_RENDERS_XML);

Rect rect = projector.io().fromURLAnnotation(Rect.class);
Circle circle = projector.io().fromURLAnnotation(Circle.class);
Ellipse ellipse = projector.io().fromURLAnnotation(Ellipse.class);

List<Shape> shapes = new LinkedList<Shape>();
shapes.add(rect.setX(10).setY(120));
shapes.add(circle.setX(60).setY(60));
shapes.add(ellipse.setX(180).setY(120));

SVG svgDoc = projector.io().fromURLAnnotation(SVG.class);
svgDoc.setShapes(shapes);

System.out.println(svgDoc.toString());

We get the following SVG document as result of
our example.

Example 1: Result
<svg>
  <!-- simple document template -->
  <g style="stroke:black; stroke-width:3;">
    <rect height="100" style="fill:blue" width="100" x="10" y="120"/>
    <circle r="50" style="fill:red" x="60" y="60"/>
    <ellipse rx="50" ry="100" style="fill:yellow" x="180" y="120"/>
  </g>
</svg>

Custom method implementations in
projections

As we have learned in the previous parts of this
series, projections can be used just like plain old Java objects
(POJOs). Thanks to predefined implementations of equals() and
hashcode(), they can be used in
Collections
directly. Inheritance is supported, and by extending
Serializeable, projections can even be
serialized just like regular objects. Here we show how to add own
method implementations to projections.

If XMLBeam is used with Java 8, you can just add
your own default methods to the projection interface. XMLBeam,
while compatible with Java 6, will still detect default method
calls and forward them to the Java 8 implementation. If you are
stuck to a pre Java 8 version and cannot use default methods, there
still is a way to override methods or to add own
implementations.

For this purpose XMLBeam utilizes a concept that
is similar to mixins in other programming languages. XMLBeam
certainly does not extend the features of the Java language with
real mixins, but the term serves as a metaphor suiting this
context. From now on it will be used for a bunch of methods which
is to become added to a projection. And this is how it works: First
a mixin-interface is defined:

A "mixin-interface“
public interface Validatable {
        boolean isValid();      
}

We let our projection interface inherit from the
mixin interface
Validatable so that the
method
isValid() becomes callable for
clients of the projection interface.

A projection with inherited method "isValid()“
public interface Person extends Validatable {
        @XBRead("/person/@age“)
        int getAge();   
}

The definition of the mixin method is still
missing. It is implemented in a separate “mixin-object” which has
to be registered by the projector before creating the
projection:

Registering a mixin implementation
projektor.mixins().addProjectionMixin(Person.class,new Validatable() {
        private Person me;
        @Override
        public boolean isValid() {
           return me.getAge() >= 18;
        }
});

XMLBeam delegates calls to person.isValid() to
the mixin object which is registered for the corresponding
projection interface
Person. To let this
projection become visible for the mixin implementation, the
projector injects the current projection to the attribute “me”
beforehand. Different mixin implementations may be registered for
different projection interfaces. It’s even possible to override
existing methods like “toString()” (see [2], or to use existing
interfaces like
Comparable as mixin
interface (see [3]).

Creating a type converter

XMLBeam takes care that read XML data becomes
converted into the desired return type of projection methods
automatically. These conversions can be extended easily to meet
your requirements.

A custom type converter
public static class HexToLongConversion extends Conversion<Long> {

        private HexToLongConversion(final Long defaultValue) {
            super(defaultValue);
        }

        @Override
        public Long convert(final String data) {
            return Long.parseLong(data, 16);
        }
    }
public interface Projection {
        @XBRead("/foo")
        long getData();
    }

In our example hex numbers in XML strings should
be converted into
Long values. To archive
this, we implement the interface
Conversion
and hand our instance over to the DefaultTypeConverter as
desired conversion for
Long.

Using the type converter
XBProjector projector = new XBProjector();
DefaultTypeConverter converter = projector.config().getTypeConverterAs(DefaultTypeConverter.class);
converter.setConversionForType(Long.TYPE, new HexToLongConversion(0L));
Projection projection = projector.projectXMLString("<foo>CAFEBABE</foo>", Projection.class);
assertEquals(3405691582L, projection.getData());

That’s all which has to be done to override
existing conversions. New conversions for own Java types are added
the same way. If you already have a value class that can be
instantiated with a
String, there is a
shortcut: XMLBeam automatically invokes

String-constructors or
String-Factory methods of projection method’s
return types (see [4]).

This completes our series about XMLBeam. There
are more tutorials and examples waiting to be explored at the
project site [1].

About the author

Sven Ewald (Twitter handle:@Cfx) is the author of
the library XMLBeam (@XMLBeam). He has been creating Java solutions
for 15 years with no end in sight. Currently he works in the field
of domain-specific languages for the automotive industry.

Links & Literature

[1] http://xmlbeam.org

[2] http://xmlbeam.org/faq.html#How_do_I_implement_the_toString_method_for_my_projection

[3] http://xmlbeam.org/t07.html

[4]http://xmlbeam.org/refcards.html#Let_a_projection_return_a_custom_type_that_can_be_created_with_a_String

 


Author
Comments
comments powered by Disqus