Reputation: 144
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
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
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
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;
}
}
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;
}
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;
}
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:
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
Reputation: 1436
You can change to Integer
private Integer number;
Then the value of the object will be null when not instantiated.
Upvotes: 1
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