XML Schema to Template Mapping Tool: Library v0.12.0
The schema2template project's goal is to provide an API for easily filling user templates from arbitrary XML schematas.
Introduction
This project allows you to process a RelaxNG schema file and to convert the definitions to any text based format you like. For example, you can produce one source code file for each element defined in schema file. Or maybe you want to produce one single text or HTML based overview of the schema.
Since this project is based on the - former Sun - Multi Schema Validator, it should be able to process multiple schema formats. However up to now it has only being used with RelaxNG.
Several examples are delivered together with this project:
- Read the OpenDocument Schema and produce a Java based DOM API
- Read the OpenDocument Schema and produce an HTML overview of all ODF elements and attributes
- Read the OpenDocument Schema and produce one single Python file which represents the element hierarchy
A non-trivial topic is the concept of multiple definitions for a single element or attribute, which is possible in RelaxNG. The easiest way would be to unite these definitions (we call them Multiples) to one common definition. However this way important information would be lost. E.g. one element may have the attribute foo with possible value left. This attribute may also be possible in another element, but then with possible values left and right. That's why we decided to keep the distinction, even if it may make it harder to understand the API of this project. We might enhance this concept in the future, e.g. to exactly determine which of the multiple definitions holds for the current element in an ODF element tree.
Velocity Help
Go to the "Engine" section of the the Velocity project site to find the Velocity User Guide. The velocity template structure and commands are explained there.Schema2Template file structure
We use a 2-step procedure: First we generate a list of files we'd like to create. While we could write this list manually, the generation of this list frees us from having to write an entry for each schema element or attribute. The initial main template to generate this list is called file-creation-list.vm, the list itself (i.e. the result of running file-creation-list.vm) is called file-creation-list.xml.
Here's an example file-creation-list.xml for typed ODF DOM elements:
<?xml version="1.0" encoding="UTF-8"?> <file-creation-list> <file path="org/odftoolkit/odfdom/dom/style/OdfStyleFamily.java" template="java-odfdom-stylefamily.vm" /> <file path="org/odftoolkit/odfdom/dom/style/props/OdfStylePropertiesSet.java" template="java-odfdom-styleset.vm" /> <file path="org/odftoolkit/odfdom/dom/DefaultElementVisitor.java" template="java-odfdom-element-visitor.vm" /> ## #foreach ($element in ${xmlModel.getElements().withoutMultiples()}) #if ($element != "*") #set($className = "${xmlModel.camelCase($element)}Element") <file path="org/odftoolkit/odfdom/dom/element/${xmlModel.extractNamespace($element)}/${className}.java" contextNode="$element" template="java-odfdom-element-template.vm" /> #end #end ...
In the second step we parse file-creation-list.xml. For every line - "file" entry - we run the as attribute given Velocity "template". The template is provided with an additional contextNode by the optional "contextNode" XML attribute and the output file is written to the given "path" XML attribute in the template. The "contextNode" property (and as seen later - the optional "param" property as well) is used in templates with varying object. Sometimes representing an element or attribute as PuzzlePiece sometimes a SourceBaseClass.
Schema2Template specific template help
In velocity templates, you may provide any information you like via the Velocity context object, provided by the Java Velocity Runner Class. These are Java objects and you can call any method of these objects like you would do in Java. In the SchemaToTemplate class we embedded the following domain objects to the Velocity context (see JavaDoc of the given class):
The three variables below are directly accessible in any template:
- XmlModel as xmlModel: Provides the elements and attributes defined in the schema.
- OdfModel as odfmodel: Provides additional information from the Odf Specification, like attribute standard values or element style families.
- SourceCodeModel as codeModel: Provides additional information for generation of Source Code, like common base classes for elements.
The following two variables below are being set from the main template file-creation-list.vm and are therefore accessible from the templates being called afterwards:
- PuzzlePieceComponent contextNode: The (optional) name of the current element, given as parameter by the file-creation-list. You can use model to get the element or attribute described by this name
- String param: A freely usable (optional) argument. An example usage is to provide a number to distinguish between elements (or attributes) sharing the same name.
Of course at the start of file-creation-list.vm the optional context Objects "contextNode and "param" are null.
PuzzlePiece Class
The PuzzlePiece class provides some sort of "piece of a puzzle", containing the definition of an Odf element, attribute, datatype or constant attribute value. This class contains methods for querying the relationship between definitions, e.g. to get all child elements, attributes, parent elements, datatypes, values, etc... PuzzlePieces are sorted by their name (which has the form "ns:localname").
Some of these return parameters are collections. For these there is the class PuzzlePieceSet, which is a SortedSet of PuzzlePieces. PuzzlePiece and PuzzlePieceSet both implement the interface QNamedPuzzleComponent, and therefore have many methods in common (you can query all child elements for a PuzzlePiece as well as for a whole PuzzlePieceSet. And you can get the name for a PuzzlePiece as well as for a whole PuzzlePieceSet - provided all PuzzlePieces are equally named, ...)
Before we come to the point on how to use PuzzlePieces and how to get them, there's one more important thing to know:
Multiples and Multiple Number
One element (form:list-value) and many attributes (chart-symbol-type, text:outline-level, etc...) are defined multiple times in Odf Schema. Each definition is represented by a PuzzlePiece object. Those PuzzlePieces sharing the same name are called Multiples. Each Multiple may differ in parent elements, child elements and attributes. From a PuzzlePiece you can get all PuzzlePieces sharing the same name as the PuzzlePiece by its method withMultiples(). If there are no other PuzzlePieces you get at least a singleton PuzzlePieceSet containing only this PuzzlePiece.
How to get QNamedPuzzleComponent (PuzzlePiece and PuzzlePieceSets)
If you do not want to distinguish between elements (or attributes) sharing the same name you can use in templates
processing only one element:
$xmlModel.getElement($context) or $xmlModel.getAttribute($context). By this you get a PuzzlePieceSet of PuzzlePieces
sharing the same name. Since both
classes share most methods, you can go on with your template like you'd use one single PuzzlePiece.
If you want to distinguish between elements (or attributes) sharing the same name you can use in templates
processing only one element:
$xmlModel.getElement($context, $param) or $xmlModel.getAttribute($context, $param), provided that context contains
the name of the PuzzlePiece and param contains the multiple number to distinguish between these PuzzlePieces. In
other words you have to fill both properties in file-creation-list.vm with the needed values.
You get the name of a PuzzlePiece by ${puzzlePiece.getQName()} or directly by $puzzlePiece and the multiple number
by ${puzzlePiece.getMultipleNumber()}.
It should be noted that this is a very rare use case (e.g. if you want to produce one file per PuzzlePiece and
not - as usual - per PuzzlePiece name).
If you loop over a Set of PuzzlePieces, this Set will contain Multiples. If you want to distinguish between these
elements/attributes, there's nothing special to do.
But if you do not want to distinguish between elements/attributes sharing the same name, you have two steps to do:
First, you have to create a new PuzzlePieceSet without
these Multiples by myPuzzlePiece.withoutMultiples(). Now you have a reduced Set of PuzzlePieces, where only one
random PuzzlePiece per name remains.
If you'd process only the random remaining PuzzlePiece for a name, and you're processing more than just the
PuzzlePiece name, you might miss some information
contained in the removed multiples. Even more, since you distinguish only between names, there might be some
information contained in
Multiples which weren't even in the list before. (like an attribute Multiple
which wasn't in the $element.getAttributes() list as it's only defined for another element). So to get all
Multiples (not only those which were contained in the Set) of a PuzzlePiece, you use myPizzlePiece.withMultiples()
for each PuzzlePiece of the reduced PuzzlePieceSet your're looping over.
How to use QNamedPuzzleComponent (PuzzlePieces and PuzzlePieceSets)
Mostly you might want to use a PuzzlePiece by inserting it directly into the template, like "this element is called $element", or like "the namespace of this element is ${element.getNamespace()}". It doesn't matter if $element is a PuzzlePiece as returned by xmlModel.getElement($context, $param), or a PuzzlePieceSet of equally named PuzzlePieces as returned by xmlModel.getElement($context).
The model provides a few additional functions for String formatting like xmlModel.camelCase($element) for text:p -> TextP or xmlModel.javaCase($element) for text:p -> textP.
Another usage of QNamedPuzzleComponents is to get other QNamedPuzzleComponents, e.g. by $element.getParents(), $attribute.getDatatypes(), $attribute.getValues(), $element.getChildElements(), $element.getAttributes().
To restrict a PuzzlePieceSet only to the PuzzlePieces which have a common parent, there is $elements.byParent($equallyNamedParents). With this method you can implement very distinct validation method in attribute classes. See the byParent-example below.
How to read the Javadoc for Template Usage
Basically you could use every public method from the context objects in a Velocity template. But this is not in our intention. There are quite a few public methods which are only to be used internally, e.g. to extract our model from the Schema file.
To see the list of template-ready methods, you might want to look at the TemplateAPICoverageTest and the OdfTemplateAPICoverageTest from the source code bundle.
To give an example you will see that the PuzzleComponent method "canHaveText" is meant for template usage. On the other hand you wont find neither method compareTo nor extractPuzzlePieces from class PuzzlePiece there, so those methods are not meant to be used in templates. Only the methods covered by the two mentioned tests are protected against internal refactoring (especially against renaming).
Examples
Here's an example template to generate a ODF reference of attribute / elements. Here the PuzzlePieces are distinguished:
#foreach( $element in ${xmlModel.getElements()} ) #if ($element != "*") ## #if (${element.withMultiples().size()} <= 1) #set ($duplicates = "") #else #set ($duplicates = "[${element.getMultipleNumber()}]") #end <h3><a name="element_${element}_${element.getMultipleNumber()}">${element}${duplicates} Element</a></h3> #if ( ${element.withMultiples().size()} > 1 ) <p>There are more than one Definitions by this name.</p> #end
Here's an example file-creation-list.vm which only produces one entry per name (processing just the element name so the Multiples can safely be removed):
<?xml version="1.0" encoding="UTF-8"?> <file-creation-list> <file path="org/odftoolkit/odfdom/dom/style/OdfStyleFamily.java" template="java-odfdom-stylefamily.vm" /> <file path="org/odftoolkit/odfdom/dom/style/props/OdfStylePropertiesSet.java" template="java-odfdom-styleset.vm" /> <file path="org/odftoolkit/odfdom/dom/DefaultElementVisitor.java" template="java-odfdom-element-visitor.vm" /> ### <file path="org/odftoolkit/odfdom/dom/DefaultAttributeVisitor.java" template="java-odfdom-attribute-visitor.vm" /> ## #foreach ($element in ${xmlModel.getElements().withoutMultiples()}) #if ($element != "*") #set($className = "${xmlModel.camelCase($element)}Element") <file path="org/odftoolkit/odfdom/dom/element/${xmlModel.extractNamespace($element)}/${className}.java" contextNode="$element" template="java-odfdom-element-template.vm" /> #end #end
Here's an example template to generate a Java class for an element name (by looking at all element Multiples at once).
To keep it short only the part where the Getter-Methods for all attribute names (by looking at all attribute multiples at once) are generated is shown:
## This will return a PuzzlePieceSet containing all element PuzzlePieces with name $context #set($element = ${xmlModel.getElement($context)}) ... ## ## Step 1: Iterate over all attributes with one random attribute PuzzlePiece per attribute name #foreach ( $singleattr in ${element.getAttributes().withoutMultiples()} ) ## ## Step 2: Get all attribute definitions for one attribute name. ## Not needed if we're just processing the attribute name #set($attribute = ${singleattr.withMultiples()} ## #set($aClassname = "${xmlModel.camelCase($attribute)}Attribute" ) /** * Gets the value of the <code>$aClassname</code> attribute, * see {@odf.attribute ${attribute}} * * @return - the <code>String</code> attribute value */ public String get${aClassname}() { ... } #end ...
Example for the byParent method: Input value validation in attribute template
Implementation detail: one attribute and one parent class implemented per name - thus ignoring PuzzlePiece multiples at Java class level. However differences between attribute PuzzlePieces are respected by using a different validation:
## Returns PuzzleComponent covering a PuzzlePiece or PuzzlePieceSet dependent if attribute is multiple times defined with $contextNode (given as parameter from the initial file-create-list.vm template) #set($attribute = $xmlModel.getAttribute($contextNode)) <td class="left">Attributes</td> <td class="right"> #foreach( $attribute in ${element.getAttributes()} ) #if ($attribute == "*") [any org.w3c.dom.Attribute] #else #if (${attribute.withMultiples().size()} <= 1) #set ($duplicates = "") #else #set ($duplicates = "[${attribute.getMultipleNumber()}]") #end ## #if (${element.isMandatory($attribute)}) #set ($mandatory = "mandatory") #else #set ($mandatory = "") #end ## </td> </tr> <tr>