1

I have created the REST response as below

@XmlRootElement(name = "customer_info")
@JsonIgnoreProperties(value = { "map" })
public class Customer {
private String name;
private Integer id;
private long time;
private Map<String, Object> map = new TreeMap<>(); 
@JsonAnyGetter 
public Map<String, Object> getMap() {
    return map;
}

@JsonAnySetter
public void setMap(String name, Object values) { 
    this.map.put(name, values); 
    }
}

I want to dynamically create this response with addional parameter.The backend returns a map which will contains addional prop for this.

<customer_info>
<name></name>
<id></id>
<!--The below will have dynamic prop based on key-->
<dynamic1></dynamic1>
<dynamic2></dynamic2>
</customer_info>

The application config

ResourceConfig config = new DefaultResourceConfig();               
 config.add(resources);
 Map<String, MediaType> type = config.getMediaTypeMappings()                
 type.put("json", MediaType.APPLICATION_JSON_TYPE);
 type.put("xml", MediaType.APPLICATION_XML_TYPE);
 servletHandler.addServlet(new ServletHolder(new ServletContainer(config)), "/*");

When i add the Map the xml structure appears as below which is not correct.I need the above xml structure.Can someone guide me how to implement this?

Also the name of the XML element should be same as the key in the map.The JSON response works fine but xml structure is not correct.

   <customer_info>
    <name></name>
    <id></id>
    <!--The below will have dynamic prop based on key-->

    <map>
        <entry>
           <key>dummy_param1</key>
           <value>11</value>
        </entry>
        <entry>
            <key>dummy_param2</key>
            <value>10</value>
        </entry>
    <map>
 </customer_info>

I am using Jersey as REST framework and its running in Jetty server. I am using the provider and getting it registered using springs along with the REST service.

<bean class="org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider"/>
<bean class="com.dummy.CustomerService"/>
<bean class="com.dummy.Service2"/>
<bean class="com.dummy.Service3"/>

The library gradle config is as below:

 "org.codehaus.jackson:jackson-core-asl:1.9.2",
    "org.codehaus.jackson:jackson-jaxrs:1.9.2",
    "org.codehaus.jackson:jackson-mapper-asl:1.9.2", 
    "org.codehaus.jackson:jackson-xc:1.9.2",
    "com.sun.jersey:jersey-client:1.12",
    "com.sun.jersey:jersey-core:1.12",
    "com.sun.jersey:jersey-json:1.12",
    "com.sun.jersey:jersey-server:1.12",
    "com.sun.jersey:jersey-servlet:1.12",
    "org.codehaus.jettison:jettison:1.1",
    "javax.ws.rs:jsr311-api:1.1.1",
    "com.sun.jersey.contribs:jersey-apache-client:1.12",
    "com.sun.jersey.contribs:jersey-apache-client4:1.12",
    "com.sun.jersey.contribs:jersey-multipart:1.12",
    "com.sun.jersey:jersey-client:1.12",
    "com.sun.jersey:jersey-core:1.12",
    "com.sun.jersey:jersey-json:1.12",
    "javax.ws.rs:jsr311-api:1.1.1",
    "org.codehaus.jackson:jackson-core-asl:1.9.2",
    "org.codehaus.jackson:jackson-jaxrs:1.9.2",
    "org.codehaus.jackson:jackson-mapper-asl:1.9.2",
    "org.codehaus.jackson:jackson-xc:1.9.2",
    "javax.servlet:javax.servlet-api:3.1.0",
   "org.eclipse.jetty:ecs-jetty-server:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-util:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-servlet:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-servlets:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-http:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-security:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-io:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-continuation:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-deploy:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-webapp:9.4.0.v20161208",
   "org.eclipse.jetty:jetty-xml:9.4.0.v20161208"

Also can someone please suggest how above can be fixed? Thanks in advance.

user3062513
  • 410
  • 1
  • 10
  • 19

1 Answers1

4

