Search code examples
javaxstream

Change Element tag based on the concrete implementation of Abstract class value


Say I have an abstract class named AbstractItem that is used as a field in another class. When I use XStream to generate the XML I want the element tag to be based on the concrete implementation of the instance of AbstractItem.

What I get:

<Test>
  <item class="Item1" name="name 1" description="description 1"/>
</Test>

What I want:

<Test>
  <Item1 name="name 1" description="description 1"/>
</Test>

I tried setting the alias on the XStream instance by doing:

stream.alias("Item1", Item1.class);

and also using:

stream.aliasType("Item1", Item1.class);

Neither one of the above worked.


For the sake of clarity here is a runnable example of the above:

Test.java

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.XStreamAlias;

@XStreamAlias("Test")
public class Test {

    public AbstractItem item;   

    public static void main(String[] args){

        Test t1 = new Test();
        Item1 item1 = new Item1();
        item1.name = "name 1";
        item1.description = "description 1";
        t1.item = item1;

        XStream stream = new XStream();
        stream.setMode(XStream.NO_REFERENCES);
        stream.autodetectAnnotations(true);
        stream.alias("Item1", Item1.class);

        System.out.println(stream.toXML(t1));
    }
}

AbstractItem.java

import com.thoughtworks.xstream.annotations.XStreamAsAttribute;

public abstract class AbstractItem {
    @XStreamAsAttribute
    public String name;
}

Item1.java

import com.thoughtworks.xstream.annotations.XStreamAsAttribute;

public class Item1 extends AbstractItem {
    @XStreamAsAttribute
    public String description;
}


UPDATE: I have attempted to do this using a converter class, but it still is not right:

stream.registerConverter(
        new Converter(){

            @Override
            public boolean canConvert(Class type) {
                if (AbstractItem.class.isAssignableFrom(type)){
                    return true;
                }
                return false;
            }

            @Override
            public void marshal(Object source, HierarchicalStreamWriter writer,
                    MarshallingContext context) {
                AbstractItem item = (AbstractItem)source;
                if(source instanceof Item1){
                    writer.startNode("Item1");
                    writer.addAttribute("description",((Item1)item).description);
                } else if(source instanceof Item2){
                    writer.startNode("Item2");
                    writer.addAttribute("description", ((Item2)item).description);
                } else {
                    writer.startNode("Item");
                }
                writer.addAttribute("name", item.name);
                writer.endNode();
            }

            @Override
            public Object unmarshal(HierarchicalStreamReader reader,
                    UnmarshallingContext context) {
                // TODO Auto-generated method stub
                AbstractItem item = null;
                String nodeName = reader.getNodeName();
                if (nodeName.equals("Item1")){
                    item = new Item1();
                    ((Item1)item).description = reader.getAttribute("description");
                } else if (nodeName.equals("Item2")){
                    item = new Item2();
                    ((Item2)item).description = reader.getAttribute("description");
                }  
                item.name = reader.getAttribute("name");
                return item;
            }
        });

The result I get now is:

<Test>
  <item class="Item1">
    <Item1 description="description 1" name="name 1"/>
  </item>
</Test>

Solution

  • I found the that the only way to accomplish this is with a custom converter for the class containing the Object I want to manipulate the element tag for. In the example in the question it would be a custom converter for the Test classand it would look like:

    import com.thoughtworks.xstream.converters.Converter;
    import com.thoughtworks.xstream.converters.MarshallingContext;
    import com.thoughtworks.xstream.converters.UnmarshallingContext;
    import com.thoughtworks.xstream.io.HierarchicalStreamReader;
    import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
    
    public class BasicConverter implements Converter {
    
        @Override
        public boolean canConvert(Class type) {
            return Test.class.isAssignableFrom(type);
        }
    
        @Override
        public void marshal(Object source, HierarchicalStreamWriter writer,
                MarshallingContext context) {
            if (((Test) source).item instanceof Item1) {
                writer.startNode("Item1");
                writer.addAttribute("description", ((Item1)((Test) source).item).description);
            } else if (((Test) source).item instanceof Item2) {
                writer.startNode("Item2");
                writer.addAttribute("description", ((Item2)((Test) source).item).description);
            }
            writer.addAttribute("name", ((Test) source).item.name);
            writer.endNode();
        }
    
        @Override
        public Object unmarshal(HierarchicalStreamReader reader,
                UnmarshallingContext context) {
            Test test = new Test();
    
            reader.moveDown();
            String nodeName = reader.getNodeName();
            AbstractItem item = null;       
            if (nodeName.equals("Item1")) {
                item = new Item1();
                ((Item1)item).description = reader.getAttribute("description");
            } else if (nodeName.equals("Item2")) {
                item = new Item2();
                ((Item2)item).description = reader.getAttribute("description");
            }   
            item.name = reader.getAttribute("name");    
            ((Test)test).item = item;
            reader.moveUp();
            return test;
        }
    }
    

    This gives the output I was looking for above, but to me is not really satisfactory. The reason being, is that the actual class I need to use this for has tons of fields, some that use their own custom converters, custom alias, etc. Plus it will essentially disregard all the annotations on the Test class, besides those defined at the class level. Plus as your class grows you have to update this converter to handle those new fields, or they will be included.

    Ideally I would like a converter that does everything as defined by annotations, except for certain fields. There currently is not one that I know of. What I am in the process of doing is extending the com.thoughtworks.xstream.converters.reflection.ReflectionConverter class to accomplish this. But it requires copying more code from the underlying implementation then I particularly care for.