Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JAXB / Jersey - JSONObject as XmlElement

I'm using Jersey (1.9.1) and utilizing JAXB for my objects, and trying to add a field which is simply a JSON object, ideally, of type Jackson's JsonNode, but I can also work with jettison's JSONObject (or even GSON's JsonObject/JsonElement)

The reason for that is since I want to allow a 'free-form' json to be sent (as the obj element in our example), in addition to other elements on that same object (which are using JAXB annotations)

My object:

import org.codehaus.jettison.json.JSONObject;

@XmlRootElement(name="moo")
@XmlAccessorType(XmlAccessType.FIELD)

public class Moo {

    @XmlElement
    JSONObject obj;

    ...
}

My resource:

@Path("/moo")
@POST
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public Response postMsg(Moo e) {
    System.out.println(e);

    return Response.status(200).entity(e).build();
}

Payload:

{"obj": {"my": "custom", "object": 1, "here": [1, 2, 3] } }

When executing a POST request with the above - obj is an empty JSONObject. The same thing happened when I tried using Jackson's JsonNode and GSON's JsonElement/JsonObject.

However, when I use JSONObject as the arg for the method, its able to parse it

@Path("/moo")
@POST
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public Response postMsg(JSONObject e) {
    System.out.println(e);

    return Response.status(200).entity(e).build();
}

I assume this happens since Jersey uses its own com.sun.jersey.json.impl.provider.entity.JSONObjectProvider to for marshalling, while JAXB uses its own marshller.

Is there a way around this while still using JAXB's annotation (for other fields)? I tried playing around with the a custom XmlAdapter (using the @XmlJavaTypeAdapter annotation) without any success (since the Object I got as the ValueType is a DOM element)

My final target is to have the field obj as Jackson representation (JsonNode).

Any directions would be much appreciated.

like image 947
natiz Avatar asked Aug 23 '17 15:08

natiz


2 Answers

If I got it right, you want to embedd a raw json for free-from values.

@XmlRootElement(name="moo")
@XmlAccessorType(XmlAccessType.FIELD)
public class Moo {
    Object json;

    @JsonRawValue
    public String getJson() {
        // default raw value: null or "[]"
        return json == null ? null : json.toString();
    }

    public void setJson(JsonNode node) {
        this.json = node;
    }
}

Taken from here.

like image 85
sschrass Avatar answered Oct 21 '22 09:10

sschrass


There are multiple problems First of all this code

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.sun.xml.bind.marshaller.CharacterEscapeHandler;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.*;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.validation.Schema;
import javax.xml.validation.TypeInfoProvider;
import javax.xml.validation.Validator;
import javax.xml.validation.ValidatorHandler;
import java.io.File;
import java.io.IOException;
import java.io.Writer;

public class TestObjectNode {

    @XmlRootElement(name = "moo")
    @XmlAccessorType(XmlAccessType.FIELD)
    static class Moo {
        @JsonProperty("obj")
        @XmlElement
        @XmlJavaTypeAdapter(ObjMarshaller.class)
        JsonNode obj;
    }

    static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    static final XmlMapper XML_MAPPER = (XmlMapper) new XmlMapper()
            .disable(SerializationFeature.WRAP_ROOT_VALUE)
            .disable(DeserializationFeature.UNWRAP_ROOT_VALUE);

    public static void main(String[] args) throws Exception {
        final String value = "{\"obj\": {\"my\": \"custom\", \"object\": 1, \"here\": [1, 2, 3] } }";
        Moo moo = JSON_MAPPER.readValue(value, Moo.class);
        System.out.println(moo.obj);
        saveToFile(moo);
        System.out.println(XML_MAPPER.writeValueAsString(moo));
        moo = readFromFile();
        System.out.println(moo.obj);
    }

    private static void saveToFile(Moo moo) throws Exception {
        File file = new File("moo.xml");
        JAXBContext jaxbContext = JAXBContext.newInstance(Moo.class);
        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
        jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        jaxbMarshaller.setProperty(CharacterEscapeHandler.class.getName(),
                new CharacterEscapeHandler() {
                    @Override
                    public void escape(char[] chars, int i, int i1, boolean b, Writer writer) throws IOException {
                        writer.write(chars, i, i1);
                    }
                });

        jaxbMarshaller.marshal(moo, file);
        jaxbMarshaller.marshal(moo, System.out);
    }

    private static Moo readFromFile() throws Exception {
        File file = new File("moo.xml");
        JAXBContext jaxbContext = JAXBContext.newInstance(Moo.class);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        MySchema mySchema = new MySchema();
        unmarshaller.setSchema(mySchema);
        Moo moo = (Moo) unmarshaller.unmarshal(file);
        String objectNode = mySchema.validatorHandler.getObjectNodeString();
        System.out.println("objectNode = " + objectNode);
        JsonNode jsonNode = XML_MAPPER.readValue(objectNode, ObjectNode.class);
        moo.obj = jsonNode;
        return moo;
    }

