XStream is a great utility for
serializing objects to XML ? and then back again.
The default XML output is neat enough ? albeit a little verbose (using fully
qualified class names for example). There are plenty of tutorials available
showing how to use XStream aliases to clean up the output (e.g. Manual Tweaking
Output).
However, sometimes it is not enough to just tweak and alias ? more serious
manipulation of the output stream is required. For example, complicated
Collection-based objects come out very cumbersome (but generic ? which is the
point). Luckily, XStream provides a powerful interface for custom generation (A
Converter
in XStream-speak).
To fully control how an object is serialised, create a new class which
implements the Converter
interface:
01 |
import com.thoughtworks.xstream.converters.Converter;
|
02 |
import com.thoughtworks.xstream.converters.MarshallingContext;
|
03 |
import com.thoughtworks.xstream.converters.UnmarshallingContext;
|
04 |
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
|
05 |
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
|
07 |
public class MyCustomMapConverter
implements Converter { |
10 |
public void marshal(Object source, HierarchicalStreamWriter writer,
|
11 |
MarshallingContext context) {
|
16 |
public boolean
canConvert(Class clazz) {
|
17 |
return clazz.equals(ClassToBeConverted. class );
|
21 |
public Object
unmarshal(HierarchicalStreamReader reader,
|
22 |
UnmarshallingContext context) {
|
As can be seen above, the canConvert
method tells XStream which
objects should use this converter. The other two methods are called when
converting objects to XML (marshal
) or converting XML to objects
(unmarshal
).
Marshalling the object is achieved by starting and ending Nodes, while
populating their attributes and values. So for example:
2 |
public void marshal(Object source,
HierarchicalStreamWriter writer, MarshallingContext context) {
|
3 |
for (RouterConfigRuleMap
routerConfigRuleMap : routerConfigRuleMapSet.getRouterConfigRuleMapSet())
|
5 |
writer.startNode( "routetype" );
|
6 |
writer.addAttribute( "type" ,
routerConfigRuleMap.getRouteType().toString());
|
will iterate through a collection generating the following output:
1 |
< routetype type = "example" /> |
2 |
< routetype type = "foo" /> |
These patterns can become arbitrarily complex.
Unmarshalling is a little more complicated. XStream will work out which
(registered ? see later) Converter to use by matching the node to the class (in
my head these seems like an extremely difficult task… I’m not sure how XStream
has implemented this ? maybe I should look into this!) The node (and all
children) will be passed to the unmarshal
method.
It is up to us to define how the target object is populated from these nodes.
Methods are provided to:
- Check if the node has children (
reader.hasMoreChildren()
)
- Move down to the child level (
reader.moveDown()
)
- Move back up to the parent level (
reader.moveUp()
)
The moveUp/Down
methods can be called as many times as necessary
to walk the tree.
I provide a full example as I couldn’t find many good ones on the net:
02 |
public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) { |
04 |
RouterConfigRuleMapSet routerConfigRuleMapSet = new RouterConfigRuleMapSet();
|
06 |
while (reader.hasMoreChildren()) {
|
09 |
RouterConfigRuleMap routerConfigRuleMap = new RouterConfigRuleMap();
|
10 |
routerConfigRuleMap.setRouteType(RouteType.valueOf(reader.getAttribute( "type" )));
|
12 |
while (reader.hasMoreChildren()) {
|
14 |
RouterConfigRule routerConfigRule = new RouterConfigRule();
|
16 |
if (reader.getAttribute( "extractor" ) != null ) {
|
17 |
routerConfigRule.setExtractor(reader.getAttribute( "extractor" ));
|
20 |
if (reader.getAttribute( "formatter" ) != null ) {
|
21 |
routerConfigRule.setFormatter(reader.getAttribute( "formatter" ));
|
24 |
if (reader.getAttribute( "distributor" ) !=
null ) {
|
25 |
routerConfigRule.setDistributor(reader.getAttribute( "distributor" ));
|
28 |
routerConfigRuleMap.putRouterConfigRule(reader.getAttribute( "name" ), routerConfigRule);
|
33 |
routerConfigRuleMapSet.addRouterConfigRuleMapSet(routerConfigRuleMap);
|
35 |
return routerConfigRuleMapSet;
|
Out of completeness, here is the equivalent marshal
implementation:
02 |
public void marshal(Object source,
HierarchicalStreamWriter writer, MarshallingContext context) {
|
04 |
RouterConfigRuleMapSet routerConfigRuleMapSet =
(RouterConfigRuleMapSet)source; |
05 |
for (RouterConfigRuleMap
routerConfigRuleMap : routerConfigRuleMapSet.getRouterConfigRuleMapSet())
|
07 |
writer.startNode( "routetype" );
|
08 |
writer.addAttribute( "type" ,
routerConfigRuleMap.getRouteType().toString());
|
10 |
for (Object routerConfig :
routerConfigRuleMap.routerConfigRuleMap.entrySet()) {
|
11 |
Entry<String, RouterConfigRule> entry =
(Entry<String, RouterConfigRule>) routerConfig;
|
12 |
RouterConfigRule routerConfigRule = entry.getValue();
|
14 |
writer.startNode( "routename" );
|
15 |
writer.addAttribute( "name" ,
entry.getKey().toString()); |
16 |
if (routerConfigRule.getExtractor() != null ) {
|
17 |
writer.addAttribute( "extractor" ,
routerConfigRule.getExtractor()); |
20 |
if (routerConfigRule.getFormatter() != null ) {
|
21 |
writer.addAttribute( "formatter" ,
routerConfigRule.getFormatter()); |
24 |
if (routerConfigRule.getDistributor() != null ) {
|
25 |
writer.addAttribute( "distributor" ,
routerConfigRule.getDistributor()); |
To register the Converter
simply add the following line when you
instantiate the XStream object:
1 |
XStream stream = new XStream();
|
2 |
stream.registerConverter( new RouterConfigMapConverter()); |