It's not possible with JAXB, but if you want to switch over to using Jackson for XML, then you easily achieve this. All you need to do is annotate the Map getter in your bean with @JsonAnyGetter. This will cause Jackson to serialize the Map keys to be normal elements in the XML. Here's a test

@Path("jackson-xml")
public class JacksonXmlResource {

    @GET
    @Produces("application/xml")
    public Response get() {
        Model model = new Model();
        model.setProp("foo", "bar");
        model.setProp("name", "Paul");
        return Response.ok(model).build();
    }


    public static class Model {
        private Map<String, Object> props = new HashMap<>();

        @JsonAnyGetter
        public Map<String, Object> getProps() {
            return this.props;
        }

        @JsonAnySetter
        public void setProp(String key, Object value) {
            this.props.put(key, value);
        }
    }
} 

This will result in the following XML

<Model>
  <foo>bar</foo>
  <name>Paul</name>
</Model>

To use Jackson for XML, you need to do the following:

  1. Add the Jackson dependency and remove (exclude) the JAXB provider

    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.glassfish.jersey.media</groupId>
                <artifactId>jersey-media-jaxb</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-xml-provider</artifactId>
        <version>${jackson2.version}</version>
    </dependency>
    

    You should exclude the JAXB provider from the jersey-container-servlet dependency that you should already have in your project. The ${jackson2.version} should be whatever version of Jackson you are currently using in your project. If you don't have any explicit dependencies on Jackson, but are using jersey-media-json-jackson, find out what version of Jackson that pulls in. You want to make sure you don't have any conflicting versions.

  2. You need to register the JacksonJaxbXMLProvider with Jersey

    public AppConfig extends ResourceConfig {
        public AppConfig() {
            register(JacksonJaxbXMLProvider,class);
        }
    }
    

With these two things done, it should work.

The cool thing about using Jackson for XML, is that the same Jackson annotations you use for your JSON, can also be used for XML, like the @JsonProperty. Also Jackson understands JAXB annotations (most of them). So you can probably just leave your JAXB annotations the same when you are migrating from JAXB to Jackson. As far as prop order, it will not work with dynamic properties, only for the ones that are already defined.

See also

  • If you want to configure Jackson for XML, have a look at this post, where I mention using a ContextResolver<XmlMapper>

Update

If you can't add any new dependencies to your project, then another way to accomplish this is to just dynamically create the XML using the org.w3c.dom APIs, which are standard Java classes. It will be much more verbose and more more work, but it will get you what you want. See this post for some explanation. Here is an example. It should be easy to follow with the added comments.

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;


@Path("dom-api")
public class DomXmlResource {

