Get jersey to deserialize into subclass - java

I'm trying to get Jersey to automatically deserialize into a subclass instance of the declared class for me.
Say A has one string field named "a". B extends A has one string field named "b". Similarly, C extends A has one string field named "c".
I'd like for an input which has fields "a" and "b" to be deserialized into an instance of B, and an input which has fields "a" and "c" to be deserialized into an instance of C with one endpoint definition, specifying the post body parameter type as A, as follow.
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public ServerResponse translateClientRequest(final A postBody) throws
ProxyException {
..
}
This example doesn't work. Is what I'm asking for possible? If so, how?

You probabily should have to use a custom response (or request) mapper.
1.- Create a class implementing MessageBodyReader in charge of writing / reading the request
#Provider
public class MyMessageBodyReader
implements MessageBodyReader<A> {
#Override
public boolean isReadable(final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
// you can check if the media type is JSon also but it's enougth to check
// if the type is a subclass of A
return A.class.isAssignableFrom(type); // is a subclass of A?
}
#Override
public A readFrom(final Class<A> type,
final Type genericType,
final Annotation[] annotations,
final MediaType mediaType,
final MultivaluedMap<String,String> httpHeaders,
final InputStream entityStream) throws IOException,
WebApplicationException {
// create an instance of B or C using Jackson to parse the entityStream (it's a POST method)
}
}
2.- Register the Mappers in your app
public class MyRESTApp
extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> s = new HashSet<Class<?>>();
s.add(MyMessageBodyReader.class);
return s;
}
}
Jersey will scan all registered Mappers calling their isReadable() method until one returns true... if so, this MessageBodyReader instance will be used to serialize the content

Sounds like you want a custom MessageBodyReader that will determine the class and convert the data to appropriate subclass.
See JAX-RS Entity Providers for examples.

Related

How can I instantiate a specific sub-type for a #RequestBody parameter based on the requested URI for a Spring MVC controller method?

Given the following basic domain model:
abstract class BaseData { ... }
class DataA extends BaseData { ... }
class DataB extends BaseData { ... }
I want to write a Spring MVC controller endpoint thus ...
#PostMapping(path="/{typeOfData}", ...)
ResponseEntity<Void> postData(#RequestBody BaseData baseData) { ... }
The required concrete type of baseData can be inferred from the typeOfData in the path.
This allows me to have a single method that can handle multiple URLs with different body payloads. I would have a concrete type for each payload but I don't want to have to create multiple controller methods that all do the same thing (albeit each would do very little).
The challenge that I am facing is how to "inform" the deserialization process so that the correct concrete type is instantiated.
I can think of two ways to do this.
First use a custom HttpMessageConverter ...
#Bean
HttpMessageConverter httpMessageConverter() {
return new MappingJackson2HttpMessageConverter() {
#Override
public Object read(final Type type, final Class<?> contextClass, final HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// TODO How can I set this dynamically ?
final Type subType = DataA.class;
return super.read(subType, contextClass, inputMessage);
}
};
}
... which gives me the challenge to determine the subType based on the HttpInputMessage. Possibly I could use a Filter to set a custom header earlier when the URL is available to me, or I could use a ThreadLocal also set via a Filter. Neither sounds ideal to me.
My second approach would be to again use a Filter and this time wrap the incoming payload in an outer object which would then provide the type in a way that enables Jackson to do the work via #JsonTypeInfo. At the moment this is probably my preferred approach.
I have investigated HandlerMethodArgumentResolver but if I try to register a custom one it is registered AFTER the RequestResponseBodyMethodProcessor and that class takes priority.
Hmm, so after typing all of that out I had a quick check of something in the RequestResponseBodyMethodProcessor before posting the question and found another avenue to explore, which worked neatly.
Excuse the #Configuration / #RestController / WebMvcConfigurer mash-up and public fields, all for brevity. Here's what worked for me and achieved exactly what I wanted:
#Configuration
#RestController
#RequestMapping("/dummy")
public class DummyController implements WebMvcConfigurer {
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#interface BaseData {}
public static class AbstractBaseData {}
public static class DataA extends AbstractBaseData {
public String a;
}
public static class DataB extends AbstractBaseData {
public String b;
}
private final MappingJackson2HttpMessageConverter converter;
DummyController(final MappingJackson2HttpMessageConverter converter) {
this.converter = converter;
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(
new RequestResponseBodyMethodProcessor(Collections.singletonList(converter)) {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BaseData.class)
&& parameter.getParameterType() == AbstractBaseData.class;
}
#Override
protected <T> Object readWithMessageConverters(
NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
throws IOException, HttpMediaTypeNotSupportedException,
HttpMessageNotReadableException {
final String uri =
webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI();
return super.readWithMessageConverters(
webRequest, parameter, determineActualType(webRequest, uri));
}
private Type determineActualType(NativeWebRequest webRequest, String uri) {
if (uri.endsWith("data-a")) {
return DataA.class;
} else if (uri.endsWith("data-b")) {
return DataB.class;
}
throw new HttpMessageNotReadableException(
"Unable to determine actual type for request URI",
new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class)));
}
});
}
#PostMapping(
path = "/{type}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<? extends AbstractBaseData> post(#BaseData AbstractBaseData baseData) {
return ResponseEntity.ok(baseData);
}
}
The key to this is that I stopped using #RequestBody because that is what was preventing me overriding the built-in behaviour. By using #BaseData instead I get a HandlerMethodArgumentResolver that uniquely supports the parameter.
Other than that it was a case of assembling the two objects that already did what I needed, so autowire a MappingJackson2HttpMessageConverter and instantiate a RequestResponseBodyMethodProcessor with that one converter. Then pick the right method to override so that I could control what parameter type was used at a point that I had access to the URI.
Quick test. Given the following payload for both requests ...
{
"a": "A",
"b": "B"
}
POST http://localhost:8081/dummy/data-a
... gives a response of ...
{
"a": "A"
}
POST http://localhost:8081/dummy/data-b
... gives a response of ...
{
"b": "B"
}
In our real-world example this means that we will be able to write one method each that supports the POST / PUT. We need to build the objects and configure the validation possibly - or alternatively if we use OpenAPI 3.0 which we are investigating we could generate the model and validate without writing any further code ... but that's a separate task ;)

