JAXB: Intercept during unmarshalling?
Question
I've got a typical web service using JAX-RS and JAXB, and upon unmarshalling I would like to know which setters were explicitly called by JAXB. This effectively lets me know which elements were included in the document provided by the caller.
I know I can probably solve this with an XmlAdapter
, but I have a lot of classes in a number of different packages, and I don't want to create adapters for each and every one of them. Nor do I want to put hooks into each and every setter. I would like a general solution if possible. Note that all of my classes are setup to use getters and setters; none of them use fields for the access type.
My service uses Jersey 2.4, Spring 3.2, and MOXy 2.5.1, so if there's anything that can be leveraged from any of those, that's all the better. Our original thought was we could dynamically create a factory class (akin to what @XmlType
supports) that would return a proxy object that would intercept the setters. We thought we could make this happen using the MetadataSource
concept in MOXy, but that does not seem to be possible.
Anyone have any ideas?
Solution
My service uses Jersey 2.4, Spring 3.2, and MOXy 2.5.1, so if there's anything that can be leveraged from any of those, that's all the better.
Create your own EclipseLink AttributeAccessor
MOXy (which is a component of EclipseLink) leverages a class called AttributeAccessor
to do operations with fields and properties. You could wrap this class to capture all the information that you need.
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.mappings.AttributeAccessor;
public class MyAttributeAccessor extends AttributeAccessor {
private AttributeAccessor attributeAccessor;
public MyAttributeAccessor(AttributeAccessor attributeAccessor) {
this.attributeAccessor = attributeAccessor;
}
@Override
public Object getAttributeValueFromObject(Object domainObject)
throws DescriptorException {
return attributeAccessor.getAttributeValueFromObject(domainObject);
}
@Override
public void setAttributeValueInObject(Object domainObject, Object value)
throws DescriptorException {
System.out.println("Thread: " + Thread.currentThread().getId() + " - Set value: " + value + " on property: " + attributeAccessor.getAttributeName() + " for object: " + domainObject);
attributeAccessor.setAttributeValueInObject(domainObject, value);
}
}
Tell MOXy to use your AttributeAccessor
We can leverage a SessionEventListener
to access the underlying metadata to specify your implementation of AttributeAccessor
. This is passed in as a property when creating the JAXBContext
.
Map<String, Object> properties = new HashMap<String, Object>(1);
properties.put(JAXBContextProperties.SESSION_EVENT_LISTENER, new SessionEventAdapter() {
@Override
public void postLogin(SessionEvent event) {
Project project = event.getSession().getProject();
for(ClassDescriptor descriptor : project.getOrderedDescriptors()) {
for(DatabaseMapping mapping : descriptor.getMappings()) {
mapping.setAttributeAccessor(new MyAttributeAccessor(mapping.getAttributeAccessor()));
}
}
super.preLogin(event);
}
});
JAXBContext jc = JAXBContext.newInstance(new Class[] {Foo.class}, properties);
Leverage a JAX-RS ContextResolver
when Creating the JAXBContext
Since you are in a JAX-RS environment you can leverage a ContextResolver
to control how the JAXBContext
is created.
Standalone Example
Java Model (Foo)
Below is a sample class where we will use field access (no setters).
import javax.xml.bind.annotation.*;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Foo {
private String bar;
private String baz;
}
Demo
import java.io.StringReader;
import java.util.*;
import javax.xml.bind.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.jaxb.JAXBContextProperties;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.sessions.*;
public class Demo {
public static void main(String[] args) throws Exception {
Map<String, Object> properties = new HashMap<String, Object>(1);
properties.put(JAXBContextProperties.SESSION_EVENT_LISTENER, new SessionEventAdapter() {
@Override
public void postLogin(SessionEvent event) {
Project project = event.getSession().getProject();
for(ClassDescriptor descriptor : project.getOrderedDescriptors()) {
for(DatabaseMapping mapping : descriptor.getMappings()) {
mapping.setAttributeAccessor(new MyAttributeAccessor(mapping.getAttributeAccessor()));
}
}
super.preLogin(event);
}
});
JAXBContext jc = JAXBContext.newInstance(new Class[] {Foo.class}, properties);
Unmarshaller unmarshaller = jc.createUnmarshaller();
StringReader xml = new StringReader("<foo><bar>Hello World</bar></foo>");
Foo foo = (Foo) unmarshaller.unmarshal(xml);
}
}
Output
Thread: 1 - Set value: Hello World on property: bar for object: forum21044956.Foo@37e47e38
UPDATE
So this works, but I have a few issues. First, the domainObject is always logging as 0 in my system. Not sure why that's occurring.
I have not idea why that is occuring, may need to check the toString()
for the object you are logging.
Second, I am not able to tell if the property in question is on the top-level item that is being unmarshalled or on a sub-element. That's actually quite annoying.
You will need to beef up the logic here. Based on the objects being set you should be able to do what you want.
Third, your solution is per JAXBContext, but I don't know if I really want to create a new context for every request. Isn't that bad from an overhead perspective?
You can cache the created JAXBContext
to prevent rebuilding it.