    @GET
    @Produces("application/xml")
    public Response getXml() throws Exception {
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

        Document doc = docBuilder.newDocument();

        // create root element
        Element rootEl = doc.createElement("Model");
        doc.appendChild(rootEl);

        Model model = new Model();
        model.setProp("foo", "bar");
        model.setProp("name", "Paul");
        model.setValue("FooBar");

        // set static defined properties
        Element valueEl = doc.createElement("value");
        valueEl.appendChild(doc.createTextNode(model.getValue()));
        rootEl.appendChild(valueEl);

        // set dynamic properties
        for (Map.Entry<String, Object> entry: model.getProps().entrySet()) {
            Element dynamicEl = doc.createElement(entry.getKey());
            dynamicEl.appendChild(doc.createTextNode(String.valueOf(entry.getValue())));
            rootEl.appendChild(dynamicEl);
        }

        // return StreamingOutput so we can just stream the
        // XML results without having to store the String into memory.
        StreamingOutput entity = new StreamingOutput() {
            @Override
            public void write(OutputStream out)
                    throws IOException, WebApplicationException {
                try {
                    // write the XML structure to the output stream.
                    Transformer transformer = TransformerFactory.newInstance()
                            .newTransformer();
                    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                    StreamResult result = new StreamResult(out);
                    DOMSource source = new DOMSource(doc);
                    transformer.transform(source, result);
                    out.flush();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        return Response.ok(entity).build();
    }

    public static class Model {
        private String value;
        private Map<String, Object> props = new HashMap<>();

        public String getValue() {
            return this.value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        public Map<String, Object> getProps() {
            return this.props;
        }

        public void setProp(String key, Object value) {
            this.props.put(key, value);
        }
    }
}
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thanks so much for wonderful explanation.I did the changes and added the below in the class When i add the prop to below class When i add the prop to below class @XmlRootElement(name = "customer_info") public class Customer {private String name;private Integer id;private long time;private Map map = new TreeMap<>(); @JsonAnyGetter public Map getMap() {return map;}@JsonAnySetterpublic void setMap(String name, Object values) { this.map.put(name, values); }} but i dot see the new tags in the response.can you pls let me know what i am missing here. – user3062513 Aug 14 '18 at 10:48
  • Can you post all your dependencies and the all the application configuration code. It's hard to tell what's wrong without seeing it. There are many ways to make this work, so I need to see the whole picture of what you are doing to see if you are doing it right. Please edit your post with this information and also post the code you just added in the comment. It's not suitable for a comment. – Paul Samsotha Aug 14 '18 at 17:15
  • I have updated the post.can u pls help me how to fix this? – user3062513 Aug 17 '18 at 22:37
  • I need to see _all_ your dependencies _and_ your **application configuration**. Without these things, I cannot see the whole picture and we will have to keep going back and forth. Please post exactly what I ask for. Thanks. – Paul Samsotha Aug 17 '18 at 22:48
  • I have added the config and all dependencies.Pls let me know exactly what other info is required. – user3062513 Aug 18 '18 at 20:43
  • 1) You need to add the Jackson XML dependency I listed above 2) You need to make sure your `@JsonAnyGetter` and `@JsonAnySetter` are using the package `com.fasterxml`, and not `org.codebaus`. 3) Register the `JacksonJaxbXmlProvider` with the `ResourceConfig`. Just do `config.getProviderClasses().add(JacksonJaxbXmlProvider.class)`. – Paul Samsotha Aug 21 '18 at 01:07
  • Note that this solution uses Jackson 2, while you're currently using Jackson 1. They two are complete incompatible, so ultimately you are using 2 different version of Jackson. for JSON, you are using 1.x and for XML, you are using 2.x. But the means that all the JSON annotation will not work for XML, as I mentioned in my answer. But if you have an JAXB annotations, they should still work for the XML. Other than that, it should work as expected. I just tested it, and you don't need to exclude any dependencies. – Paul Samsotha Aug 21 '18 at 01:10
  • I am not sure if it would be to allowed to add this dependency as its not part of the libraries.Can u pls tell me if we have any other way to generate the XML using the current set of dependencies? – user3062513 Aug 21 '18 at 01:12
  • Yeah, I'm not sure. AFAIK, Jackson did not add XML support until version 2. – Paul Samsotha Aug 21 '18 at 01:13
  • Just to confirm Since we are adding the JacksonJaxbXmlProvider we should remove the JacksonJaxbJsonProvider.We should always have only one provider right? – user3062513 Aug 21 '18 at 01:17
  • No, one is for JSON and one is for XML. – Paul Samsotha Aug 21 '18 at 01:17
  • Or if you really can't add any different dependencies, you can use the `org.w3c.dom` APIs. These are standard Java classes. It will take a little more work, but it will allow you to get the result. Have a look at [this post](https://stackoverflow.com/q/51790968/2587435) to get some idea. If you are interested, I can put together an example and add it my answer. – Paul Samsotha Aug 21 '18 at 01:28
  • Thanks a lot for your help.That would be a great help if you can also add this solution. – user3062513 Aug 21 '18 at 01:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178404/discussion-between-user3062513-and-paul-samsotha). – user3062513 Aug 21 '18 at 01:51