6

I'm trying to POST the following payload to my Jersey-based web service:

{
    "firstname":"Jimmy",
    "lastname":"Johns",
    "addresses":
    [
        {
            "street":"19 Mayberry Drive",
            "city":"Mayberry",
            "state":"nc",
            "postalcode":"27043",
            "country":"us",
            "addresstype":1
        }
    ],
    "data":
    {
        "eyes":"blue",
        "hair":"brown",
        "sandwich":"roast beef"
    }
}

My Jersey code:

@POST
public Response create( Person person )
{
    createBo( person );    <------- stopped here in debugger
    ...

Stopped just as Jersey calls me, I see addresses in person flushed out with exactly what I'm looking for (what's in the JSON above). However, my data tuples aren't there. I know Jersey is calling my no-arg constructor for Address es and its setters are getting called, but I'm up in the night as far as what Jersey might or might not be trying to do with these random ("data") tuples in my JSON. (I say "random" because in a different invocation, these might be "cave":"deep, dark", "mountain":"high, wide", etc. This is part and parcel of my interface.)

To flesh out what I'm talking about, consider these POJOs as context for the above:

@XmlAccessorType( XmlAccessType.FIELD )
@XmlRootElement
public class Person implements Serializable
{
    @XmlElement
    private List< Address > addresses = new ArrayList< Address >();

    @XmlElement
    private Map< String, String > data = new HashMap< String, String >();