Is it possible to use #XmlHeader together with Jackson?

I'm marshalling my objects with Jackson (annotations) to JSON & XML and it works really great but there's a problem with XML.I want to add a DTD-File.I searched a little bit and found the #XmlHeader-Annotation (org.glassfish.jersey.message.XmlHeader) to add a header :
#Path("resources/xml/hashtagstatistic")
#GET
#XmlHeader("<!DOCTYPE note SYSTEM \"test.dtd\">")
#Produces(MediaType.APPLICATION_XML)
public Database getStatisticAsXml(){
return serviceController.getDatabase();
}
But it's not working. I tried the same with jaxb and there the header was added to my XML-Output.But I want to use Jackson because it returns easy my wanted structure (I don't like/want adapters). Is there a possibility to fix this or are there other solutions to handle this problem with the header?
Yeah it's a Jersey specific annotation, so Jackson won't know anything about it. I see a couple options. You could use a WriterInterceptor, and just write the header yourself.
#Provider
public class XmlHeaderWriterInterceptor implements WriterInterceptor {
#Context
private ResourceInfo info;
#Override
public void aroundWriteTo(WriterInterceptorContext context)
throws IOException, WebApplicationException {
final OutputStream outputStream = context.getOutputStream();
XmlHeader anno = info.getResourceMethod().getAnnotation(XmlHeader.class);
if (anno != null) {
String value = anno.value();
writeToStream(outputStream, value);
}
context.proceed();
}
}
Or you could create a MessageBodyWriter. But instead of implementing your own from scratch, you could just extend the one from Jackson (assuming this is what you're currently using)
#Provider
public class MyJackonXmlProvier extends JacksonJaxbXMLProvider {
#Context
private ResourceInfo info;
#Override
public void writeTo(Object value, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String,Object> httpHeaders,
OutputStream entityStream) {
// do same thing as example above
super.writeTo(pass, all, arguments)
}
Which ever one you use, just make sure to register it with the application.

How to retrieve resource method's Annoations in MessageBodyWriter in Jersey?

jersey 2.21.
I have a resource file like below
……
#POST
#Path("/userReg")
#Produces("application/json;charset=UTF-8")
public JsonResp userReg(UserRegReq userRegReq) throws LoginNameExists {
HttpHeaderUtils.parseHeaders(userRegReq, headers);
//JsonResp is a custom java class.
JsonResp result = new JsonResp();
//will throw LoginNameExists
User user = userManager.register(userRegReq.getLoginName(), userRegReq.getPassword());
//success
result.setResult(0);
result.setData(user.getId);
return result;
}
……
To return the result to client, I implement a custom MessageBodyWriter like below
#Produces("application/json")
public class MyRespWriter implements MessageBodyWriter<JsonResp> {
#Override
public boolean isWriteable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return type == JsonResp.class;
}
#Override
public long getSize(JsonResp jsonResp, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return 0;
}
#Override
public void writeTo(JsonResp jsonResp, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> multivaluedMap, OutputStream outputStream) throws IOException, WebApplicationException {
//if these no exception in userReg(),
//the parameter annotations contains the annotations
//such as POST, Path, Produces;
//but if there is an exception in userReg(),
//the parameter annotations contains none of POST, Path, Produces;
//So, is there any way to retrieve the original annotations all along?
//JsonUtils is a custom java class.
String data = JsonUtils.toJsonString(jsonResp);
Writer osWriter = new OutputStreamWriter(outputStream, "UTF-8");
osWriter.write(data);
osWriter.flush();
}
}
And to handle the exceptions, I implement a ExceptionMapper like this:
public class MyExceptionMapper implements ExceptionMapper<Exception> {
public Response toResponse(Exception e) {
JsonResp result = new JsonResp();
//error
result.setResult(-1);
result.setErrMsg("System error.");
return Response.ok(result, MediaType.APPLICATION_JSON_TYPE).status(Response.Status.OK).build();
}
}
Now, if everything is ok and there’s no exception, the code execution router is userReg() -> MyRespWriter.writeTo(), the parameter "annotations" of MyRespWriter.writeTo() contains the correct annotations of method userReg(), such as POST, Path, Produces.
But if userReg() throws exception, the code execution router is userReg() -> MyExceptionMapper.toResponse() -> MyRespWriter.writeTo(), the parameter "annotations" of method MyRespWriter.writeTo() has none of the annotations of method userReg().
I want to know, is there any way that MyRespWriter.writeTo() can retrieve the original annotations all along?
You can inject ResourceInfo, then get the Method with ri.getResourceMethod(), then call method.getAnnotations() to get the annotations.
public class MyRespWriter implements MessageBodyWriter<JsonResp> {
#Context
ResourceInfo ri;
...
Annotations[] annos = ri.getResourceMethod().getAnnotations();

Jersey route to #Consumes based on MediaType parameter?

I suspect this can't be done, but maybe there's a trick I'm missing. I want to use to different methods that take the same MediaType, but have different parameters to the mediatype. Perhaps this is abusing MediaType parameters...
#POST
#Consumes("application/json;internal=true")
public Response handleInternal(String request) {
}
#POST
#Consumes("application/json;internal=false")
public Response handleExternal(String request) {
}
Jersey complains I have two methods consuming the same MediaType, which is true. I was hoping it'd go on to pick the right one by the parameter. Is there some trick to making this work? In a nutshell, I have two use cases for how to treat the information coming in (specifically, domain level validation) and this seemed like a decent way to distinguish between those two.
You could use a MessageBodyReader along with two user types, one for internal json and the other for external json
1- Create two types than extends String (via delegation -using lombok is easier-):
#RequiredArgsConstructor
public class InternalJSON {
#Delegate
private final String _theJSONStr;
}
#RequiredArgsConstructor
public class ExternalJSON {
#Delegate
private final String _theJSONStr;
}
2- Create the MessageBodyReader type
#Provider
public class MyRequestTypeMapper
implements MessageBodyReader<Object> {
#Override
public boolean isReadable(final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
// this matches both application/json;internal=true and application/json;internal=false
return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE);
}
#Override
public Object readFrom(final Class<Object> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType,
final MultivaluedMap<String,String> httpHeaders,
final InputStream entityStream) throws IOException,
WebApplicationException {
if (mediaType.getSubType().equals("internal=true") {
// Build an InternalJSON instance parsing entityStream
// ... perhaps using JACKSON or JAXB by hand
} else if (mediaType.getSubType().equals("internal=false") {
// Build an ExternalJSON instance parsing entityStream
// ... perhaps using JACKSON or JAXB by hand
}
}
}
3- Register your MessageBodyReader at the Application (this is optional since jersey will scan the classpath for #Provider annotated types
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> s = new HashSet<Class<?>>();
...
s.add(MyRequestTypeMapper .class);
return s;
}
4- Reformat your rest methods usgin the two user types for internal and external json

In Jersey, how do I make some methods use the POJO mapping feature and some not?

I've got one resource (this is not a requirement) that has many different #GET methods in it. I've configured my web.xml to have this in it:
<init-param>
<param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
<param-value>true</param-value>
</init-param>
This turns on the POJO mapping feature which works great. It allows me to return a pojo and it gets automatically converted to JSON.
Problem is I've got some code I want to reuse that returns JSON as a String. I'm having trouble using these methods because Jersey does not interpret this as a JSON response, but as a String value to a JSON object. So if, for example, the method that returns a String returns an empty list the client sees
"[]"
instead of
[]
The problem is the JSON is wrapped in a double quotes. For these methods that return Strings, how can I tell Jersey to return the string as-is?
You probabily should have to use a custom response (or request) mapper.
1.- Create a class implementing MessageBodyWriter (or MessageBodyReader) in charge of writing / reading the response
#Provider
public class MyResponseTypeMapper
implements MessageBodyWriter<MyResponseObjectType> {
#Override
public boolean isWriteable(final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
... use one of the arguments (either the type, an annotation or the MediaType)
to guess if the object shoud be written with this class
}
#Override
public long getSize(final MyResponseObjectType myObjectTypeInstance,
final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
// return the exact response length if you know it... -1 otherwise
return -1;
}
#Override
public void writeTo(final MyResponseObjectType myObjectTypeInstance,
final Class<?> type,final Type genericType,
final Annotation[] annotations,
final MediaType mediaType,
final MultivaluedMap<String,Object> httpHeaders,
final OutputStream entityStream) throws IOException, WebApplicationException {
... serialize / marshall the MyResponseObjectType instance using
whatever you like (jaxb, etC)
entityStream.write(serializedObj.getBytes());
}
}
2.- Register the Mappers in your app
public class MyRESTApp
extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> s = new HashSet<Class<?>>();
s.add(MyResponseTypeMapper.class);
return s;
}
}
Jersey will scan all registered Mappers calling their isWriteable() method until one returns true... if so, this MessageBodyWriter instance will be used to serialize the content to the client

Categories