I use Jersey and decided to go with GSON instead of Moxy for JSON handling (didn't like the fact that Moxy requires setters).
Everything works fine until now, except one very annoying issue in my JerseyTest subclasses: custom GsonProvider is not being recognized unless explicitly registered for each call. It is, however, being recognized if I deploy the application to Tomcat.
My ResourceConfig:
#ApplicationPath("")
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig() {
register(GsonProvider.class);
register(SomeResource.class);
}
}
Implementation of GsonProvider (though I don't think it is related to the issue I experience):
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class GsonProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private final Gson mGson;
public GsonProvider() {
mGson = new GsonBuilder().create();
}
#Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
#Override
public T readFrom(Class<T> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException, WebApplicationException {
InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8");
try {
return mGson.fromJson(reader, type);
} finally {
reader.close();
}
}
#Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
#Override
public long getSize(T t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
#Override
public void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
PrintWriter printWriter = new PrintWriter(entityStream);
try {
String json = mGson.toJson(t);
printWriter.write(json);
printWriter.flush();
} finally {
printWriter.close();
}
}
}
This test results in MessageBodyProviderNotFoundException:
public class SomeResourceTest extends JerseyTest {
#Override
public Application configure() {
return new MyResourceConfig();
}
#Test
public void someApi_200Returned() throws Exception {
// Arrange
// Act
SomeResponse response =
target("/somepath")
.request()
.post(Entity.json(""), SomeResponse.class);
// Assert
assertThat(response.getStatus(), is(200));
}
}
In order to resolve this issue I register GsonProvider for request. The following change makes the test pass:
public class SomeResourceTest extends JerseyTest {
#Override
public Application configure() {
return new MyResourceConfig();
}
#Test
public void someApi_200Returned() throws Exception {
// Arrange
// Act
SomeResponse response =
target("/somepath")
.register(GsonProvider.class)
.request()
.post(Entity.json(""), SomeResponse.class);
// Assert
assertThat(response.getStatus(), is(200));
}
}
So, registration of GsonProvider in MyResourceConfig is good for deployment, but JerseyTest requires additional registration per request.
While I can live with that, it is annoying, time consuming and will be hard to communicate to other team members. Any solution for this issue?
You haven't shown the stacktrace, but I'm pretty sure that if you look closely at it, it will show that it is actually a client side error. What you need to do is register the gson provider with the client also since you are trying to deserialize the response JSON to a POJO
#Override
public void configureClient(ClientConfig config) {
config.register(GsonProvider.class)
}
The configureClient method is a method in the JerseyTest that you can override.
Related
Iam learning REST webservice. I have written a very basic code to return a List from the webservice. below is code snippet
#Path("hello")
public class Hello {
#GET
#Produces(MediaType.TEXT_PLAIN)
public List<String> greeting() {
List<String> greeting = new ArrayList<>();
greeting.add("Hello World");
greeting.add("How are you");
greeting.add("Hope you are doing good");
greeting.add("Hey WhatsApp");
greeting.add("Take care");
greeting.add("Perform well");
return greeting;
}
}
the messagebodywriter implementation is below
#Provider
#Produces(MediaType.TEXT_PLAIN)
public class ListMessageBodyWriter implements MessageBodyWriter<List<String>>{
#Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
System.out.println("here in the isWriteable");
return type == List.class;
}
#Override
public void writeTo(List<String> t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
System.out.println("here in the writeTo");
System.out.println("t="+t);
System.out.println("size of t "+t.size());
Writer writer = new PrintWriter(entityStream);
writer.write("list of string will be returned later");
writer.flush();
writer.close();
}
But when i run the code i still get the same error as below
MessageBodyWriter not found for media type=text/plain, type=class java.util.ArrayList, genericType=java.util.List.
Why iam getting the same error despite implementing the messagebodywriter?
#Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
System.out.println("here in the isWriteable");
return type == List.class;
}
changing the return type == List.class; to
return type == ArrayList.class;
solved the error.
I'm creating a Spring 4 REST application, using MappingJackson2XmlHttpMessageConverter to convert incoming XML requests to domain objects. Is there any way to apply XSD validation in that process? If not, I think my fallback is to just make the #RequestBody a String, parse and validate it, and then convert it to the domain object. Is there a better approach?
One approach to this may be to write a custom HttpMessageConverter<T> that checks XSD validation (look here for a way to validate XML with XSD) before returning the object.
Suppose that you have the following method in your Controller class:
#RequestMapping(value = "/")
public CustomObject getCustomObject(#RequestParam(value = "id") String id){
return new CustomObject();
}
Then your converter may look like this:
public class CustomObjectConverter implements HttpMessageConverter<CustomObject> {
// a real message converter that will respond to ancillary methods and do the actual work
protected HttpMessageConverter<Object> delegateConverter;
public CustomObjectConverter (HttpMessageConverter<Object> delegate) {
super(delegate, personService);
super.delegateConverter = delegate;
this.employeePhotoBaseUrl = employeePhotoBaseUrl;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canRead(clazz, mediaType) && CustomObject.class.equals(clazz);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canWrite(clazz, mediaType) && CustomObject.class.equals(clazz);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegateConverter.getSupportedMediaTypes();
}
#Override
public CustomObject read(Class<? extends CustomObject> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return (CustomObject) delegateConverter.read(clazz, inputMessage);
}
#Override
public void write(CustomObject t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if(validationOK)
delegateConverter.write(t, contentType, outputMessage);
else
// You may implement a custom exception handler to return a proper HTTP error code
throw new YourCustomException();
}
}
Remember to configure your new converter. I do this in my configuration class:
#Configuration
#EnableWebMvc
public class RestConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// initialize your MappingJackson2XmlHttpMessageConverter
MappingJackson2XmlHttpMessageConverter xmlMessageConverter = xmlMessageConverter();
//Here we add our custom-configured HttpMessageConverters
converters.add(new CustomObjectConverter(xmlMessageConverter));
super.configureMessageConverters(converters);
}
}
When I send this request:
{"invalidField": "value", "date": "value"}
to my rest service:
#PUT
#Consumes("application/json")
public void putJson(Test content) {
System.out.println(content.toString());
}
I expected to get an exception because:
There is no invalidField in my domain model.
Date format is not valid.
But really I get test object with null values. My dmain model is:
public class Test {
private String name;
private Date date;
//getters and setters here
}
I think this is not a valid behavior. How can I fix that?
Thanks for help.
Solution:
As Blaise Doughan said, it is required to extend MOXy's MOXyJsonProvider and override the preReadFrom method to set custom javax.xml.bind.ValidationEventHandler. But the problem is that Jersey's ConfigurableMoxyJsonProvider will always be picked first, unless you write a MessageBodyWriter/MessageBodyReader that is parameterized with a more specific type than Object. To solve this problem it is necessary to disable MOXy and then enable CustomMoxyJsonProvider.
Example:
Create your own feature that extends javax.ws.rs.core.Feature:
#Provider
public class CustomMoxyFeature implements Feature {
#Override
public boolean configure(final FeatureContext context) {
final String disableMoxy = CommonProperties.MOXY_JSON_FEATURE_DISABLE + '.' + context.getConfiguration().getRuntimeType().name().toLowerCase();
context.property(disableMoxy, true);
return true;
}
}
Create your own provider that extends MOXyJsonProvider:
#Provider
public class CustomMoxyJsonProvider extends MOXyJsonProvider {
#Override
protected void preReadFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, Unmarshaller unmarshaller) throws JAXBException {
unmarshaller.setEventHandler(new ValidationEventHandler() {
#Override
public boolean handleEvent(ValidationEvent event) {
return false;
}
});
}
}
Add this resources in Application config:
package com.vinichenkosa.moxyproblem;
import java.util.Set;
import javax.ws.rs.core.Application;
#javax.ws.rs.ApplicationPath("webresources")
public class ApplicationConfig extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new java.util.HashSet<>();
addRestResourceClasses(resources);
return resources;
}
private void addRestResourceClasses(Set<Class<?>> resources) {
resources.add(com.vinichenkosa.moxyproblem.TestResource.class);
resources.add(com.vinichenkosa.moxyproblem.custom_provider.CustomMoxyFeature.class);
resources.add(com.vinichenkosa.moxyproblem.custom_provider.CustomMoxyJsonProvider.class);
}
}
MOXy will report information about invalid property values, but by default it does not fail on them. The reporting is done to an instance of javax.xml.bind.ValidationEventHandler. You can override the ValidationEventHandler set on the Unmarshaller to do this.
You can create your own MesageBodyReader/MessageBodyWriter that extends MOXy's MOXyJsonProvider and override the preReadFrom method to do this.
#Override
protected void preReadFrom(Class<Object> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
Unmarshaller unmarshaller) throws JAXBException {
unmarshaller.setEventHandler(yourValidationEventHandler);
}
Filtering out properties with Jackson is pretty simple:
final FilterProvider filters = new SimpleFilterProvider().
addFilter(... the name of the filter ...,
SimpleBeanPropertyFilter.filterOutAllExcept(... enumeration of properties ...));
<object mapper>.writer(filters).writeValueAsString(... the bean ...);
I am trying to integrate this in my Jersey REST application. The API user has the possibility to filter the properties by providing a query string:
https://the-api/persons?fields=name,age,location,gender
What is the most elegant way to do this in Jersey? I could easily execute the above in my resource methods, but this somehow kills the elegance of Jersey. Also, I believe that creating a new ObjectMapper for every request will have performance penalties.
I could write a MessageBodyWriter which fetches the fields query parameter from the UriInfo context and serializes the entity to json while applying a filter based on the fields query parameter. Is this the best way to do this? Like this:
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JustTesting implements MessageBodyWriter<Object> {
#Context
UriInfo uriInfo;
#Context
JacksonJsonProvider jsonProvider;
public boolean isWriteable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return MediaType.APPLICATION_JSON_TYPE.equals(mediaType);
}
public long getSize(Object object, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return -1;
}
public void writeTo(Object object, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> stringObjectMultivaluedMap, OutputStream outputStream) throws IOException, WebApplicationException {
final FilterProvider filters = new SimpleFilterProvider().addFilter("MyFilter", SimpleBeanPropertyFilter.filterOutAllExcept(uriInfo.getQueryParameters().getFirst("fields")));
jsonProvider.locateMapper(aClass, mediaType).writer(filters).writeValue(outputStream, object);
}
}
It seems to work, but I am unsure if it is smart to do it like this. I am new to the Jersey library.
My current solution to a similar problem is to register the following servlet filter:
#Singleton
public class ViewFilter implements Filter {
private #Inject Provider<ViewBeanPropertyFilter> filter;
#Override
public void init(FilterConfig filterConfig) throws ServletException { }
#Override
public void destroy() { }
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ObjectWriterInjector.set(new ObjectWriterModifier() {
#Override
public ObjectWriter modify(EndpointConfigBase<?> endpoint, MultivaluedMap<String, Object> responseHeaders, Object valueToWrite, ObjectWriter w, JsonGenerator g) throws IOException {
return w.with(new FilterProvider() {
#Override
public BeanPropertyFilter findFilter(Object filterId) {
if(filterId.equals(ViewFilterJacksonModule.FILTER_NAME)) {
return filter.get();
}
return null;
}
});
}
});
chain.doFilter(request, response);
}
public static class ViewBeanPropertyFilter extends SimpleBeanPropertyFilter {
private #Inject ViewManager manager;
#Override
protected boolean include(BeanPropertyWriter writer) {
Class<?> cls = writer.getMember().getDeclaringClass();
return manager.isFieldInView(cls, writer.getMember().getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
}
}
It's a little more gnarly than the solution you provided, but leaves the standard JacksonJsonProvider serialization in place.
It can be improved by pulling the (possibly) existing FilterProvider via m.getConfig().getFilterProvider() and delegating to it before / after your filter.
I am currently working on a RESTful API. I have an Employee class and an EmployeeResource class. I also have a custom DateAdapter, which changes my Date properties to Long timestamps. However, my JSON responses are showing the timestamps as strings (wrapped in double quotes) rather than numbers (without double quotes). Here is an abbreviated version of my code and captured JSON response...
Custom DateAdapter
public class DateAdapter extends XmlAdapter<Long, Date> {
#Override
public Date unmarshal(Long v) throws Exception {
return new Date(Long.valueOf(v));
}
#Override
public Long marshal(Date v) throws Exception {
return v.getTime();
}
}
Entity Class
#Entity
#javax.xml.bind.annotation.XmlRootElement
#XmlType(propOrder={"createdOn","empId"})
public class Employee implements Serializable {
private Date createdOn;
private Integer empId;
#Column(nullable=false)
#Temporal(TemporalType.TIMESTAMP)
#XmlJavaTypeAdapter(DateAdapter.class)
public Date getCreatedOn() {
return createdOn;
}
public void setCreatedOn(Date createdOn) {
this.createdOn = createdOn;
}
#Id
#XmlID
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
}
EmployeeResource
#Path("/Employees")
#javax.xml.bind.annotation.XmlRootElement
#XmlType(propOrder={"hateoas","employees"})
public class EmployeeResource {
List<Employee> employees;
public List<Employee> getEmployees() {
return employees;
}
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
#GET
#Path("/{id}")
#Produces("application/json")
public Response getEmployee(#Context UriInfo ui, #PathParam("id") Integer id) {
Session session = HibernateUtil.getSession();
session.beginTransaction();
Criteria criteria=session.createCriteria(Employee.class);
criteria.add(Restrictions.eq("empId", new Integer(10150)));
this.employees = criteria.list();
return Response.ok(this).build();
}
}
Current JSON response
{
"employees":{
"createdOn":"1330915130163",
"empId":"10150"
}
}
Expected JSON response
{
"employees":{
"createdOn":1330915130163,
"empId":10150
}
}
I'm assuming that there's some way to prevent JAXB or JAX-RS from wrapping all numbers in quotes. Could someone guide me to where I could configure this?
Thanks in advance!
EDIT #1 2012.03.07
Ok, so after some more researching, I think my problem is with the default JSONConfiguration.Notation being used, MAPPED . It looks like the NATURAL JSONConfiguration.Notation would get me what I want. However, I haven't found a clear example of how to apply that application wide. I'm assuming I would specify this in my ApplicationConfig class that extends javax.ws.rs.core.Application .
EDIT #2 2012.03.10
Ok, so after some more researching I decided to use the JSON parser library, Jackson. It seems to be the most complete JSON solution out there using just the default configuration. By default Dates are translated to their corresponding timestamps and serialized as numbers (w/o quotes). The only drawback I have come across is that Jackson currently does not support the JAXB annotations, "#XmlID" and "#XmlIDREF". Since I have direct self-references in my data model (not shown above), I have created another question to discuss. If anyone is interested click here to follow that thread...
Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB 2 (JSR-222) expert group.
The JAX-RS implementation you are using may be using a JAXB (JSR-222) implementation with something like Jettison to produce JSON. Jettison provides a StAX API to interact with JSON, since that StAX APIs don't have any sort of typing WRT text, all the simple values get treated as strings:
http://blog.bdoughan.com/2011/04/jaxb-and-json-via-jettison.html
To get the behaviour you are looking for you can use a different binding solution. We are adding this support into the MOXy component for EclipseLink 2.4:
http://blog.bdoughan.com/2011/08/json-binding-with-eclipselink-moxy.html
To configure MOXy as your JSON-binding provider in a JAX-RS environment, you could create a MessageBodyReader/MessageBodyWriter that looks like:
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collection;
import javax.xml.transform.stream.StreamSource;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;
import javax.xml.bind.*;
#Provider
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class MOXyJSONProvider implements
MessageBodyReader<Object>, MessageBodyWriter<Object>{
#Context
protected Providers providers;
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
public Object readFrom(Class<Object> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
try {
Unmarshaller u = getJAXBContext(type, mediaType).createUnmarshaller();
u.setProperty("eclipselink.media-type", mediaType.toString());
u.setProperty("eclipselink.json.include-root", false);//tiny fix
return u.unmarshal(new StreamSource(entityStream), (Class) genericType);
} catch(JAXBException jaxbException) {
throw new WebApplicationException(jaxbException);
}
}
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
public void writeTo(Object object, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException,
WebApplicationException {
try {
Marshaller m = getJAXBContext(Customer.class, mediaType).createMarshaller();
m.setProperty("eclipselink.media-type", mediaType.toString());
m.setProperty("eclipselink.json.include-root", false);
m.marshal(object, entityStream);
} catch(JAXBException jaxbException) {
throw new WebApplicationException(jaxbException);
}
}
public long getSize(Object t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
private JAXBContext getJAXBContext(Class<?> type, MediaType mediaType)
throws JAXBException {
ContextResolver<JAXBContext> resolver
= providers.getContextResolver(JAXBContext.class, mediaType);
JAXBContext jaxbContext;
if(null == resolver || null == (jaxbContext = resolver.getContext(type))) {
return JAXBContext.newInstance(type);
} else {
return jaxbContext;
}
}
}
For More Information
https://stackoverflow.com/a/8989659/383861
http://blog.bdoughan.com/2010/08/creating-restful-web-service-part-15.html
http://blog.bdoughan.com/2011/04/moxys-xml-metadata-in-jax-rs-service.html