In the few months of JaxMe's life (writing these words in May 2002)
the program has proved to be very useful. Quite some suggestions and
ideas for enhancements came up.
To my surprise, the most discussions haven't been with the lack
of support for the various XML Schema possibilities (for example,
no one has requested support for the schema enumerations so far).
Instead the wish to generate other sources from the schema has
been there from the start. Thanks to ideas from Stefan Becher,
there is now a framework for other generators: The
decorators and the parselets.
Decorators: Adding information to the schema tree
The first versions of JaxMe had a strict separation between
Schema readers and Source writers. Schema readers had to build
a schema tree, source writers had to use it. The idea was that
it should be possible to use different readers for the same
writer.
While this idea is still valid, it became difficult in practice.
It turned out to be very important that writers
had to be configured from the schema. In other words: The readers
have to supply the information that a specific writer wants.
A new concept was needed, which are the decorators. A reader
can create a decorator which is a writer specific object
attached to some element or attribute. The decorator is
configured through its bean properties and later used by
the writer.
More formally, the following steps are happening:
- The SchemaReader creates a
Decorator factory. As the name says, this factory
can create the actual decorators. The factory is
typically configured through bean properties.
- From now on, the schema reader will call the
factory for any element or attribute that it
has read. The factory may create a decorator, which
is attached to the element or attribute, thus a
part of the tree that the reader builds. The
decorators are also configured through bean
properties.
- The decorator factory can also create a source
writer. This source writer receives the readers
tree, including the attached decorators. It can
now emit sources, using the decorators for configuring
itself.
You can imagine the process similar to this:
Configuring a decorator factory
As an example for decorators we'll take the Parselets,
which will be described later in
more detail. The full example is available in
message.xsd.
First of all, we have to create the factory.
This is done in the appinfo section of the schema node:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xml:lang="EN"
xmlns:jm="http://ispsoft.de/namespaces/jaxme/schema"
xmlns:jp="http://ispsoft.de/namespaces/jaxme/schema/parselet"
targetNamespace="http://jaxme.ispsoft.de/namespaces/examples/parselets"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:annotation>
<xs:appinfo>
<jm:elementDecoratorFactory
class="de.ispsoft.jaxme.generator.parselets.ParseletDecoratorFactory"/>
</xs:appinfo>
</xs:annotation>
...
</xs:schema>
Things you should note:
- The decorator factory has its own namespace. This is not
required, but definitely recommended.
- The factory is declared with a node
xs:elementDecoratorFactory
. This forces the
schema reader to instantiate an instance of the given
class and add it to its list of decorator factories.
Multiple factories are allowed.
- What you don't see here: The factory declaration may
have attributes and/or atomic child elements. These
attributes and child elements represent beans. That is,
if the factory declaration has an attribute
someProperty="someValue" and the
factory has a method
setSomeProperty(String pValue)
,
then this method will be called with the string
someValue as an argument. Likewise, if there
is a method setSomeProperty(int pValue)
,
then the schema reader will attempt to convert
someValue into an integer and call the
method. Of course, the conversion can fail, which is
a fatal error.
Besides the initial configuration, a decorator factory
can configure itself arbitrarily through the schema nodes
xs:appinfo
section. The schema reader will
call the decorator factories parseAppInfoDefaultNode
for any child element of the xs:appinfo
element
that appears after the factory declaration.
Configuring a decorator
Once the factory is configured, it can create decorators.
From now on, whenever the schema reader creates an element
or attribute, it will allow the factory to add a decorator
to the element. For that purpose, the factory method
public ElementDecorator
newElementDecorator(SchemaAnnotationOwner parent,
SchemaElement schemaElement,
ParserData parserData,
Element node)
throws SchemaException;
is called. The method can use the created
schemaElement
(for example, it might want to
know its multiplicity or whether it is an attribute),
it can look at the parent
in order to inspect the tree
built so far, it can add data to the parserData
node, which is used for configuration settings that hold true
for childs and it can look at the elements DOM tree given by
node
. If the factory returns a decorator, then
it will be attached to the element. The node can later be
retrieved by calling
schemaElement.getElementDecorator(factory)
.
It's a part of the schema tree.
Once again, the decorator can configure itself from the
elements xs:appinfo
section: The schema reader
will call the decorators method parseAppInfoNode
for any child element of this section. If the decorator detects,
that the child is relevant, then it may use the method
de.ispsoft.jaxme.generator.util.DOM.assignBeanProperties
to configure itself from the node.
We demonstrate this by looking at the parselet example from
above.
<xs:element name="Type" type="xs:integer">
<xs:annotation>
<xs:appinfo>
<jp:parselet name="IntegerParselet"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
The parselet decorator has a method
setName(String pName)
. If the decorators
parseAppInfoNode
method is called for the
jp:parselet element, then it calls
DOM.assignBeanProperties
, which in turn will
call setName("IntegerParselet");
.
A decorators source writer
The decorator factory has another method which we haven't
seen so far: It's called getSourceWriter()
and may return a source writer. This method is called
before any decorators are created. In other words, the
factory can tell the source writer about any decorator,
if it likes. However, it doesn't need to.
Once the schema reader has built its tree, it calls
the source writer for doing its work. The source writer
receives the element tree with the attached decorators
and all the collected information.
Parselets: Implementing conversions from or to other objects
The parselet framework was originally designed for a very
simple, but probably typical task: An XML element had to be
converted into a binary stream. Vice versa, a binary stream
had to be read and converted into XML.
The basic idea is as follows: A Parselet is an interface
with two methods:
public void serialize(Object pSerializable,
Object pData) throws SAXException;
public Object parse(Object pData) throws SAXException;
The method serialize
converts the XML document
pSerializable into another object, which is
created by pData. On the other hand, the
method parse
is called for converting the
object pData into an XML document, which it
returns. This is typically simple for atomic elements
or attributes. For complex elements, a Parselet can
probably be built, by calling the child Parselets in
the right order. For example, suggest the following
XML document:
<A id="someId">
<B>324</B>
<C>12.0>
<D>
<E>x</E>
<E>y</E>
</D>
</A>
If we have Parselets for the attribute id
and the elements B, C
and E which may serialize themselfes into
a stream, then we also have Parselets for
D (by calling the Parselet for E twice)
and F (by using the Parselets for id,
B, C and D, in that order.