4

We are using JAXB Annotations on our model objects that we return from our Web API and we want the data to be localized and other values formatted based on user preferences (i.e. metric vs. statute). We do this by adding custom adapters to the Marshaller.

Marshaller marshaller = jc.createMarshaller();
marshaller.setAdapter(NumberPersonalizedXmlAdapter.class,
          new NumberPersonalizedXmlAdapter(locale);
marshaller.marshal(expected, writer);

Attempting the simplest approach I will just get the Locale from the HTTP Request Headers and provide it to the Marshaller in one of the MessageBodyWriter classes.

I looked at extending the default registered providers like XMLRootElementProvider, but realized they were written as mostly final so I abandoned that approach. There would have been at least 10 classes I needed to extend anyway so that wasn't ideal.

Does anyone know how best to get the marshaller in the MessageBodyWriter set up with customer Adapters for each request? I'm pretty sure it has something to do with ContextResolver.

Aaron Roller
  • 1,074
  • 1
  • 14
  • 19

3 Answers3

4

Writing a ContextResolver for the Marshalling results in a much cleaner and more appropriate solution than writing a MessageBodyWriter. All of the JAXB classes use the Providers.getContextResolver method to obtain a marshaller. I provide my custom ContextResolver and I have i18n responses.

@Provider
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public class JaxbPersonalizerContextResolver implements ContextResolver<Marshaller> {

    private HttpHeaders requestHeaders;

    public JaxbPersonalizerContextResolver(@Context HttpHeaders requestHeaders) {
        this.requestHeaders = requestHeaders;
    }

    @Override
    public Marshaller getContext(Class<?> type) {
        Locale locale = If.first(this.requestHeaders.
                                    getAcceptableLanguages(), Locale.US);
        NumberFormat formatter = NumberFormat.getNumberInstance(locale);
        formatter.setMaximumFractionDigits(1);

        Marshaller marshaller;
        try {
            JAXBContext jc = JAXBContext.newInstance(type);
            marshaller = jc.createMarshaller();
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
        marshaller.setAdapter(QuantityXmlAdapter.class, 
                           new QuantityXmlAdapter.Builder().locale(locale).build());
        marshaller.setAdapter(NumberPersonalizedXmlAdapter.class,
                new NumberPersonalizedXmlAdapter.Builder().
                                    formatter(formatter).build());
        return marshaller;
    }
}

The JSON wasn't being localized and after some investigation I realized the Jackson JSON libraries were being used instead of JAXBJSONElementProvider distributed with the Jersey libraries. I removed the POJOMappingFeature configuration in web.xml and I have localized JSON, however, it isn't as nice as the Jackson JSON.

A very clean solution which makes me think that the JAX-RS and Jersey implementations are done very well.

Aaron Roller
  • 1,074
  • 1
  • 14
  • 19
1

I solved the problem writing my own MessageBodyWriter which receives the HttpHeaders injected into the constructor which I use later when writing the response. I will include the entire class since it's not that large.

@Produces(MediaType.APPLICATION_XML)
@Provider
public class JaxbPersonalizationProvider implements MessageBodyWriter<Object> {

    private HttpHeaders requestHeaders;
    private Providers providers;

    public JaxbPersonalizationProvider(@Context HttpHeaders requestHeaders, @Context Providers providers) {
        this.requestHeaders = requestHeaders;
        this.providers = providers;
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return type.getAnnotation(XmlRootElement.class) != null && mediaType.equals(MediaType.APPLICATION_XML_TYPE);
    }

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

    @Override
    public void writeTo(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException,
            WebApplicationException {
        Locale locale = If.first(this.requestHeaders.getAcceptableLanguages(), Locale.US);
        NumberFormat formatter = NumberFormat.getNumberInstance(locale);
        formatter.setMaximumFractionDigits(1);

        Marshaller marshaller;
        try {
            JAXBContext jc = JAXBContext.newInstance(TrackInfo.class);
            marshaller = jc.createMarshaller();
            marshaller.setAdapter(QuantityXmlAdapter.class, new QuantityXmlAdapter.Builder().locale(locale).build());
            marshaller.setAdapter(NumberPersonalizedXmlAdapter.class, new NumberPersonalizedXmlAdapter.Builder()
                    .formatter(formatter).build());

            marshaller.marshal(t, entityStream);
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}

Which produces this xml excerpt with the default locale of en-US:

 <display lang="en_US">
    <value>3,286.1</value>
 </display>

and this xml excerpt when the locale of fr-FR is sent in the headers:

 <display lang="fr_FR">
   <value>3 286,1</value>
 </display>

This approach is still not ideal as I now will need to write a similar MessageBodyWriter for JSON or add JSON support to this MessageBodyWriter. Additionally I assume the default JAXB Providers are doing some tweaking that I'm not taking advantage of.

Aaron Roller
  • 1,074
  • 1
  • 14
  • 19
0

A MessageBodyWriter is the right approach for this use case. I would recommend adding the following field to your MessageBodyWriter:

@javax.ws.rs.core.Context
protected Providers providers;

And then using it to access the JAXBContext to create the Marshaller

public void writeTo(DataObject dataObject, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> multivaluedMap, OutputStream outputStream) throws IOException, WebApplicationException {
    JAXBContext jaxbContext = null;
    ContextResolver<JAXBContext> resolver = providers.getContextResolver(JAXBContext.class, arg3);
    if(null != resolver) {
        jaxbContext = resolver.getContext(type);
    }
    if(null == jaxbContext) {
        jaxbContext = JAXBContext.newInstance(type);
    }
    Marshaller marshaller = jaxbContext.createMarshaller();
}

Related Example

Community
  • 1
  • 1
bdoughan
  • 147,609
  • 23
  • 300
  • 400