    private static class ObjMarshaller extends XmlAdapter<String, JsonNode> {

        @Override
        public JsonNode unmarshal(String v) throws Exception {
            throw new IllegalArgumentException("i'm never here///");
        }

        @Override
        public String marshal(JsonNode v) throws Exception {
            return XML_MAPPER.writeValueAsString(v);
        }
    }

    static class MySchema extends Schema {

        final ValidatorHandlerImpl validatorHandler = new ValidatorHandlerImpl();

        @Override
        public Validator newValidator() {
            return null;
        }

        @Override
        public ValidatorHandler newValidatorHandler() {
            return validatorHandler;
        }
    }

    static class ValidatorHandlerImpl extends ValidatorHandler {

        private final StringBuilder objectNode = new StringBuilder();
        private boolean insideObjectNode = false;

        public String getObjectNodeString() {
            return objectNode.toString();
        }

        @Override
        public void setContentHandler(ContentHandler receiver) {

        }

        @Override
        public ContentHandler getContentHandler() {
            return null;
        }

        @Override
        public void setErrorHandler(ErrorHandler errorHandler) {

        }

        @Override
        public ErrorHandler getErrorHandler() {
            return null;
        }

        @Override
        public void setResourceResolver(LSResourceResolver resourceResolver) {

        }

        @Override
        public LSResourceResolver getResourceResolver() {
            return null;
        }

        @Override
        public TypeInfoProvider getTypeInfoProvider() {
            return null;
        }

        @Override
        public void setDocumentLocator(Locator locator) {

        }

        @Override
        public void startDocument() throws SAXException {

        }

        @Override
        public void endDocument() throws SAXException {

        }

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException {

        }

