XsdAsm is a library dedicated to generate a fluent java DSL based on a XSD file. It uses
XsdParser library to parse the XSD file into a list of Java classes
that XsdAsm will use in order to obtain the information needed to generate the correspondent classes. In order to
generate classes this library uses the ASM library, which is a library that provides a Java interface to perform
bytecode manipulation, which allows for the creation of classes, methods, etc.
This library main objective is to generate a fluent Java DSL based on an existing XSD DSL. It aims to verify the largest number of the restrictions defined in the XSD DSL. It uses the Java compiler to perform most validations and in some cases where such isn't possible it performs run time validations, throwing exceptions if the rules of the used language are violated.
This library main objective is to generate a fluent Java DSL based on an existing XSD DSL. It aims to verify the largest number of the restrictions defined in the XSD DSL. It uses the Java compiler to perform most validations and in some cases where such isn't possible it performs run time validations, throwing exceptions if the rules of the used language are violated.
First, in order to include it to your Maven project, simply add this dependency:
<dependency>
<groupId>com.github.xmlet</groupId>
<artifactId>xsdAsm</artifactId>
<version>1.0.14</version>
</dependency>
The XSD language uses two main types of values: elements and attributes. Elements are complex value types, they can
have attributes and contain other elements. Attributes are defined by a type and a value, which can have restrictions.
With that in mind XsdAsm created a common set of classes that supports every generated DSL as shown below:
The Attribute and Element interfaces serve as a base to all attribute and element classes that will be generated in any generated DSL, with AbstractElement as an abstract class from which the concrete element classes will derive. This abstract class contains a list of attributes and elements present in the concrete element and other shared features from elements. BaseAttribute serves as a base to every Attribute that validates their restrictions. In the diagram the Html and AttrManifestString classes are shown as concrete implementations of AbstractElement and BaseAttribute, respectively.
XsdAsm provides a XsdAsmMain class that receives two arguments, the first one being the XSD file path and the second
one is the name of the DSL to be generated. All the generated DSLs are placed in the same base package, org.xmlet,
the difference being the chosen DSL name, for example, if the DSL name is htmlapi, the resulting package name
is org.xmlet.htmlapi.
public class Example{
void generateApi(String filePath, String apiName){
XsdAsmMain.main(new String[] {filePath, apiName} );
}
}
The generated classes will be written in the target folder of the invoking project. For example, the
HtmlApi project
invokes the XsdAsmMain, generating all the HmlApi classes and writing them in the HtmlApi target folder, this
way when HtmlApi is used as a dependency those classes appear as normal classes as if they were manually created.
Using the Html element from the HTML5 specification a simple example will be presented, which can be extrapolated to
other elements. Some simplification will be made in this example for easier understanding.
<xs:element name="html">
<xs:complexType>
<xs:choice>
<xs:element ref="body"/>
<xs:element ref="head"/>
</xs:choice>
<xs:attributeGroup ref="commonAttributeGroup" />
<xs:attribute name="manifest" type="xsd:anyURI" />
</xs:complexType>
</xs:element>
With this example in mind what classes will need to be generated?
Html Class - A class that represents the Html element, represented in XSD by the xs:element name="html", deriving from AbstractElement.
body and head Methods - Both methods present in the Html class that add Body and Head instances to Html children. This methods are created due to their presence in the xs:choice XSD element.
attrManifest Method - A method present in Html class that adds an instance of the AttrManifestString attribute to the Html attribute list. This method is created because the XSD html element contains a xs:attribute name="manifest" with a xsd:anyURI type, which maps to String in Java.
Html Class - A class that represents the Html element, represented in XSD by the xs:element name="html", deriving from AbstractElement.
body and head Methods - Both methods present in the Html class that add Body and Head instances to Html children. This methods are created due to their presence in the xs:choice XSD element.
attrManifest Method - A method present in Html class that adds an instance of the AttrManifestString attribute to the Html attribute list. This method is created because the XSD html element contains a xs:attribute name="manifest" with a xsd:anyURI type, which maps to String in Java.
public class Html extends AbstractElement implements CommonAttributeGroup {
public Html() { }
public Html attrManifest(String attrManifest) {
this.addAttr(new AttrManifestString(attrManifest));
}
public Body body() {
this.addChild(new Body());
}
public Head head() {
this.addChild(new Head());
}
}
Body and Head classes - Classes for both Body and Head elements, created based on their respective XSD xsd:element.
public class Body extends AbstractElement {
// Contents based on the respective xsd:element name="body"
}
public class Head extends AbstractElement {
// Contents based on the respective xsd:element name="head"
}
AttrManifestString Attribute - A class that represents the Manifest attribute, deriving from BaseAttribute. Its type is
String because the XSD type xsd:anyURI maps to the type String in Java.
public class AttrManifestString extends BaseAttribute<String> {
public AttrManifestString(String attrValue) {
super(attrValue);
}
}
CommonAttributeGroup Interface - An interface with default methods that add the group attributes to the element which implements this interface.
public interface CommonAttributeGroup extends Element {
//Assuming CommonAttribute is an attribute group with a single
//attribute named SomeAttribute with the type String.
default Html attrSomeAttribute(String attributeValue) {
this.addAttr(new SomeAttribute(attributeValue));
return this;
}
}
As we've stated previously, the DSLs generated by this project aim to guarantee the validation of the set of rules associated
with the language. To achieve this we heavily rely on Java types, as shown above, i.e. the Html class can only
contain Body and Head instances as children and attributes such as AttrManifest or any attribute
belonging to CommonAttributeGroup. This solves our problem, but since we are using a fluent approach to the generated
DSLs another important aspect is to always mantain type information. To guarantee this we use type parameters, also known
as generics.
class Example{
void example(){
Html<Element> html = new Html<>();
Body<Html<Element>> body = html.body();
P<Header<Body<Html<Element>>>> p1 = body.header().p();
P<Div<Body<Html<Element>>>> p2 = body.div().p();
Header<Body<Html<Element>>> header = p1.__();
Div<Body<Html<Element>>> div = p2.__();
}
}
In this example we can see how the type information is mantained. When each element is created it receives the parent
type information, which allows to keep the type information even when we navigate to the parent of the current element.
A good example of this are both P element instances, p1 and p2. Both share their type, but each
one of them have diferent parent information, p1 is a child of an Header instance, while p2 is
a child of a Div instance. When the method that navigates to the parent element is called, the __() method,
each one returns its respective parent, with the correct type.
In the description of any given XSD file there are many restrictions in the way the elements are contained in each
other and which attributes are allowed. Reflecting those same restrictions to the Java language we have two ways of
ensure those same restrictions, either at runtime or in compile time. This library tries to validate most of the
restrictions in compile time, as shown in the example above. But in some restrictions it isn't possible to validate
in compile time, an example of this is the following restriction:
<xs:schema>
<xs:element name="testElement">
<xs:complexType>
<xs:attribute name="intList" type="valuelist"/>
</xs:complexType>
</xs:element>
<xs:simpleType name="valuelist">
<xs:restriction>
<xs:maxLength value="5"/>
<xs:minLength value="1"/>
</xs:restriction>
<xs:list itemType="xsd:int"/>
</xs:simpleType>
</xs:schema>
In this example we have an element that has an attribute called valueList. This attribute has some restrictions, it
is represented by a xsd:list and its element count should be between 1 and 5. Transporting this example to the Java
language it will result in the following class:
public class AttrIntList extends BaseAttribute<List<Integer>> {
public AttrIntList(List<Integer> attrValue) {
super(attrValue, "intList");
}
}
But with this solution the xsd:maxLength and xsd:minLength restrictions are ignored. To solve this problem the
existing restrictions of any given attribute are hardcoded in the class constructor. This will result in method calls
to validation methods, which verify the attribute restrictions whenever an instance is created. If the instances fails
any validation the result is an exception thrown by the validation methods.
public class AttrIntList extends BaseAttribute<List<Integer>> {
public AttrIntList(List<Integer> attrValue) {
super(attrValue, "intList");
RestrictionValidator.validateMaxLength(5, attrValue);
RestrictionValidator.validateMinLength(1, attrValue);
}
}
In regard to the restrictions there is a special restriction that can be enforced at compile time, the xsd:enumeration.
In order to obtain that validation at compile time the XsdAsm library generates Enum classes that contain all the
values indicated in the xsd:enumeration tags. In the following example we have an attribute with three possible
values: command, checkbox and radio.
<xs:attribute name="type">
<xs:simpleType>
<xs:restriction base="xsd:string">
<xs:enumeration value="command" />
<xs:enumeration value="checkbox" />
<xs:enumeration value="radio" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
This results in the creation of an Enum, EnumTypeCommand, as shown below. This means that any attribute that uses
this type will receive an instance of EnumTypeCommand instead of receiving a String. This guarantees at
compile time that only the allowed set of values are passed to the respective attribute.
public enum EnumTypeCommand {
COMMAND(String.valueOf("command")),
CHECKBOX(String.valueOf("checkbox")),
RADIO(String.valueOf("radio"))
}
public class AttrTypeEnumTypeCommand extends BaseAttribute<String> {
public AttrTypeEnumTypeCommand(EnumTypeCommand attrValue) {
super(attrValue.getValue());
}
}
This library also uses the Visitor pattern. Using this pattern allows different uses for the same DSL, given that
different Visitors are implemented. Each generated DSL will have one ElementVisitor, this class is
an abstract class which contains four main visit methods:
Apart from this four methods we have create specific methods for each element class created, e.g. the Html class. This introduces a greater level of control, since the concrete ElementVisitor implementation can manipulate each visit method in a different way. These specific methods invoke the sharedVisit as their default behaviour, as shown below.
- sharedVisit(Element element) - This method is called whenever a class generated based on a XSD xsd:element has its accept method called. By receiving the Element we have access to the element children and attributes.
- visit(Text text) - This method is called when the accept method of the special Text element is invoked.
- visit(Comment comment) - This method is called when the accept method of the special Comment element is invoked.
- visit(TextFuction textFunction) - This method is called when the accept method of the special TextFunction element is invoked.
Apart from this four methods we have create specific methods for each element class created, e.g. the Html class. This introduces a greater level of control, since the concrete ElementVisitor implementation can manipulate each visit method in a different way. These specific methods invoke the sharedVisit as their default behaviour, as shown below.
public class ElementVisitor {
// (...)
default void visit(Html html) {
this.sharedVisit(html);
}
}
To support the definition of reusable templates the Element and AbstractElement classes were changed to support binders.
This allows programmers to postpone the addition of information to the defined element tree. An example is shown below.
public class BinderExample{
public void bindExample(){
Html<Element> root = new Html<>()
.body()
.table()
.tr()
.th()
.text("Title")
.__()
.__()
.<List<String>>binder((elem, list) ->
list.forEach(tdValue ->
elem.tr().td().text(tdValue)
)
)
.__()
.__()
.__();
}
}
In this example a Table instance is created, and a Title is added in the first row as a title header, i.e. th.
After defining the table header of the table we can see that we invoke a binder method. This method bounds the Table
instance with a function, which defines the behaviour to be performed when this instance receives the information.
This way a template can be defined and reused with different values. A full example of how this works is available at
the method testBinderUsage.
There are some tests available using the HTML5 schema and the Android layouts schema, you can give a look at that
examples and tweak them in order to gain a better understanding of how the class generation works. The tests also
cover most of the code, if you are interested in verifying the code quality, vulnerabilities and other various
metrics, check the following link:
Sonarcloud Statistics
Sonarcloud Statistics
Some examples presented here are simplified in order to give a better understanding of how this library works. In
order to allow a better usage for the generated API end user there are multiple improvements made using type arguments.