Since version 2.0 Spicelib contains a small but powerful and flexibel XML-to-Object Mapper. It allows you to map from XML to AS3 classes - in both directions. It comes with builtin mappers which cover the most common use cases like mapping properties of AS3 classes to XML attributes or child elements. But it is easily extensible to add your custom mappers for some of your XML elements and combine them with the builtin ones.
Let's start with a simple example. Consider this XML structure:
<order>
<book
isbn="0942407296"
page-count="256"
title="Rain"
author="Karen Duve"
/>
<book
isbn="0953892201"
page-count="272"
title="Perfume"
author="Patrick Suskind"
comment="Special Offer"
/>
</order>
Now we create two classes that this XML structure should map to:
public class Order {
public var books:Array;
}
public class Book {
[Required]
public var isbn:String;
[Required]
public var pageCount:int;
[Required]
public var title:String;
[Required]
public var author:String;
public var comment:String;
}
It should be obvious how XML elements and attributes are supposed to map to these two classes and their properties.
The only detail that probably needs a bit of explanation is the [Required] metadata tag. The mapper validates
the XML based on these annotations. So for the book element only the comment attribute is optional.
If any of the other 4 attributes would be missing the mapping operation will fail. For more details see
18.3 Validation.
Next we create the mapper that is responsible for transforming this structure;
var bookBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Book, new QName("book"));
bookBuilder.mapAllToAttributes();
var orderBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Order, new QName("order"));
orderBuilder.mapToChildElement("books", bookBuilder.build());
var mapper:XmlObjectMapper = orderBuilder.build();
First we create the PropertyMapperBuilder for the book tag.
We pass the class the element should be mapped to as an ClassInfo instance (ClassInfo
is part of the Spicelib Reflection API) and the name of the XML element.
With the call to mapAllToAttributes we simply instruct the builder to map all properties
of the Book class to attributes of the book XML element. This includes processing
the [Required] tags we already mentioned as well as type conversions (if necessary).
We then create the builder for the order tag in a similar way. This time we don't want
properties to map to attributes but to child elements. We get this behaviour through this line:
orderBuilder.mapToChildElement("books", bookBuilder.build());
This instructs the framework to map child elements to the books Array property.
It will use the mapper returned by bookBuilder.build() for mapping the child elements
itself. This way you can build possibly large and complex hierarchies of mappers where each mapper
is only responsible for a single tag.
The fact that the books property is of type Array has an influence on the validator:
It will allow multiple child elements. Otherwise it would allow at most a single child element of that type.
The mapper we created is now ready to use. This is how we would map from XML to objects:
var xml:XML = ...;
var context:XmlProcessorContext = new XmlProcessorContext();
var order:Order = mapper.mapToObject(xml, context) as Order;
And this is how we'd map from objects to XML:
var order:Order = ...;
var context:XmlProcessorContext = new XmlProcessorContext();
var xml:XML = mapper.mapToXml(order, context);
The PropertyMapperBuilder class we already introduced in our example is the main entry
point for building mappers. In theory you create your own implementations of the XmlObjectMapper
interface from scratch. But in most cases the mapping logic would involve mapping properties of classes
to XML nodes (elements, attributes or text nodes), and for these most common use cases the PropertyMapperBuilder
is the most convenient option.
Like in the example in the previous section you always create an instance of this class passing
the AS3 class and the XML element name that should be mapped to the constructor. You then call one or more
of the mapXXX methods to give detailed instructions on how to map the properties of the specified
class. Finally you create the actual mapper with PropertyMapperBuilder.build(), possibly using
it as the mapper for child elements of another mapper.
In the following sections we describe the exact behaviour of the various mapXXX methods.
But first we'll provide some more detail on the validation process.
Like shown in the simple example you can place [Required] metadata on properties so that the
mapper throws an Error if the attribute or child element that the property is mapped to is not present in XML.
This section provides some more detail on the exact semantics of this feature.
Validating single valued properties
When a property is single valued, either with a simple type that maps to an attribute or a text node or typed to a class that maps to a child element, the validation process includes the following checks:
[Required] tag the mapper checks if the attribute, text node
or child element is present in the mapped XML and throws an error if it is missing. Without the metadata tag
the mapped XML element is considered optional. Validating Array properties
Array properties cannot be mapped to attributes (as multiple occurences of the same attribute in a single element are not possible). If they are mapped to child text nodes or child elements, the validation process includes the following checks:
[Required] tag the mapper checks if the child text node
or child element has at least a single occurence and throws an Error if otherwise. [Required] tag any number of occurences for the child element (including 0)
are permitted. Properties with a simple type like String, int, Boolean, Class or Date
can be mapped to attributes:
public class Song {
public var year:int;
public var title:String;
public var artist:String;
}
<song
year="1989"
title="Monkey Gone To Heaven"
artist="Pixies"
/>
The PropertyMapperBuilder class contains two methods that let you instruct the mapper to map to attributes:
public function mapAllToAttributes () : void
public function mapToAttribute (propertyName:String, attributeName:QName = null) : void
The first method was already used in our usage example: It instructs the mapper to map all properties of the mapped
class to attributes in XML. This is a convenient short cut which you'll probably use in a lot of use cases. This method
can be combined with other methods of the PropertyMapperBuilder class. If you invoke it after you already
explicitly mapped some of the properties of a class, it will just map all remaining properties to attributes.
The second method let's you specify a mapping for a single property. You have to specify the name of the
property and optionally the name of the XML attribute. If you omit the second parameter the attribute name
will be the same as the property name (but camel-case notation transformed to dash-based notation, e.g.
pageCount to page-count.
Properties with a simple type like String, int, Boolean, Class or Date
can also be mapped to child text nodes, a mechanism very similar to mapping to attributes:
public class Song {
public var year:int;
public var title:String;
public var artist:String;
}
<song>
<year>1989</year>
<title>Monkey Gone To Heaven</title>
<artist>Pixies</artist>
</song>
The PropertyMapperBuilder class contains two methods that let you instruct the mapper to map to child text nodes:
public function mapAllToChildTextNodes () : void
public function mapToChildTextNode (propertyName:String, childName:QName = null) : void
The first method instructs the mapper to map all properties of the mapped
class to child text nodes in XML. This method
can be combined with other methods of the PropertyMapperBuilder class. If you invoke it after you already
explicitly mapped some of the properties of a class, it will just map all remaining properties to child text nodes.
The second method let's you specify a mapping for a single property. You have to specify the name of the
property and optionally the name of the XML child element. If you omit the second parameter the child element name
will be the same as the property name (but camel-case notation transformed to dash-based notation, e.g.
pageCount to page-count.
This is different from mapping to child text nodes. It maps a property to the text node that belongs to the same element. Since this can only apply for a single property it is often combined with attribute mapping like in the following example:
public class Song {
public var year:int;
public var title:String;
public var artist:String;
}
<song year="2000" artist="Goldfrapp">Felt Mountain</song>
Since the text node has no name it has to be mapped explicitly. This is how the mapping for the example above would be initialized:
var songBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Song, new QName("song"));
songBuilder.mapToTextNode("title");
songBuilder.mapAllToAttributes();
First we explicitly map the text node to the title property, then we map all remaining properties to attributes.
Mapping to child elements allows you to build a hierarchy of nested mappers like shown in the usage example in the beginning of this chapter.
public class Album {
public var year:int;
public var title:String;
public var artist:String;
public var songs:Array;
}
public class Song {
public var duration:String;
public var title:String;
}
<album year="2000" artist="Goldfrapp" title="Felt Mountain">
<song title="Lovely Head" duration="3:50"/>
<song title="Pilots" duration="4:30"/>
<song title="Deer Stop" duration="4:07"/>
<song title="Utopia" duration="4:18"/>
</album>
In this example the song child elements will be mapped into the songs property of
the Album class. This is how you would set up such a mapper:
var songBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Song, new QName("song"));
songBuilder.mapAllToAttributes();
var albumBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Album, new QName("album"));
albumBuilder.mapToChildElement("songs", songBuilder.build());
albumBuilder.mapAllToAttributes();
For the song element we simply map all properties to attributes. For the album element
we map the songs Array property to the song child element, passing the mapper that we have
built for the child element. We then map all remaining properties to attributes.
This is a variant for the child element mapping mechanism that allows for even greater flexibility. With choices you can map several different child elements into a single Array property (or single valued property).
public class Order {
public var products:Array;
}
public class Album {
public var artist:String;
public var title:String;
public var duration:String;
}
public class Book {
public var author:String;
public var title:String;
public var pageCount:String;
}
<order>
<album artist="Goldfrapp" title="Felt Mountain" duration="38:50"/>
<album artist="Unkle" title="Never, Never, Land" duration="49:27"/>
<book author="Karen Duve" title"Rain" pageCount="256"/>
<book author="Judith Hermann" title"Summerhouse, Later" pageCount="224"/>
</order>
This time we map the products Array property of the Order class to multiple different
child elements. This is how you set up the mapper:
var albumBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Album, new QName("album"));
albumBuilder.mapAllToAttributes();
var bookBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Book, new QName("book"));
bookBuilder.mapAllToAttributes();
var orderBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Order, new QName("order"));
var choice:Choice = new Choice();
choice.addMapper(albumBuilder.build());
choice.addMapper(bookBuilder.build());
orderBuilder.mapAllToChildElementChoice("products", choice);
The mappers for the Book and Album classes are simple: Both simply map all
properties to attributes. For the Order class we create a Choice instance,
add all child element mappers to it and finally map the products property of the Order
class to the choice.
Of course in all the examples we have shown you can also add [Required] metadata tags
for improved validation.
Finally there may be a scenario where none of the available mapping types are sufficient. In this case
you can create a custom mapper implementing the XmlObjectMapper element from scratch.
The interface is quite simple:
public interface XmlObjectMapper {
function get objectType () : ClassInfo;
function get elementName () : QName;
function mapToObject (element:XML, context:XmlProcessorContext) : Object;
function mapToXml (object:Object, context:XmlProcessorContext) : XML;
}
It specifies the class and the XML element name that should be mapped and then two methods for mapping in both directions. In case you have a large and complex XML structure where you can use existing property mappers for most of the tags, but need a custom mapper for a single tag, you can combine the builtin mappers with your custom one:
var albumBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Album, new QName("album"));
albumBuilder.mapAllToAttributes();
var bookBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Book, new QName("book"));
bookBuilder.mapAllToAttributes();
var orderBuilder:PropertyMapperBuilder = new PropertyMapperBuilder(Order, new QName("order"));
var choice:Choice = new Choice();
choice.addMapper(albumBuilder.build());
choice.addMapper(bookBuilder.build());
choice.addMapper(new MyCustomMapperImpl());
orderBuilder.mapAllToChildElementChoice("products", choice);