Wolsie
Wolsie

Reputation: 144

Prevent writing default attribute values JAXB

I am trying to write the class in order to write an element with attributes in JAXB. In this XML there are some default values whether they be Strings, ints, or custom class types.

The following cut down example:

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "FIELD")
public class TestLayoutNode
{
    // I want to not write this to the xml when it is 0
    @XmlAttribute(name = "num")
    private int number;

    // I want to not write this when it is "default"
    @XmlAttribute(name = "str")
    private String str;
}

As per JAXB Avoid saving default values I know if I want to not write the String I can modify the getters/setters to write null and read in the default value if it reads in null.

However, with the int I am not sure what to do as it will always have the value 0 unless it is specifically changed.

Is there a nicer way to do this? I could change the internal data types to String and then cast it whenever it is needed but that's a bit messy.

Upvotes: 5

Views: 9406

Answers (5)

laszlok
laszlok

Reputation: 2515

I find the solution using custom getters/setters or adapters annoyingly verbose, so I went for a different solution: a marshaller that checks values and nulls them out if they are at default.

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Set;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.bind.helpers.AbstractMarshallerImpl;
import javax.xml.transform.Result;
import com.google.common.collect.ImmutableSet;

class MyJaxbMarshaller extends AbstractMarshallerImpl {
    /** See https://docs.oracle.com/cd/E13222_01/wls/docs103/webserv/data_types.html#wp221620 */
    private static final Set<String> SUPPORTED_BASIC_TYPES = ImmutableSet.of(
            "boolean", "java.lang.Boolean", "byte", "java.lang.Byte", "double", "java.lang.Double",
            "float", "java.lang.Float", "long", "java.lang.Long", "int", "java.lang.Integer",
            "javax.activation.DataHandler", "java.awt.Image", "java.lang.String",
            "java.math.BigInteger", "java.math.BigDecimal", "java.net.URI", "java.util.Calendar",
            "java.util.Date", "java.util.UUID", "javax.xml.datatype.XMLGregorianCalendar",
            "javax.xml.datatype.Duration", "javax.xml.namespace.QName",
            "javax.xml.transform.Source", "short", "java.lang.Short");
    private final Marshaller delegate;

    MyJaxbMarshaller(Marshaller delegate) {
        this.delegate = delegate;
    }

    @Override
    public void setProperty(String name, Object value) throws PropertyException {
        super.setProperty(name, value);
        delegate.setProperty(name, value);
    }

    @Override
    public void marshal(Object jaxbElement, Result result) throws JAXBException {
        try {
            delegate.marshal(clearDefaults(jaxbElement), result);
        } catch (ReflectiveOperationException ex) {
            throw new JAXBException(ex);
        }
    }

    private Object clearDefaults(Object element) throws ReflectiveOperationException {
        if (element instanceof Collection) {
            return clearDefaultsFromCollection((Collection<?>) element);
        }
        Class<?> clazz = element.getClass();
        if (isSupportedBasicType(clazz)) {
            return element;
        }
        Object adjusted = clazz.getConstructor().newInstance();
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            copyOrRemove(field, element, adjusted);
        }
        return adjusted;
    }

    private Object clearDefaultsFromCollection(Collection<?> collection)
            throws ReflectiveOperationException {
        @SuppressWarnings("unchecked")
        Collection<Object> result = collection.getClass().getConstructor().newInstance();
        for (Object element : collection) {
            result.add(clearDefaults(element));
        }
        return result;
    }

    private static boolean isSupportedBasicType(Class<?> clazz) {
        return SUPPORTED_BASIC_TYPES.contains(clazz.getName());
    }

    private void copyOrRemove(Field field, Object element, Object adjusted)
            throws ReflectiveOperationException {
        Object value = field.get(element);
        if (value != null) {
            if (value.equals(field.get(adjusted))) {
                value = null;
            } else {
                value = clearDefaults(value);
            }
        }
        field.set(adjusted, value);
    }
}

This works with classes like

@XmlRootElement
public class Foo {
    @XmlAttribute public Integer intAttr = 0;
    @XmlAttribute public String strAttr = "default";
}

You can make this more flexible if you want, e.g. you can use an annotation to mark attributes you want to omit when they're at default, or extend the class to be aware of things like @XmlTransient or method accessors (neither of which is an issue in my project right now).

The price you pay for the simplicity of your binding classes is that the marshaller is going to create a deep copy of the object you're about to marshal, and make lots of comparisons to defaults to determine what to null out. So if runtime performance is an issue for you, this might be a no-go.

Upvotes: 0