    ...

@XmlRootElement
public class Address implements Serializable
{
    private String  street;
    private String  city;
    private String  state;
    private String  country;
    private String  postalcode;
    private Integer addresstype;
    ...

Note: I can't do the random tuples as I've done Address because I don't actually know beforehand what the keys will be (whereas I limit Address to street, city, etc.).

What I need is some kind of magic serializer for HashMaps in Jersey and I cannot seem to interpret the docs well enough to understand how to write one or work around this problem while still maintaining the flexibility of my interface.

I would appreciate any indication as to how to accomplish this.

Russ Bateman

P.S. Note sadly that Java.util.Map to JSON Object with Jersey / JAXB / Jackson was not helpful, though it showed great promise.

Community
  • 1
  • 1
Russ Bateman
  • 18,333
  • 14
  • 49
  • 65

2 Answers2

8

Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.

The following will work if you are using MOXy, and should work with any other JAXB provider. This approach converts the java.util.Map to an org.w3c.dom.Element using an XmlAdapter.

MapAdapter

An XmlAdapter allows you to marshal an instance of one class as an instance of another class (see: http://blog.bdoughan.com/2010/07/xmladapter-jaxbs-secret-weapon.html).

package forum11353790;

import java.util.*;
import java.util.Map.Entry;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.*;
import org.w3c.dom.*;

public class MapAdapter extends XmlAdapter<Element, Map<String, String>> {

    private DocumentBuilder documentBuilder;

    public MapAdapter() throws Exception {
        documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    }

    @Override
    public Element marshal(Map<String, String> map) throws Exception {
        Document document = documentBuilder.newDocument();
        Element rootElement = document.createElement("data");
        document.appendChild(rootElement);
        for(Entry<String,String> entry : map.entrySet()) {
            Element childElement = document.createElement(entry.getKey());
            childElement.setTextContent(entry.getValue());
            rootElement.appendChild(childElement);
        }
        return rootElement;
    }

    @Override
    public Map<String, String> unmarshal(Element rootElement) throws Exception {
        NodeList nodeList = rootElement.getChildNodes();
        Map<String,String> map = new HashMap<String, String>(nodeList.getLength());
        for(int x=0; x<nodeList.getLength(); x++) {
            Node node = nodeList.item(x);
            if(node.getNodeType() == Node.ELEMENT_NODE) {
                map.put(node.getNodeName(), node.getTextContent());
            }
        }
        return map;
    }

}

Person

You specify that a field/property should leverage the XmlAdapter via the @XmlJavaTypeAdapter annotation.

package forum11353790;

import java.io.Serializable;
import java.util.*;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlAccessorType( XmlAccessType.FIELD )
@XmlRootElement
public class Person implements Serializable {

    private String firstname;

    private String lastname;

    private List< Address > addresses = new ArrayList< Address >();

    @XmlAnyElement
    @XmlJavaTypeAdapter(MapAdapter.class)
    private Map< String, String > data = new HashMap< String, String >();

}

Address

package forum11353790;

import java.io.Serializable;
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class Address implements Serializable {

    private String  street;
    private String  city;
    private String  state;
    private String  country;
    private String  postalcode;
    private Integer addresstype;

}

jaxb.properties

To specify MOXy as your JAXB provider you need to include a file called jaxb.properties in the same package as your domain model with the following entry (see: http://blog.bdoughan.com/2011/05/specifying-eclipselink-moxy-as-your.html).

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Demo

Below is a standalone example you can run to prove everything works.

package forum11353790;

import java.io.FileInputStream;
import java.util.*;
import javax.xml.bind.*;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.persistence.jaxb.JAXBContextProperties;

public class Demo {

    public static void main(String[] args) throws Exception {
        Map<String, Object> properties = new HashMap<String,Object>(2);
        properties.put(JAXBContextProperties.MEDIA_TYPE, "application/json");
        properties.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
        JAXBContext jc = JAXBContext.newInstance(new Class[] {Person.class}, properties);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        StreamSource json = new StreamSource(new FileInputStream("src/forum11353790/input.json"));
        Person person = unmarshaller.unmarshal(json, Person.class).getValue();

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(person, System.out);
    }

}

input.json/Output

{
   "firstname" : "Jimmy",
   "lastname" : "Johns",
   "addresses" : [ {
      "street" : "19 Mayberry Drive",
      "city" : "Mayberry",
      "state" : "nc",
      "country" : "us",
      "postalcode" : "27043",
      "addresstype" : 1
   } ],
   "data" : {
      "sandwich" : "roast beef",
      "hair" : "brown",
      "eyes" : "blue"
   }
}

MOXy and JAX-RS/Jersey

You can leverage MOXy in a JAX-RS environment by leveraging the MOXyJsonProvider class:

bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • Thank you for the quick and impressive answer. I'm in the middle of trying out your demo. I can't seem to lay my hands on a JAR containing org.eclipse.persistence.jaxb.JAXBContextProperties. Am I required to swallow the whole MOXy/EclipseLink download or can I just consume a JAR (or a few)? – Russ Bateman Jul 06 '12 at 15:02
  • @RussBateman - You will need an EclipseLink 2.4.0 installer or bundles from http://www.eclipse.org/eclipselink/downloads/. If you are familiar with Maven you could do something similar to: https://github.com/bdoughan/blog20120418/blob/master/pom.xml – bdoughan Jul 06 '12 at 15:06
  • In short, I've grep'd and jar tf'd all the source code and JAR files in EclipseLink for this symbol to no avail so far. (I don't do Maven.) I downloaded EclipseLink 2.1.0 ('cause I use Helios and not Juno--does this make a difference?). Also, as noted, project restrictions make it difficult to walk the whole framework route. Again, must I swallow all of EclipseLink or can I cherry-pick this one feature? I can walk this road to see this solution, but if I can't cherry pick, I'll have to find another solution. Thanks (and sorry for the lameness here). – Russ Bateman Jul 06 '12 at 15:24
  • @RussBateman - You will need an EclipseLink 2.4 install. You can use the eclipselink.jar (all of EclipseLink), or the following bundles: core, moxy, antlr, and asm. – bdoughan Jul 06 '12 at 15:33
  • Don't have to swallow all of EclipseLink to make the demo work, just the antlr, core and moxy JARs. Thanks. – Russ Bateman Jul 06 '12 at 15:59
  • The demo works fine, of course. I'm trying to adapt this to my ReST web service. Insertion of jaxb.properties breaks my existing Jersey services including PersonService (@POST code above) which I'm trying to fix. Is the point to abandon Jersey in favor of a different framework, i.e.: EclipseLink? Or can I be successful in just adding this Map adapter for Jersey to call? (I'm guessing not.) Thanks for this solution; I hope I can use it. – Russ Bateman Jul 06 '12 at 17:14
  • @RussBateman - You can try the above approach without the `jaxb.properties` file as it may also work with your current JAXB provider. Jersey is a great JAX-RS framework, EclipseLink provides JAXB & JPA implementations and is not a Jersey replacement. If you wish to use EclipseLink if you provide more details on what environment you're running in, I may be able to help with the configuration. – bdoughan Jul 06 '12 at 17:24
  • I'm using bare-bones Jersey simply as my @POST code illustrates above. The web service code (POSTs, GETs, PUTs, DELETEs, etc.) calls down to a thin business logic layer, then a super-thin DAO layer atop simple POJOs (like Address and Person) except for Person needing a Map, then in-and-out of a database underneath. It's that simple. We tried and threw out Spring as being overkill and crushing. It's late in the game, but I would entertain any suggestions. BTW, I'm getting something working here. Would you like to take this out of stackoverflow into the EclipseLink forum, or elsewhere? – Russ Bateman Jul 06 '12 at 17:39
  • We could move it to the EclipseLink forum (http://www.eclipse.org/forums/index.php?t=thread&frm_id=111), or Stack Overflow chat might work too? – bdoughan Jul 06 '12 at 17:42
  • 1
    I've added EclipseLink to the Eclipse forums I'm active in, but I have successfully integrated this solution into my Jersey ReST server application. It works perfectly and totally meets my cherry-picking requirement. I can't thank you enough for this solution, Blaise! – Russ Bateman Jul 06 '12 at 20:36
  • I'm happy to help, always nice to solve a problem before the weekend :). – bdoughan Jul 06 '12 at 20:39
0

Jackson provides the facility for you. You can force it by adding the following to your Application class. Note this may disable automatic location of your @Path annotated classes.

@Override
public Set<Object> getSingletons() {
    return ImmutableSet
            .<Object> builder()
            .add(new JacksonJaxbJsonProvider(new ObjectMapper(),
                    JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS)).build();
}
Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265