Like i18n JAX-RS (Jersey) JAXB Annotated Answers

We use JAXB annotations for our model objects, which we return from our web API, and we want the data to be localized and other values ​​formatted based on user settings (i.e. metrics versus charter). 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); 

Trying the simplest approach, I just get Locale from the headers of the HTTP requests and provide it to the marshaller in one of the MessageBodyWriter classes.

I looked at the extension of the default registered providers, such as XMLRootElementProvider, but realized that they were written as mostly final, so I abandoned this approach. There would be at least 10 classes that I needed to extend, so it was not perfect.

Does anyone know what is the best way to configure the marshaller in MessageBodyWriter using client adapters for each request? I am sure this has something to do with ContextResolver .

+4
source share
3 answers

Writing a ContextResolver for Marshalling leads to much cleaner and more suitable solutions than writing a MessageBodyWriter. All JAXB classes use the Providers.getContextResolver method to get the marshaller. I provide my custom ContextResolver and I have i18n answers.

 @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; } } 

JSON was not localized, and after some investigation, I realized that Jackson JSON libraries are used instead of JAXBJSONElementProvider distributed with Jersey libraries. I removed the POJOMappingFeature configuration in web.xml and I have localized JSON, however this is not as good as Jackson JSON.

A very clean solution that makes me think that the JAX-RS and Jersey implementation performed very well.

+4
source

I solved the problem with writing my own MessageBodyWriter , which gets the HttpHeaders injected into the constructor, which I use later when writing the response. I will include the whole class, as it is not so big.

 @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); } } } 

Creates this xml fragment with the default locale en-US:

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

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

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

This approach is still not ideal, since now I will need to write a similar MessageBodyWriter for JSON or add JSON support for this MessageBodyWriter. In addition, I assume that JAXB providers by default make some settings that I do not use.

+1
source

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

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

And then using it to access JAXBContext to create a 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

0
source

Source: https://habr.com/ru/post/1392066/


All Articles