Vladimir Nesterovsky
Vladimir Nesterovsky

Reputation: 649

Though it's not as terse as one would wish, one can create XmlAdapters to avoid marshalling the default values.

The use case is like this:

@XmlRootElement(name = "FIELD")
public class TestLayoutNode
{
  @XmlAttribute(name = "num")
  @XmlJavaTypeAdapter(value = IntegerZero.class, type = int.class)
  public int number;

  @XmlAttribute(name = "str")
  @XmlJavaTypeAdapter(StringDefault.class)
  public String str = "default";
}

And here are adapters.

IntegerZero:

public class IntegerZero extends DefaultValue<Integer>
{
  public Integer defaultValue() { return 0; }
}

StringDefault:

public class StringDefault extends DefaultValue<String>
{
  public String defaultValue() { return "default"; }
}

DefaultValueAdapter:

public class DefaultValue<T> extends XmlAdapter<T, T>
{
  public T defaultValue() { return null; }

  public T marshal(T value) throws Exception
  {
    return (value == null) || value.equals(defaultValue()) ? null : value;
  }

  public T unmarshal(T value) throws Exception
  {
    return value;
  }
}

With small number of different default values this approach works well.

Upvotes: 0

bdoughan
bdoughan

Reputation: 148977

You could do the following by changing the fields to be the object types by default null values do not appear in the XML representation) and putting some logic in the getters:

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "FIELD")
public class TestLayoutNode
{

    @XmlAttribute(name = "num")
    private Integer number;

    @XmlAttribute
    private String str;

    public int getNumber() {
        if(null == number) {
           return 0;
        } else {
           return number;
        }
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getStr() {
        if(null == str) {
            return "default";
        } else {
            return str;
        }
    }

    public void setStr(String str) {
        this.str = str;
    }
}

Allowing the Property to be Unset

If you want to allow the set operation to return a property to its default state then you need to add logic in the set method.

public void setNumber(int number) {
    if(0 == number) {
        this.number = null;
    } else {
        this.number = number;
    }
 }

Alternatively you could offer an unset method:

public void unsetNumber() {
    this.number = null;
}

Allowing a Set to null

If you want to allow the str property to be set to null so that the get method will return null and not "default" then you can maintain a flag to track if it has been set:

    private strSet = false;

    public String getStr() {
        if(null == str && !strSet) {
            return "default";
        } else {
            return str;
        }
    }

    public void setStr(String str) {
        this.str = str;
        this.strSet = true;
    }

UPDATE

Blaise, don't you think that the solution is pretty verbose?

Yes

I mean that such use case should be probably supported by framework. For example using annotation like @DefaultValue.

How JAXB Supports Default Values Today

If a node is absent from the XML then a set is not performed on the corresponding field/property in the Java Object. This means whatever value you have initialized the property to be is still there. On a marshal since the value is populated it will be marshalled out.

What is Really Being Asked For

What is really being asked for is to not marshal the field/property when it has the default value. In this way you want the marshal behaviour to be the same for null and default values. This introduces some problems to be solved:

  1. How do you now marshal null to XML? By default is it still marshalled as a missing node?
  2. Does a mechanism need to be provided to distinguish between the property being the default value (not present in the XML) and having been set to the same value as the default (present in the XML)?

What Are People Doing Today?

Generally for this use case people would just change the int property to Integer and have null be the default. I haven't encountered someone asking for this behaviour for a String before.

Upvotes: 3

Tzen
Tzen

Reputation: 1436

You can change to Integer

private Integer number;

Then the value of the object will be null when not instantiated.

Upvotes: 1

MihaiC
MihaiC

Reputation: 1583

Use Integer instead of primitive int. Replace all primitive types with their object counterparts, then you can use NULL.

As per the string default value, use and modify the getter

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "FIELD")
public class NullAttrs {

    private Integer number;
    private String str;

    public void setNumber(Integer number) {
        this.number = number;
    }

    @XmlAttribute(name = "num")
    public Integer getNumber() {
        return number;
    }

    public void setStr(String str) {
        this.str = str;
    }

    @XmlAttribute(name = "str")
    public String getStr() {
        if (str != null && str.equalsIgnoreCase("default"))
         return null;
        else if (str == null)
         return "default";
        else
         return str;
    }

    public static void main(String[] args) throws JAXBException {
        JAXBContext jc = JAXBContext.newInstance(NullAttrs.class);

        NullAttrs root = new NullAttrs();
        root.setNumber(null);
        root.setStr("default");

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }
}

Result in this case would be, empty FIELD:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<FIELD/>

Upvotes: 1

Related Questions