        @Override
        public void endPrefixMapping(String prefix) throws SAXException {

        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            if(localName.equals("ObjectNode")) {
                insideObjectNode = true;
            }
            if (insideObjectNode) {
                objectNode.append("<" + localName + ">");
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (insideObjectNode) {
                objectNode.append("</" + localName + ">");
            }
            if(localName.equals("ObjectNode")) {
                insideObjectNode = false;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (insideObjectNode) {
                objectNode.append(ch, start, length);
            }
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {

        }

        @Override
        public void processingInstruction(String target, String data) throws SAXException {

        }

        @Override
        public void skippedEntity(String name) throws SAXException {

        }
    }

}

Provides following output

{"my":"custom","object":1,"here":[1,2,3]}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<moo>
    <obj><ObjectNode><my>custom</my><object>1</object><here>1</here><here>2</here><here>3</here></ObjectNode></obj>
</moo>
<Moo><obj><my>custom</my><object>1</object><here>1</here><here>2</here><here>3</here></obj></Moo>
objectNode = <ObjectNode><my>custom</my><object>1</object><here>1</here><here>2</here><here>3</here></ObjectNode>
{"my":"custom","object":"1","here":"3"}

So, this because Jackson XML could not parse "here" as array, unfortunately. So, we should serialize it in different way:

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.sun.xml.bind.marshaller.CharacterEscapeHandler;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.*;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.validation.Schema;
import javax.xml.validation.TypeInfoProvider;
import javax.xml.validation.Validator;
import javax.xml.validation.ValidatorHandler;
import java.io.File;
import java.io.IOException;
import java.io.Writer;

public class TestObjectNode {

    @XmlRootElement(name = "moo")
    @XmlAccessorType(XmlAccessType.FIELD)
    static class Moo {
        @JsonProperty("obj")
        @XmlElement
        @XmlJavaTypeAdapter(ObjMarshaller.class)
        JsonNode obj;
    }

    static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    static final XmlMapper XML_MAPPER = (XmlMapper) new XmlMapper()
            .disable(SerializationFeature.WRAP_ROOT_VALUE)
            .disable(DeserializationFeature.UNWRAP_ROOT_VALUE);

    public static void main(String[] args) throws Exception {
        final String value = "{\"obj\": {\"my\": \"custom\", \"object\": 1, \"here\": [1, 2, 3] } }";
        Moo moo = JSON_MAPPER.readValue(value, Moo.class);
        System.out.println(moo.obj);
        saveToFile(moo);
        System.out.println(XML_MAPPER.writeValueAsString(moo));
        moo = readFromFile();
        System.out.println(moo.obj);
    }

    private static void saveToFile(Moo moo) throws Exception {
        File file = new File("moo.xml");
        JAXBContext jaxbContext = JAXBContext.newInstance(Moo.class);
        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
        jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        jaxbMarshaller.setProperty(CharacterEscapeHandler.class.getName(),
                new CharacterEscapeHandler() {
                    @Override
                    public void escape(char[] chars, int i, int i1, boolean b, Writer writer) throws IOException {
                        writer.write(chars, i, i1);
                    }
                });

        jaxbMarshaller.marshal(moo, file);
        jaxbMarshaller.marshal(moo, System.out);
    }

    private static Moo readFromFile() throws Exception {
        File file = new File("moo.xml");
        JAXBContext jaxbContext = JAXBContext.newInstance(Moo.class);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        MySchema mySchema = new MySchema();
        unmarshaller.setSchema(mySchema);
        Moo moo = (Moo) unmarshaller.unmarshal(file);
        String objectNode = mySchema.validatorHandler.getObjectNodeString();
        System.out.println("objectNode = " + objectNode);
        JsonNode jsonNode = XML_MAPPER.readValue(objectNode, ObjectNode.class);
        Object hereObject = jsonNode.get("here");
        if (hereObject instanceof TextNode) {
            String hereArray = ((TextNode) hereObject).asText().replace("<![CDATA[", "").replace("]]>", "");
            ArrayNode here = JSON_MAPPER.readValue(hereArray, ArrayNode.class);
            ((ObjectNode) jsonNode).put("here", here);
        }
        moo.obj = jsonNode;
        return moo;
    }

    private static class ObjMarshaller extends XmlAdapter<String, JsonNode> {

        @Override
        public JsonNode unmarshal(String v) throws Exception {
            return null;
        }

        @Override
        public String marshal(JsonNode v) throws Exception {
            Object hereObject = v.get("here");
            if (hereObject instanceof ArrayNode) {
                ArrayNode here = (ArrayNode) hereObject;
                final StringBuilder value = new StringBuilder("<![CDATA[").append('[');
                for (int i = 0; i < here.size(); ++i) {
                    if (i > 0) {
                        value.append(',');
                    }
                    value.append(here.get(i));
                }
                value.append(']').append("]]>");
                ((ObjectNode) v).put("here", value.toString());
            }
            return XML_MAPPER.writeValueAsString(v);
        }
    }

    static class MySchema extends Schema {

        final ValidatorHandlerImpl validatorHandler = new ValidatorHandlerImpl();

        @Override
        public Validator newValidator() {
            return null;
        }

        @Override
        public ValidatorHandler newValidatorHandler() {
            return validatorHandler;
        }
    }

    static class ValidatorHandlerImpl extends ValidatorHandler {

        private final StringBuilder objectNode = new StringBuilder();
        private boolean insideObjectNode = false;

        public String getObjectNodeString() {
            return objectNode.toString();
        }

        @Override
        public void setContentHandler(ContentHandler receiver) {

        }

        @Override
        public ContentHandler getContentHandler() {
            return null;
        }

        @Override
        public void setErrorHandler(ErrorHandler errorHandler) {

        }

        @Override
        public ErrorHandler getErrorHandler() {
            return null;
        }

        @Override
        public void setResourceResolver(LSResourceResolver resourceResolver) {

        }

        @Override
        public LSResourceResolver getResourceResolver() {
            return null;
        }

        @Override
        public TypeInfoProvider getTypeInfoProvider() {
            return null;
        }

        @Override
        public void setDocumentLocator(Locator locator) {

        }

        @Override
        public void startDocument() throws SAXException {

        }

        @Override
        public void endDocument() throws SAXException {

        }

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException {

        }

        @Override
        public void endPrefixMapping(String prefix) throws SAXException {

        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            if(localName.equals("ObjectNode")) {
                insideObjectNode = true;
            }
            if (insideObjectNode) {
                objectNode.append("<" + localName + ">");
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (insideObjectNode) {
                objectNode.append("</" + localName + ">");
            }
            if(localName.equals("ObjectNode")) {
                insideObjectNode = false;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (insideObjectNode) {
                objectNode.append(ch, start, length);
            }
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {

        }

        @Override
        public void processingInstruction(String target, String data) throws SAXException {

        }

        @Override
        public void skippedEntity(String name) throws SAXException {

        }
    }

}

Results:

{"my":"custom","object":1,"here":[1,2,3]}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<moo>
    <obj><ObjectNode><my>custom</my><object>1</object><here>&lt;![CDATA[[1,2,3]]]&gt;</here></ObjectNode></obj>
</moo>
<Moo><obj><my>custom</my><object>1</object><here>&lt;![CDATA[[1,2,3]]]&gt;</here></obj></Moo>
objectNode = <ObjectNode><my>custom</my><object>1</object><here><![CDATA[[1,2,3]]]></here></ObjectNode>
{"my":"custom","object":"1","here":[1,2,3]}

This is only example, how the exact your task could be achieved. You see that it needed some hard coding, so maybe it's better to think about another approach. Maybe @XmlAnyElement could help you. Or use text variant from previous answer. Also if you delete CharacterEscapeHandler.class you would receive escaped xml string.

like image 1
egorlitvinenko Avatar answered Oct 21 '22 09:10

egorlitvinenko