Search code examples
javaxmlxstream

How to use addImplicitMap method of Xstream?


I want to serialize the class using xstream with the help of addImplicitMap method available in it. Class will look like:

class MapTest{
private Map<String, String> mapList;

public MapTest() {

    mapList= new HashMap<String, String>();
}

public void setServicesHealth(String id, String name) {
    map.put(id, name);
}

I tried like:

class MapTestMain{
public static void main(String args[]){ 
MapTest services = new MapTest();
services.setServicesHealth("ID01", "Jack");
services.setServicesHealth("ID02", "Neil);

    XStream stream = new XStream(new StaxDriver());
    stream.alias("MapTest", MapTest.class);
    stream.addImplicitMap(MapTest.class, "map", "id", String.class, "name");
    String xmlStr = stream.toXML(services);
    System.out.println(xmlStr);
  }
}

But I'm not getting the correct output. My expected output is like:

<?xml version="1.0" ?>
<MapTest>
 <id>Started</id>
 <name>Started</name>
</MapTest>

Kindly help me out...


Solution

  • TLDR: addImplicitMap will not allow you to remove the <mapList> container element and the entry elements and rename the key/value elements all at once (example 1 and 2). For more control over naming of elements in maps, use NamedMapConverter (example 3). To be able to do everything at once, you will need a custom Converter implementation (example 4).


    Long answer:

    There doesn't appear to be a good explanation on SO of what addImplicitMap does, and it's also not very clear in their documentation, so I'm going to try to explain here.

    addImplicitMap will remove the container element but, when used like this, it will not let you remove the <entry> element or rename the key/value elements. For example: (1)

    stream.addImplicitMap(MapTest.class, "mapList", "s", Map.Entry.class, null);
    

    Results in:

    <MapTest>
      <s>
        <string>ID01</string>
        <string>Jack</string>
      </s>
      <s>
        <string>ID02</string>
        <string>Neil</string>
      </s>
    </MapTest>
    

    Alternatively, addImplicitMap lets you store only the values of a map when writing (omitting the container element and key elements). When reading XML it recreates the map keys using a specified field of the value objects, so the map key must be stored in the map value object.

    For example, we can make the map's value a Service object with an ID attribute and make Xstream store just the values and recreate the map from them: (2)

    private Map<String, Service> serviceMap; // modified in MapTest
    
    static class Service {
      private String id, name;
    
      public Service(String id, String name) {
        this.id = id;
        this.name = name;
      }
    }
    
    MapTest services = new MapTest();
    services.setService("ID01", new Service("ID01", "Jack"));
    services.setService("ID02", new Service("ID02", "Neil"));
    
    stream.addImplicitMap(MapTest.class, "serviceMap", "s", Service.class, "id");
    

    Outputs:

    <MapTest>
      <s>
        <id>ID01</id>
        <name>Jack</name>
      </s>
      <s>
        <id>ID02</id>
        <name>Neil</name>
      </s>
    </MapTest>
    

    Note that here each <s> element is actually the serialized Service object, not a Map.Entry object.


    This still doesn't quite solve your problem (and we had to change what was in the map too), so we can instead try the namedMapConverter. If we go back to using a map of <String, String> as you had originally, we can use: (3)

    stream.registerConverter(new NamedMapConverter(stream.getMapper(),
                    null, "id", String.class, "name", String.class));
    

    Which gives the output:

    <MapTest>
      <serviceMap>
        <id>ID01</id>
        <name>Jack</name>
        <id>ID02</id>
        <name>Neil</name>
      </serviceMap>
    </MapTest>
    

    Still not quite right, and I don't believe you can make it better without implementing a custom Converter class (there's a good tutorial here). So we add the line (instead of the NamedMapConverter):

    stream.registerConverter(new MapTestConverter());
    

    And use a custom Converter implementation: (4)

    static class MapTestConverter implements Converter {        
        public boolean canConvert(Class type) {
            return type.equals(MapTest.class);
        }
    
        public void marshal(Object source, HierarchicalStreamWriter writer,
                MarshallingContext context) {
            MapTest mt = (MapTest) source;
            for (Entry<String, String> e : mt.serviceMap.entrySet()) {
                writer.startNode("id");
                writer.setValue(e.getKey());
                writer.endNode();
                writer.startNode("name");
                writer.setValue(e.getValue());
                writer.endNode();
            }
        }
    
        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
            MapTest mt = new MapTest();
            String id = null;
    
            while (reader.hasMoreChildren()) {
                reader.moveDown();
                if ("id".equals(reader.getNodeName())) {
                    if (id != null) { throw new RuntimeException("Malformed XML, ID was set twice: " + id); }
                    id = (String) context.convertAnother(mt, String.class);
                } else if ("name".equals(reader.getNodeName())) {
                    String name = (String) context.convertAnother(mt, String.class);
                    if (id == null) { throw new RuntimeException("Malformed XML: Found name without ID: " + name); }
                    mt.serviceMap.put(id, name);
                    id = null;
                }
                reader.moveUp();
            }
            return mt;
        }
    }
    

    Finally we get the desired result:

    <MapTest>
      <id>ID01</id>
      <name>Jack</name>
      <id>ID02</id>
      <name>Neil</name>
    </MapTest>
    

    (sorry I don't have enough rep to make the following into links)

    Full code for addImplicitMap example: pastebin.com/MYiAde3m

    Full code for namedMapConverter example: pastebin.com/kVChup5x

    Full code for Converter example: pastebin.com/vUTwaHkk