Quick JAXB codec (deserialize + serialize)

I never liked JAXB because it messes with the concept of tolerant API. It sometimes is very strict when it validates the user inputs, no matter if the problem is recoverable or not.

I wrote several months ago a mapper that is able to deserialize a xml text into a recursive Map of xml elements.

The reality in our daily jobs is that sometimes the company we work for enforces the use of JAXB.

So to make things a bit less painful, I rewrote from scratch a naive codec called XmlMapper and that sound a bit like the jackson XmlMapper.

Feel free to copy / paste some slices of the following snippet.

package com.company.api.infra.messages;

import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

import static java.util.Collections.synchronizedMap;
import static java.util.Optional.ofNullable;

public class XmlMapper {

private static final Map<String, JAXBContext> CONTEXT_BY_PACKAGE = synchronizedMap(new HashMap<>());
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = Stream.of(DocumentBuilderFactory
.newInstance()).peek(factory -> {
factory.setValidating(true);
factory.setNamespaceAware(true);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
try {
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
} catch (AbstractMethodError abstractMethodError) {
// Unit test mode
} catch (ParserConfigurationException e) {
throw new IllegalStateException("cannot deactivate external entities from the xml deserializer, " +
"that is a big security concern");
}
}).findFirst().get();

public static <T> Node valueToTree(T value, String rootName, String namespace) throws
IOException, SAXException, ParserConfigurationException, JAXBException {
return readTree(writeValue(value, rootName, namespace));
}

public static <T> T treeToValue(Node node, Class<T> rootObjectClass)
throws JAXBException {
return treeToValue(node, rootObjectClass, false);
}

@SuppressWarnings("unchecked")
public static <T> T treeToValue(Node node, Class<T> rootObjectClass, boolean forceTheClassMapping)
throws JAXBException {
if (node instanceof JAXBElement) {
JAXBElement<T> element = (JAXBElement<T>) node;
return element.getValue();
}
Unmarshaller unmarshaller = ensuredCreatedJaxbContextFor(rootObjectClass).createUnmarshaller();
return (T) (forceTheClassMapping ?
unmarshaller.unmarshal(node, rootObjectClass) :
((JAXBElement<T>)unmarshaller.unmarshal(node))).getValue();
}

@SuppressWarnings("unchecked")
public static <T> String writeValue(T value, String rootName, String namespace) throws UnsupportedEncodingException {
Class<T> valueClass = (Class<T>) value.getClass();
String nodeName = ofNullable(rootName).orElse(valueClass.getSimpleName());
return Stream.of(new ByteArrayOutputStream()).peek(outputStream -> {
try {
ensuredCreatedJaxbContextFor(valueClass).createMarshaller().marshal(
new JAXBElement<T>(new QName(namespace, nodeName), valueClass, value), outputStream);
} catch (JAXBException e) {
throw new IllegalStateException("cannot marshall this value " + value +
"please have a look");
}
}).findFirst().get().toString("UTF-8");
}

public static Node readTree(String any) throws IOException, ParserConfigurationException, SAXException {
return DOCUMENT_BUILDER_FACTORY
.newDocumentBuilder()
.parse(new ByteArrayInputStream(
(any.startsWith("<?") ? any : "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + any)
.substring(any.indexOf("?>") + 2).getBytes()))
.getDocumentElement();
}

private static <T> JAXBContext ensuredCreatedJaxbContextFor(Class<T> objectClass) {
return ofNullable(CONTEXT_BY_PACKAGE.get(objectClass.getPackage().getName())).orElseGet(() -> {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(objectClass.getPackage().getName());
CONTEXT_BY_PACKAGE.put(objectClass.getPackage().getName(), jaxbContext);
return jaxbContext;
} catch (JAXBException e) {
throw new IllegalStateException("cannot instantiate a jaxb context for " + objectClass +
"please have a look");
}
});
}
}

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.