Wednesday Jan 12, 2011

Returning XML representation of List from Jersey

One user asked about wrapping element that Jersey uses when serves List<T> recently and I recalled I didn't write a blog post about one change I made in this area so.. here we are..

 Jersey supports returning JSR222 (JAXB) annotated classes and even List of them. Let's say we have following class:

@XmlRootElement
public class A {
        public String s;
        public int i;
}

and some corresponding method which returns List<A>

    @GET
    @Path("list")
    public List<A> getList() {
        ArrayList<A> list = new ArrayList<A>();

        return list;

    }

 when we deploy this and do a GET on /list, we'll see something like

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<as><a><s>first</s><i>1</i></a><a><s>second</s><i>2</i></a></as>

which is fine and valid for most cases, but you might need something else. For example, you want to rename <a> to something meaningful, let's say "contract". XmlRootElement annotation has "name" property which is designed for these cases, so lets add it to our class A annotation (@XmlRootElement(name="contract") public class A { ... } ) and make that request again..

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<as><contract><s>first</s><i>1</i></contract><contract><s>second</s><i>2</i></contract></as>

Looks better, <a> has changed to <contract> and we might be satisfied.. oh wait, what about <as>? It haven't changed! Firstly, <as> was just plural for <a> and it stayed same when XmlRootElements name property was specified.. oops. This was a bug in Jersey and we couldn't fix it without breaking backwards compatibility, so fix for this is enabled only when FEATURE_XMLROOTELEMENT_PROCESSING is enabled (set to true). Lets do that and see how output changes..

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<contracts><contract><s>first</s><i>1</i></contract><contract><s>second</s><i>2</i></contract></contracts>

Ha, we are almost there. Or.. depends what you want, I guess most of users is satisfied and doesn't need to read further. But there is always someone who wants something "extra".. like specify wrapping element. That is needed for some cases (like B2B communication) and Jersey needs to be able to handle this usecase as well. Unfortunately its not as clean as other solutions. We need to implement our own MessageBodyWriter and for those not familiar with JAX-RS API - it basically allows you to specify how the output will be presented for which mediatype.

Your MessageBodyWriter needs to have @javax.ws.rs.ext.Provider  annotation (or be added as provider to ResourceConfig) and implement MessageBodyWriter interface.. I create such class which is sufficient for basic cases, you might want to improve it by creating support for other charsets than UTF-8 or gathering Marshaller from other source that creating new instance.. but at least its a working startpoint..

@Provider
public class ListAProvider implements MessageBodyWriter<List> {

  private String myWrapElemName = "wrapper";
  private Marshaller m;

  public ListAProvider() throws Exception{
    JAXBContext context = JAXBContext.newInstance(A.class);
    m = context.createMarshaller();
    m.setProperty(Marshaller.JAXB_FRAGMENT, true);
  }

  @Override
  public long getSize(List as, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return -1;
  }

  @Override
  public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {

    if(mediaType.getSubtype().endsWith("xml") &&
          List.class.isAssignableFrom(type) &&
          genericType instanceof ParameterizedType) {
      if ((((ParameterizedType)genericType).getActualTypeArguments()[0]).equals(A.class)) {
        return true;
      }
    }

    return false;
  }

  @Override
  public void writeTo(List list, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {

    Charset c = Charset.forName("UTF-8");
    String cName = c.name();

    entityStream.write(String.format("<?xml version=\\"1.0\\" encoding=\\"%s\\" standalone=\\"yes\\"?>", cName).getBytes(cName));

    entityStream.write(String.format("<%s>", myWrapElemName).getBytes(cName));

    for (Object o : list)
      try {
        m.marshal(o, entityStream);
      } catch(JAXBException exp) {}

    entityStream.write(String.format("</%s>", myWrapElemName).getBytes(cName));

  }
}

when you add this to your Jersey enabled application and make same request as we did earlier, you should get:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<wrapper><contract><s>first</s><i>1</i></contract><contract><s>second</s><i>2</i></contract></wrapper>

and we are done for this article, hope somebody will find it useful.

About

Pavel Bucek

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today