Set default value for fields not in XML in XStream
Asked Answered
K

3

11

Is there a way to create a converter or some operation that is performed after every single conversion? For context, I am trying to populate default values for fields that are not in my XML in order to maintain backwards compatibility if my data model changes. For instance, if I had this object:

class A {
    private String b;
    private String c;
    private String d;
}

and my XML was something like:

<a>
 <b>b</b>
 <d>d</d>
</a>

I want my import of the XML to know that there is a default value for the field c that is "c" and set it on A as such. This should be a generic operation to which I can add defaults to any field of a very complex graph. If there were some way to trigger a function after every conversion, it could check the current object against a map of objects I'd like to set a default value on.

Also note, that using readResolve/readObject does not seem to be an option since 1. readObject() never seemed to work for me at all and 2. readResolve would overwrite the field with the default value even if it were actually included in the XML. Please let me know if my assumptions here are wrong though.

Edit:: I found this related thread on the user mailing list: http://article.gmane.org/gmane.comp.java.xstream.user/4619/match=default+value

and it seems like the only suggested solution is to use readResolve() which I already said was not a valid solution.

Keddah answered 17/4, 2013 at 13:2 Comment(1)
You said "readResolve would overwrite the field with the default value even if it were actually included in the XML". You can write the readResolve() method to handle this case. if (c==null) {c="c";} I frequently use code like that and it works well.Unterwalden
V
14

Use the PureJavaReflectionProvider

XStream xstream = new XStream(new PureJavaReflectionProvider());

and just initialize your object with default values as usual. Either through field initialization or constructor code (initializer).

background

If you do not specify a ReflectionProvider xstream tries to find the best reflection provider. But the best ReflectionProvider for xstream might not be the best for you, because it usually selects the Sun14ReflectionProvider.

The Sun14ReflectionProvider uses the same instantiation strategy as the java serialization mechanism and that means that it skips constructor code or to be more precise - the object initializer code.

Therefore instance field initializations like

class A {
    private String b = "DEFAULT";
}

will NOT be applied and also constructor code will NOT be applied, e.g.

class A {
    private String b;

    public A(){
        b = "DEFAULT";
    }
}

The PureJavaReflectionProvider instead uses (as the name implies) the java reflection API to instantiate objects, e.g. Class.newInstance() and therefore object initialization code is executed.

Volk answered 20/4, 2015 at 12:19 Comment(2)
This approach does not work. Even PureJavaReflectionProvider sets default value as nullGum
this doesn't seem working with Scala where a class has a default value constructor, i.e. class A (String b = "")Prem
D
1

You'll need a converter.

Here is a code example for your case:

public class AConverter implements Converter {

    @Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    A a = new A();
    String bValue = "b";
    String cValue = "c";
    String dValue = "d";

    while (reader.hasMoreChildren()) {
        reader.moveDown();
        if ("b".equals(reader.getNodeName())) {
            bValue = reader.getValue();
        } else if ("c".equals(reader.getNodeName())) {
            cValue = reader.getValue();
        } else if ("d".equals(reader.getNodeName())) {
            dValue = reader.getValue();
        }
        reader.moveUp();
    }
    a.setB(bValue);
    a.setC(cValue);
    a.setD(dValue);

    return a;
}

@Override
public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
    A a = (A) object;
    writer.startNode("b");
    writer.setValue(a.getB());
    writer.endNode();
    writer.startNode("c");
    writer.setValue(a.getC());
    writer.endNode();
    writer.startNode("d");
    writer.setValue(a.getD());
    writer.endNode();


}

@Override
public boolean canConvert(Class clazz) {
    return clazz == A.class;
}

}

Do not forget to register the converter:

XStream xs = new XStream();
xs.registerConverter(new AConverter());

EDIT: Fixed the converter code.

Deflect answered 18/4, 2013 at 14:11 Comment(4)
Thanks pablosaraiva, but this will only work explicitly for some class A. I need this to work generically across any object in my full object graph. So it has to work for objects B/C/D, and any objects that B C D might have as well. Also, if additional fields were added in this case, it would require maintaining this converter. Ideally, I could just say something like newDefaultField(classWithNewDefault.class, newFieldName, newDefaultValue) any time I need to add a new default.Keddah
This is only a toy solution for the toy problem. You'll probably need a converter to every class with default values. You don't need to construct the objects by hand at the unmarshal, you can have a factory to take care of it. I hope it helps.Deflect
Right. I guess the goal is to avoid any real maintenance which could get complex and out of hand quickly. Unfortunately I don't think XStream offers a better solution for this beyond converters for each, or a readResolve() everywhere.Keddah
This a suitable option to set default values to members whose tag is missing in the XML when using fromXML(..) ; especially if the defaulting behavior affects only certain tags/members of - say - a specific class.Flaxen
A
1

c will be set to null when the XML does not have any value set for c. You can then check for null in your getter and return the appropriate default value.

Armalla answered 18/6, 2014 at 14:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.