After reading Blaise's post and the blog, it took me two days to come up with a working solution. First of all, the current status of MOXyJsonProvider
and ConfigurableMoxyJsonProvider
make it a pain-in-the-ass exprience to make it work and have never been designed for that.
My first test was to make a clean room implementation which is completely decoupled from Jersey and runs in a main
method.
Here is the main
method:
public static void main(String[] args) throws JAXBException {
Map<String, Object> props = new HashMap<>();
InputStream importMoxyBinding = MOXyTest.class
.getResourceAsStream("/json-binding.xml");
List<InputStream> moxyBindings = new ArrayList<>();
moxyBindings.add(importMoxyBinding);
props.put(JAXBContextProperties.OXM_METADATA_SOURCE, moxyBindings);
props.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
props.put(JAXBContextProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
JAXBContext jc = JAXBContext.newInstance("my.package",
CreateProjFolders.class.getClassLoader(), props);
Unmarshaller um = jc.createUnmarshaller();
InputStream json = MOXyTest.class
.getResourceAsStream("/CreateProjFolders.json");
Source source = new StreamSource(json);
JAXBElement<CreateProjFolders> create = um.unmarshal(source, CreateProjFolders.class);
CreateProjFolders folders = create.getValue();
System.out.printf("Used JAXBContext: %s%n", jc);
System.out.printf("Unmarshalled structure: %s%n", folders);
Marshaller m = jc.createMarshaller();
m.setProperty(MarshallerProperties.INDENT_STRING, " ");
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
System.out.print("Marshalled structure: ");
m.marshal(folders, System.out);
}
Here the json-binding.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
package-name="my.package"
xml-mapping-metadata-complete="false">
<xml-schema namespace="urn:namespace"
element-form-default="QUALIFIED" />
<java-types>
<java-type name="CreateProjFolders">
<xml-root-element />
<java-attributes>
<xml-element java-attribute="projFolders" name="createProjFolders" />
</java-attributes>
</java-type>
<java-type name="CreateProjFolder">
<java-attributes>
<xml-element java-attribute="access" name="access" />
</java-attributes>
</java-type>
<java-type name="Access">
<java-attributes>
<xml-element java-attribute="productionSites" name="productionSites" />
</java-attributes>
</java-type>
</java-types>
</xml-bindings>
and a sample input file:
{"parentItemId":"some-id",
"createProjFolders":[
{"objectNameEn":"bogus", "externalProjectId":"123456",
"access":{"productionSites":["StackOverflow"], "user":"michael-o"}}
]
}
Unmarshalling and marshalling work flawlessly. Now, how to make it work with Jersey? You can't because you cannot pass JAXBContext properties.
You need to copy MOXy's MOXyJsonProvider
and the entire source of Jersey Media MOXy except for XML stuff into a new Maven project because of the AutoDiscoverable
feature. This package will replace the original dependency.
Apply the following patches. Patches aren't perfect and can be imporoved because some code is duplicate, thus redundant but that can be done in a ticket later.
Now confiure that in your Application.class
:
InputStream importMoxyBinding = MyApplication.class
.getResourceAsStream("/json-binding.xml");
List<InputStream> moxyBindings = new ArrayList<>();
moxyBindings.add(importMoxyBinding);
final MoxyJsonConfig jsonConfig = new MoxyJsonConfig();
jsonConfig.setOxmMedatadataSource(moxyBindings);
ContextResolver<MoxyJsonConfig> jsonConfigResolver = jsonConfig.resolver();
register(jsonConfigResolver);
Now try it. After several calls on different models you will see JAXBExceptions
with 'premature end of file'. You will ask you why?! The reason is that the MOXyJsonProvider
creates and caches JAXBContexts
per domain class and not per package which means that your input stream is read several times but has already been closed after the first read. You are lost. You need to reset the stream but cannot change the inner guts of MOXy. Here is a simple solution for that:
public class ResetOnCloseInputStream extends BufferedInputStream {
public ResetOnCloseInputStream(InputStream is) {
super(is);
super.mark(Integer.MAX_VALUE);
}
@Override
public void close() throws IOException {
super.reset();
}
}
and swap your Application.class
for
moxyBindings.add(new ResetOnCloseInputStream(importMoxyBinding));
After you have felt the pain in the ass, enjoy the magic!
Final words:
- I did not accept Blaise's answer (upvote given) because it was only a fraction of a solution but lead me into the right direction.
- Both classes make it quite hard to solve a very simple problem, more suprisingly that I am appearantly the only one who wants to pass
OXM_METADATA_SOURCE
. Seriously?