Of course the adapter will never be called if there isn't any element in the input to trigger that action. What happens in that example you're linked is that an attribute with an empty value is presented:
<element att="" />
The key here is that there is an att
attribute, but it has an empty String. So a JAXB unmarshaller is gonna present that to the setter. But, since there's an adapter declared on it, it will pass through there and get turned into a null value.
But if you had this
<element />
it's another story. There's no att
attribute, so the setter would never need to be called.
There's a difference between an element that occurs but has no content and a complete absence of an element. The former can basically be considered to contain an empty String, but the latter is just "not there".
EDIT: tested with these classes...
Bean.java
package jaxbadapter;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name="Test")
public class Bean {
@XmlElement
@XmlJavaTypeAdapter(NullStringAdapter.class)
private String someValue;
public Bean() {
}
public String getSomeValue() {
return someValue;
}
public void setSomeValue(final String someValue) {
this.someValue = someValue;
}
}
NullStringAdapter.java
package jaxbadapter;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class NullStringAdapter extends XmlAdapter<String, String> {
@Override
public String unmarshal(final String v) throws Exception {
if("".equals(v)) {
return null;
}
return v;
}
@Override
public String marshal(final String v) throws Exception {
if(null == v) {
return "";
}
return v;
}
}
ObjectFactory.java
package jaxbadapter;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.namespace.QName;
@XmlRegistry
public class ObjectFactory {
public ObjectFactory() {
}
public Bean createBean() {
return new Bean();
}
@XmlElementDecl(name = "Test")
public JAXBElement<Bean> createTest(Bean value) {
return new JAXBElement<>(new QName("Test"), Bean.class, null, value);
}
}
Main.java
package jaxbadapter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.transform.stream.StreamResult;
public class Main {
public static void main(String[] args) throws Exception {
final JAXBContext context = JAXBContext.newInstance("jaxbadapter");
final Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
final ObjectFactory of = new ObjectFactory();
final Bean b1 = new Bean();
final Bean b2 = new Bean();
b2.setSomeValue(null);
final Bean b3 = new Bean();
b3.setSomeValue("");
m.marshal(of.createTest(b1), System.out);
System.out.println("");
m.marshal(of.createTest(b2), System.out);
System.out.println("");
m.marshal(of.createTest(b3), System.out);
System.out.println("");
}
}
This is the output:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Test/>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Test/>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Test>
<someValue></someValue>
</Test>
Actually surprised me quite a bit. I've then tried changing the getter to return someValue == null ? "" : someValue;
to no avail. Then set a breakpoint on the getter and found out it never gets called.
Apparently JAXB uses reflection to try and retrieve the value rather than going through the setter when using XmlAccessType.FIELD
. Hardcore. Now, you can bypass this by using XmlAccessType.PROPERTY
instead and annotating either the getter or setter...
package jaxbadapter;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(name="Test")
public class Bean {
private String someValue;
public Bean() {
}
@XmlElement
@XmlJavaTypeAdapter(NullStringAdapter.class)
public String getSomeValue() {
return someValue;
}
public void setSomeValue(final String someValue) {
this.someValue = someValue;
}
}
... but that still didn't help. The adapter's marshal
method was only called once, on the last test case where an empty String had been set. Apparently it first calls the getter and when that returns null, it simply skips the adapter stuff altogether.
The only solution I can come up with is just foregoing the use of an adapter altogether here and put the substitution in the getter, making sure to use XmlAccessType.PROPERTY
:
package jaxbadapter;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(name="Test")
public class Bean {
private String someValue;
public Bean() {
}
@XmlElement
// @XmlJavaTypeAdapter(NullStringAdapter.class)
public String getSomeValue() {
return someValue == null ? "" : someValue;
}
public void setSomeValue(final String someValue) {
this.someValue = someValue;
}
}
That worked for me. It's only really an option if you're creating the JAXB-annotated classes yourself and not generating them via XJC from a schema, though.
Maybe someone can clarify a bit why adapters are skipped for nulls and if there's a way to change that behaviour.