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();
Related
This may be a strange question, although I wonder why it hasn't been asked or proposed before... so please correct me if any ignorance.
First off, I am using Jackson in conjunction with Spring and the #ResponseBody annotation.
Currently, for every request handler I am returning a "Response" wrapper object, as that is what the client expects. This wrapper is quite simple:
{ "response": { "data" : ACTUAL_DATA } }
Thing is, I'm not a fan of explicitly wrapping each return value for all my request handlers. I also do not like having to unwrap these response wrappers in my unit tests.
Rather, I wonder if it were possible to return the ACTUAL_DATA as it were, and to intercept and wrap this data elsewhere.
If this is in fact possible, would it then be possible to read the annotations attached to the intercepted request handler? This way I can use custom annotations to decide how to wrap the data.
For example something like this would be amazing (note that #FetchResponse and #ResponseWrapper are made up proposed annotations):
#RequestMapping(...)
#FetchResponse
#ResponseBody
public List<User> getUsers() {
...
}
#ResponseWrapper(FetchResponse.class)
public Object wrap(Object value) {
ResponseWrapper rw = new ResponseWrapper();
rw.setData(value);
return rw;
}
Anyone familiar with this territory? Or alternatively, and reasons why this might be bad practice?
Well, looks like I'm looking for Spring's "ResponseBodyAdvice" and "AbstractMappingJacksonResponseBodyAdvice".
For anybody looking on more info on this topic: I was facing the same issue as well, and thanks to the tip of kennyg, i managed to come up with the following solution:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
#ControllerAdvice
public class JSendAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof JSendResponse) {
return body;
}
return new JSendResponse<>().success(body);
}
}
This solution wraps all the objects returned in your controllers inside a (for this example) JSendResponse class, which saves you the hassle of returning JSendResponses in all of your controller methods.
I know it's been a while since the answer was accepted but I recently stumbled onto an issue with Jackson that allowed me to discover a problem with using ResponseBodyAdvice.
Jackson will not correctly serialize your polymorphic types that use #JsonTypeInfo / #JsonSubTypes if during runtime the values of your types are not known: i.e. for example if you have a generic container type like class ResponseWrapper<T> { List<T> objects; }. That is unless you provide Jackson with specialization of that generic type before you ask it to serialize your value, refer to Why does Jackson polymorphic serialization not work in lists? . Spring does this for you when you return say a list of T and that T is known because it's provided explicitly in the method return type (as in public List<MyEntity> getAllEntities();).
If you simply implement ResponseBodyAdvice and return a new, wrapped value from beforeBodyWrite() then Spring will no longer know your full generic type with its specialization, and it will serialize your response as ResponseWrapper<?> instead of ResponseWrapper<MyEntity>.
The only way around this is to both extend from AbstractJackson2HttpMessageConverter and override writeInternal(). See how the method treats the type here: https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java#L437
And you also need to implement a Controller advice using AbstractMappingJacksonResponseBodyAdvice and your own custom MappingJacksonValue that includes Type targetType that custom HttpMessageConverter will use.
ResponseWrapper
public class ResponseWrapper<T> {
#Nullable Error error;
T result;
public ResponseWrapper(T result) {
this.result = result;
}
}
WrappingAdvice
#Component
public class WrappingAdvice extends AbstractMappingJacksonResponseBodyAdvice {
#Override
protected MappingJacksonValue getOrCreateContainer(Object body) {
MappingJacksonValue cnt = super.getOrCreateContainer(body);
if (cnt instanceof MyMappingJacksonValue) {
return cnt;
}
return new MyMappingJacksonValue(cnt);
}
#Override
protected void beforeBodyWriteInternal(
MappingJacksonValue bodyContainer, MediaType contentType,
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
MyMappingJacksonValue cnt = (MyMappingJacksonValue) bodyContainer;
Type targetType = getTargetType(bodyContainer.getValue(), returnType);
cnt.setValue(new ResponseWrapper(cnt.getValue()));
cnt.setTargetType(TypeUtils.parameterize(
ResponseWrapper.class,
targetType));
}
/**
* This is derived from AbstractMessageConverterMethodProcessor
*/
private Type getTargetType(Object value, MethodParameter returnType) {
if (value instanceof CharSequence) {
return String.class;
}
Type genericType;
if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
genericType = ResolvableType.forType(returnType.getGenericParameterType()).getGeneric().getType();
} else {
genericType = returnType.getGenericParameterType();
}
return GenericTypeResolver.resolveType(genericType, returnType.getContainingClass());
}
public static class MyMappingJacksonValue extends MappingJacksonValue {
private Type targetType;
public MyMappingJacksonValue(MappingJacksonValue other) {
super(other.getValue());
setFilters(other.getFilters());
setSerializationView(other.getSerializationView());
}
public Type getTargetType() {
return targetType;
}
public void setTargetType(Type targetType) {
this.targetType = targetType;
}
}
}
JsonHttpMessageBodyConverter
#Component
public class JsonHttpMessageBodyConverter extends AbstractJackson2HttpMessageConverter {
// omitted all constructors
#Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof WrapAPIResponseAdvice.MyMappingJacksonValue) {
type = ((WrapAPIResponseAdvice.MyMappingJacksonValue) object).getTargetType();
}
super.writeInternal(object, type, outputMessage);
}
}
I am trying to map a REST-call with my POJO. The POJO looks like this:
public class ResultWrapper implements Serializable{
private int total;
private List<Movies> movies; ... getters and setters
In the call I use:
WebResource webResource = client.resource(RequestURI + URLEncoder.encode(movie, "UTF-8"));
ResultWrapper result = webResource.accept("application/json").get(ResultWrapper.class);
The error:
com.sun.jersey.api.client.ClientHandlerException: A message body reader for Java class models.ResultWrapper, and Java type class models.ResultWrapper, and MIME media type text/javascript; charset=ISO-8859-1 was not found
Client is a Jersey client. I have tried making the call from Chrome (Postman) and it sais that the application type returned is "text/javascript", not "application/json" as one would expect? I think that would be my problem.
Is there any way I can get the ObjectMapper to resolve that it is actually "application/json" and not "text/javascript". I have tried using String.class and then I get the Json object just fine.
My purpose is to use automatic mapping from Jersey Client.
Thanks for any tips or advice.
try adding annotation #Produces (MediaType.APPLICATION_JSON )
You can try this:
#Provider
#Produces(application/json)
public class YourTestBodyWriter implements MessageBodyWriter<ResultWrapper> {
private static final Logger LOG = LoggerFactory.getLogger(YourTestBodyWriter.class);
#Override
public boolean isWriteable(
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType)
{
return ResultWrapper.class.isAssignableFrom(type);
}
#Override
public long getSize(
ResultWrapper t,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType)
{
return -1;
}
#Override
public void writeTo(
ResultWrapper t,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException
{
String message = t.someMethod()
entityStream.write(message.getBytes(Charsets.UTF_8));
LOG.info(message);
}
}
Add in your app run():
// Serializer
environment.jersey().register(new YourTestBodyWriter ());
Thats the normal way in your application.
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
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
I am trying to create a rest api server on top of my quartz scheduler. I want to be able to return the org.quartz.Trigger and org.quartz.JobDetail objects as JSON. The problem is that I cannot add the #XmlRootElement to these classes without having to recompile the jar and this causes problems with future upgrades etc. I have tested and am able to serialize a list of classes when adding the #XmlRootElement but when I try to return a List I get the error "A message body writer for Java class java.util.ArrayList, and Java type java.util.List, and MIME media type application/json was not found". I have tried adding a custom MessageBodyWriter but that does not seem to fix the problem either. Is there a way to marshal the quartz classes to JSON without having to modify the quartz code to add the #XmlRootElement. I am using this in an embedded web server with jetty btw.
#Path("/jobs")
public class JobsResource {
#GET
#Produces(MediaType.APPLICATION_JSON)
public List<Trigger> listScheduledJobs() throws SchedulerException {
return TaskEngine.getInstance().listScheduledJobs();
}
}
Web server configuration
public class TaskEngineWebServer {
private static final Logger logger = Logger.getLogger(TaskEngineWebServer.class.getName());
private Server server;
public TaskEngineWebServer() {
this(8585);
}
public TaskEngineWebServer(Integer port) {
server = new Server(port);
logger.info("Configuring rest service to start at url /r");
ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.NO_SECURITY);
//handler.getInitParams().put("com.sun.jersey.api.json.POJOMappingFeature", "true");
PackagesResourceConfig packagesResourceConfig = new PackagesResourceConfig("com.hp.vf.scheduler.server.rest");
ServletContainer servletContainer = new ServletContainer(packagesResourceConfig);
handler.addServlet(new ServletHolder(servletContainer), "/r/*");
server.setHandler(handler);
logger.info("Done configuring rest service");
}
public void start() throws Exception {
server.start();
}
public void stop() throws Exception {
server.stop();
}
public boolean isStarted() {
return server.isStarted();
}
public boolean isStopped() {
return server.isStopped();
}
}
I dont think you can return a List as JSON directly. You need to have a wrapper class which contains this list. For eg try something like this
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public class TriggerWrapper{
private List<Triggers> triggers;
public List<Triggers> getTriggers(){
if(triggers==null){
triggers = new ArrayList<Triggers>();
}
return triggers;
}
}
Then in your rest service class :
#Path("/jobs")
public class JobsResource {
#GET
#Produces(MediaType.APPLICATION_JSON)
public TriggerWrapper listScheduledJobs() throws SchedulerException {
TriggerWrapper response = new TriggerWrapper();
List<Triggers> triggers = TaskEngine.getInstance().listScheduledJobs();
response.getTriggers.addAll(triggers);
return response;
}
}
Your json would something like this :
{
"triggerwrapper": {
"triggers": [
{
"triggerid": 1
},
{
"triggerid": 2
}
]
}
}
And ofcourse if you want you can drop the root element tag from your json its configurable in jersey.
I finally figured out a clean solution, it involves creating my own MediaBodyWriter class and adding it as a provider. You have to make sure you are not using the jersey-bundle jar as the default jaxb to json provider will override yours.
jars required
jersey-core
jersey-servlet
jersey-server
jackson-annotations
jackson-databind
jackson-core
I found this MediaWriter example on the web somewhere. Sorry for not having the url but thanks to whoever write it.
#Provider
#Produces({ MediaType.APPLICATION_JSON })
public class JacksonWriter implements MessageBodyWriter<Object> {
private static final ObjectMapper MAPPER = new ObjectMapper();
#Override
public boolean isWriteable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
return true;
}
#Override
public long getSize(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}
#Override
public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) {
try {
MAPPER.writeValue(entityStream, value);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
When it loads you will see a log message that your provider was loaded.
This gave me the json output I was expecting as it does not rely on the JAXB annotations and simply uses the object mapper/ reflection. Probably less efficient but for my case it does not matter.