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. In version 2.3 support for metadata configuration was added, leading to a significantly easier setup.
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 {
[ChoiceType("com.foo.Book")]
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 details that probably need a bit of explanation are the [Required]
and [ChoiceType]
metadata tags.
The former gives hint to the validator. 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
20.9 Validation.
The [ChoiceType]
metadata tag lists the classes that are permitted as elements in the Array. All XML elements
that map to the Book class or any subtype are valid child elements in this case. If this tag would be omitted, all tags
would be allowed. For most mapping types there are sensible defaults. For simple property types like String and int the default
is to map them to XML attributes, the same way as if you would add the [Attribute]
metadata tag to the property declaration.
Next we create the mapper that is responsible for transforming this structure;
var mapper:XmlObjectMapper = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Order)
.mappedClasses(Book)
.build();
Here we are using the DSL for the mapper setup introduced with version 2.3. First we specify that our elements are unqualified, support for namespaces is also included of course. Then we have to specify the root element of our mapping and finally all other mapped classes used in any nested tags (in this case only one - Book). Metadata will be processed on the specified classes to look for any custom mappings.
The mapper we created is now ready to use. This is how we would map from XML to objects:
var xml:XML = ...;
var order:Order = mapper.mapToObject(xml) as Order;
And this is how we'd map from objects to XML:
var order:Order = ...;
var xml:XML = mapper.mapToXml(order);
Although the mappings support metadata configuration the most convenient way to set up your mappings is to rely on the defaults (configuration by convention). A third option to determine how a particular property should map to XML is programmatic setup.
Configuration Precedence
For each property of a class the mapper determines if and how it should be mapped by checking and applying configuration options in the following order:
Default Behaviours
The default behaviour if no programmatic or metadata configuration was applied is as follows:
String
, Number
, int
, uint
,
Boolean
, Date
, Class
and ClassInfo
, the default behaviour is to map to an XML attribute.
You can also map other types to attributes, but those would require an explicit [Attribute]
tag on the property (or
corresponding programmatic setup). [ChoiceType}
or [ChoiceId]
tags may be used to explicitly specify the permitted types.
See 20.6 Mapping Child Elements and 20.7 Mapping disjointed Choices for examples. Naming Conventions
As you can see in our first example in this chapter, we were mapping the pageCount
property
to the page-count
XML attribute. Per default the mapper always translates camel-case ActionScript names
into dash notation which is more common in XML. So using pageCount
as the attribute name in XML would
actually give you an error.
This is just the default behavior though and you can easily apply your own naming strategy:
var mapper:XmlObjectMapper = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Order)
.defaultNamingStrategy(new MyNamingStrategy());
.mappedClasses(Book)
.build();
NamingStrategy
is a trivial interface with just a single method that translates an ActionScript
name to an XML name.
The NamingStrategy is useful to change the convention globally. But you can also explicitly specify the name
for a single element or attribute. See the following section on 20.3 Mapping Attributes for an example
on how to do that for a property that maps to an XML attribute. If you want to specify the matching XML element
name for a class name, you can do that with the XmlMapping
tag:
[XmlMapping(elementName="product")]
public class ProductModel
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"
/>
Since mapping to XML attributes is the default for simple properties, no metadata configuration is required
on the properties in this case. You can use the [Attribute]
metadata tag whenever the property type
is not one of the simple types automatically mapped to attributes (see 20.2 Metadata Configuration and Defaults) or when the
name of the attribute does not match the property name:
[Attribute("song-title")]
public var title:String;
Otherwise the setup for such a mapper is straightforward:
var xml:XML = ...;
var song:Song = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Song)
.build()
.mapToObject(xml) as Song;
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 default for simple property types is to map them to XML attributes. Thus, for switching to text nodes in child elements you'd usually have to use explicit metadata configuration then:
[ChildTextNode]
public var year:int;
But if all or most of your XML elements follow this pattern, you can also switch the default globally and thus avoid any metadata configuration:
var xml:XML = ...;
var song:Song = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Song)
.defaultSimpleMappingType(SimpleMappingType.CHILD_TEXT_NODE)
.build()
.mapToObject(xml) as Song;
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 artist:String;
[TextNode]
public var title:String;
}
<song year="2000" artist="Goldfrapp">Felt Mountain</song>
This is how the mapping for the example above would be initialized:
var xml:XML = ...;
var song:Song = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Song)
.build()
.mapToObject(xml) as Song;
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;
[ChoiceType("com.foo.Song")]
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. Again we could also use the defaults, but that would allow any tag to be nested
inside the album
tag. That's why we explicitly specify the permitted type (including subtypes) with
the [ChoiceType]
metadata tag.
This is how you would set up such a mapper:
var xml:XML = ...;
var album:Album = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Album)
.mappedClasses(Song)
.build()
.mapToObject(xml) as Album;
In the preceding section we mapped to child elements through the use of the [ChoiceType]
tag, which allows
us to specify the permitted types for the elements of an Array in a polymorphic way. Sometimes though this is not sufficient
if the permitted types for an Array are not part of a distinct type hierarchy. In such a case a choice with a string
identifier can be used, so that you can explicitly specify which classes are permitted.
public class Order {
[ChoiceId("products")]
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>
In the mapper setup we then have to list all classes that should be associated with that choice id:
var xml:XML = ...;
var order:Order = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Order)
.choiceId("products", Album, Book)
.build()
.mapToObject(xml) as Order;
So far we only used unqualified XML elements in all examples to keep them simple. Therefor we always used
XmlObjectMappings.forUnqualifiedElements()
to start the mapper setup. But of course the mapper fully
supports namespaces, too. If you only use a single namespace the overall setup is quite similar to that for
unqualified elements. Consider the example from the preceding section, just adding a namespace to all elements:
<order xmlns="http://www.mynamespace.com">
<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>
To set up the corresponding mapper we then have to explicitly specify the namespace:
var xml:XML = ...;
var order:Order = XmlObjectMappings
.forNamespace("http://www.mynamespace.com")
.withRootElement(Order)
.choiceId("products", Album, Book)
.build()
.mapToObject(xml) as Order;
Finally, in some cases you may wish to work with multiple namespaces in the same document. Consider you'd want to add a separate namespace for software products to the example above:
<order
xmlns="http://www.mynamespace.com"
xmlns:sf="http://www.mynamespace.com/software">
<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"/>
<sf:software product="PageDesigner" version="4.0"/>
</order>
For such a scenario we'd set up separate groups of mappings for each namespace and then merge them:
var xml:XML = ...;
var softwareNS:XmlObjectMappings = XmlOhjectMappings
.forNamespace("http://www.mynamespace.com/software")
.withoutRootElement()
.choiceId("products", Software);
var order:Order = XmlObjectMappings
.forNamespace("http://www.mynamespace.com")
.withRootElement(Order)
.choidId("products", Album, Book)
.mergedMappings(softwareNS)
.build()
.mapToObject(xml) as Order;
As you see such a merged namespace often does not need a root element and you also do not need to call
build
on the namespaces to be merged. You can also see that merging namespaces also means to merge
all choices with the same id. This way the choice with the id products
in our example can contain
child elements from different namespaces.
Like shown in 20.1 Usage 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. Ignoring properties
Sometimes a class may contain properties that should not be mapped to XML. You can exclude individual
properties from the mapping with the [Ignore]
tag:
[Ignore]
public var somethingUnusual:String;
Without this tag (and without any other mapping tag on that property) the mapper would create the default mappings for that property.
Ignoring xml elements and attributes
In some scenarios you may not be able to create strict mappings as the XML may contain child elements or attributes
which are not relevant for the client side and should be ignored. To do that for a particular mapped class
you can add the [XmlMapping]
metadata to the class declaration:
[XmlMapping(ignoreUnmappedAttributes="true", ignoreUnmappedChildren="true")]
public class Order {
Without these settings any attribute or child element unknown to the mapper would lead to an Error.
There may be edge cases where neither the default settings nor the available metadata tags provide the required behaviour for a particular property. In these cases you can mix the default behaviour with explicit programmatic setup:
var xml:XML = ...;
var mappings:XmlObjectMappings = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Order);
mappings
.newMapperBuilder(Album)
.mapToAttribute("title")
.mapToChildTextNode("artist");
var order:Order = mappings
.choidId("products", Album, Book)
.build()
.mapToObject(xml) as Order;
In the example above we set up the mapper for the Album class programmatically. The mapper will still look for metadata or apply the default behaviour for all properties of the Album class that were not explicitly mapped. This would even allow to use metadata in the class and override it in specific scenarios through programmatic setup.
Similarly like with the metadata tags for Parsley configuration, you can create additional mapping tags
in case the builtin ones are not sufficient. For this you'd simply have to implement the MetadataMapperDecorator
interface:
public interface MetadataMapperDecorator {
function decorate (builder:MetadataMapperBuilder) : void;
}
For each metadata tag on any property of a mapped class the decorate
method would be invoked.
For full examples you may browse the existing implementations, as all builtin tags use this hook, too. They
reside in the package org.spicefactory.lib.xml.mapper.metadata
.
Like with the Parsley metadata tags, make sure that they meet the following requirements:
[Metadata]
tag on the class level. See the existing tags for examples.
For details on how mapping metadata to classes works in general you may wish to read 19.8 Mapping classes to metadata tags. Metadata.registerMetadataClass(MyTag)
for each of the implementations before you create
the first mapper. -keep-as3-metadata
option. Finally there may even 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 xml:XML = ...;
var order:Order = XmlObjectMappings
.forUnqualifiedElements()
.withRootElement(Order)
.customMapper(new MyCustomMapper())
.choidId("products", Album, Book)
.build()
.mapToObject(xml) as Order;