Difference between revisions of "GxRest/GxJRS/Responses"
Manuele.simi (Talk | contribs) (→Access to the Content) |
Manuele.simi (Talk | contribs) (→Content as Json serialization) |
||
Line 414: | Line 414: | ||
==== Content as Json serialization ==== | ==== Content as Json serialization ==== | ||
− | If the serialization does not contain processor-specific | + | If the serialization does not contain processor-specific annotations (like Jackson annotations), the response can deserialize the Json content. |
<source lang="Java"> | <source lang="Java"> | ||
import org.gcube.informationsystem.model.entity.Context; | import org.gcube.informationsystem.model.entity.Context; | ||
Line 421: | Line 421: | ||
Context returnedContext = response.tryConvertStreamedContentFromJson(Context.class); | Context returnedContext = response.tryConvertStreamedContentFromJson(Context.class); | ||
</source> | </source> | ||
+ | |||
+ | If processor-specific annotations are in the request, the content can be retrieved as String (see above) and then deserialized with the preferred Json processor. | ||
==== Content from original Response ==== | ==== Content from original Response ==== |
Revision as of 02:58, 3 May 2018
There are two classes of responses in gxRest. They differ for the side of the communication they serve:
- outbound responses, that model messages, statuses or errors returned from the web application
- inbound responses, that provide facilities to interpret the response from the webapp and access to its content at the client side.
It's not mandatory that the client of a webapp uses (and depends on) gxRest. If it is just interested in the HTTP status code embedded in the response, it can retrieve it with the mechanisms offered by the underlying web framework, without using inbound responses.
Contents
Outbound Responses
Outbound responses are the information returned from a webapp to its client. They model a success status or an error condition.
Success Responses
Success responses can be created in fluent API manner. All the parameters are optional except the ones requested by the method that constructs the new response instance.
The following code shows how to return a CREATE (201) response from a resource method (in the RESTful meaning) that creates a new resource.
import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; @Path("context") public class RMContext { @POST public Response create(...) { //Resource successfully created URI location = ... //URI of the new resource return GXOutboundSuccessResponse.newCREATEResponse(location) .withMessage("Context successfully created.") .ofType(ResourceInitializer.APPLICATION_JSON_CHARSET_UTF_8) .build(); } }
The following code shows how to return an OK (200) response from a resource method that deletes resources.
import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; @Path("context") public class RMContext { @DELETE public Response delete(...) { //Resource successfully deleted return GXOutboundSuccessResponse.newOKResponse() .withMessage("Context successfully deleted.") .ofType(ResourceInitializer.APPLICATION_JSON_CHARSET_UTF_8) .build(); } }
Error Responses
gxRest offers two approaches to return an error from the webapp to its client: Plain Old Java Exceptions (or POJEs) and Code Exceptions.
Outbound error responses share the following advantages:
- abstract over an error response returned by a REST resource method
- throw an error response wherever the developer feels it is safe to terminate the execution
- simplify the method declaration (they do not need to be declared).
Thus, an important difference between success and error responses is that error responses can be returned at any downstream execution point in the resource method, while success responses are returned at the end of the execution. In this sense, they perfectly parallel with Exceptions and return values in a normal piece of Java code.
Plain Old Java Exceptions
Any Java programmer is used to manage error conditions by throwing and catching Exceptions. gxRest brings this behavior into web applications with a mechanism called POJE (Plain Old Java Exception)..
Let's revise the create method previously shown to demonstrate how to send to a client an Exception (RMContextAlreadyExistException in this case) instance with an associated error message.
import javax.ws.rs.core.Response; import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; import org.gcube.resourcemanagement.manager.io.rs.RMContextAlreadyExistException; @Path("context") public class RMContext { @POST public Response create(...) { //Oh no, something failed here GXOutboundErrorResponse.throwException(new RMContextAlreadyExistException("Context already exists.")); //Resource successfully created URI location = ... //URI of the new resource return GXOutboundSuccessResponse.newCREATEResponse(uri) .withMessage("Context successfully created.") .ofType(ResourceInitializer.APPLICATION_JSON_CHARSET_UTF_8).build(); } }
GXOutboundErrorResponse.throwException
allows to throw the exception and its message back to the remove REST client. The client can then retrieve the exception from the GXInboundResponse.
POJE with stacktrace
By default, POJEs are rebuilt at client side with the Java reflection API without their original stacktrace. This is usually good enough for a try-catch statement or re-thrown the exception upstream. Stacktraces are not sent back to the remote REST client because they considerably increase the size of the HTTP response (of a factor of 10 times or more, depending on the length of the trace).
However, in some cases (especially for debugging and testing purposes), it might be useful to access to the original trace (or part of it) at client side. GXOutboundErrorResponse
offers a way to add a configurable number of trace elements to the exception. The following example shows how to invoke the throwExceptionWithTrace
method that does the job:
import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; import org.gcube.resourcemanagement.manager.io.rs.RMContextDoesNotExistException; public class RMTestForGXRest { @DELETE @Path("{" + UUID_PARAM + "}") public Response delete(@PathParam(UUID_PARAM) String uuid) { methodOne(); return ...; } private void methodOne() { methodTwo(); } private void methodTwo() { //something fails here GXOutboundErrorResponse.throwExceptionWithTrace(new RMContextDoesNotExistException("Error in methodTwo"),3); } }
The second parameter (an integer) allows to specify how many elements in the stacktrace we want to send back to the client. This is especially useful when debugging a service and we want to know the exact line of code where the Exception occurred.
On the client side, if the stacktrace of the exception above is printed, it will show something like:
org.gcube.resourcemanagement.manager.io.rs.RMContextDoesNotExistException: Error in methodTwo at org.gcube.resourcemanagement.manager.webapp.rs.RMTestForGXRest.methodTwo(RMTestForGXRest.java:45) at org.gcube.resourcemanagement.manager.webapp.rs.RMTestForGXRest.methodOne(RMTestForGXRest.java:38) at org.gcube.resourcemanagement.manager.webapp.rs.RMTestForGXRest.delete(RMTestForGXRest.java:33)
This trace reports the class of the exception, the message in the exception and the first 3 elements of its stracktrace (according to the parameter above).
Code Exceptions
Although POJEs are commonly used in software written in Java, sometimes it is more convenient to deal with error codes (a.k.a. statuses).
Instead of creating separate classes for each exception type, the idea behind Code Exceptions is to use a single, system-wide exception class. And make it extend WebApplicationException
(from javax.ws.rs
) that in turn extends RuntimeException
.
A Code Exception is capable to hold an error code and an associated message, wrap them into a REST response and return them to the caller.
Compared to POJEs, Code Exceptions have these additional advantages:
- provide a single (and self-documented) point where to declare error codes and messages returned by the webapp
- reduce the class count in a project (not to mention in a system) avoiding the proliferation of custom Exception classes
- simplify the client code that manages only the error codes it is capable to handle
- remove the need to declare exceptions that sometimes aren’t going to be handled anyway.
On the other hand, Code Exceptions require a bit of more coding than POJEs.
Declare your Error Codes
The first step is to declare an enumeration of the error codes and associated messages. The enum must extend the ErrorCode
interface.
import org.gcube.common.gxrest.response.outbound.ErrorCode; public enum RMCode implements ErrorCode { INVALID_METHOD_REQUEST(0, "The request is invalid."), MISSING_PARAMETER(1,"Required query parameter is missing."), MISSING_HEADER(2, "Required header is missing."), CONTEXT_ALREADY_EXISTS(3, "Context already exists at the same level of the hierarchy."), CONTEXT_PARENT_DOES_NOT_EXIST(4, "Failed to validate the request. The request was not submitted to the Resource Registry."), INVALID_REQUEST_FOR_RR(5, "Failed to validate the request. The request was not submitted to the Resource Registry."), GENERIC_ERROR_FROM_RR(6, "The Resource Registry returned an error."); private int id; private String msg; private RMCode(int id, String msg) { this.id = id; this.msg = msg; } public int getId() { return this.id; } public String getMessage() { return this.msg; } }
Different enums can be created for different resource methods or per REST resource. The granularity is up to the developer.
Throw Error Codes
The following method fails to create a Context given certain parameters (not shown).
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.gcube.resourcemanagement.manager.io.rs.RMCode; import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; @Path("context") public class RMContext { @POST public Response create(...) { //Error condition: we detect that the requested context already exists GXOutboundErrorResponse.throwErrorCode(RMCode.CONTEXT_ALREADY_EXISTS); } }
Do note that GXOutboundErrorResponse
can be thrown by any object invoked within the create method without the need of declaring them (this is because they use RuntimeExceptions
).
By default, a GXOutboundErrorResponse has the NOT ACCEPTABLE(406)
status. It is possible to assign a different status to the response by using overloaded methods of the class as shown below.
import javax.ws.rs.core.Response; import org.gcube.resourcemanagement.manager.io.rs.RMCode; import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; @Path("context") public class RMContext { @POST public Response create(...) { // ops, the parent context does not exist GXOutboundErrorResponse.throwErrorCode(Response.Status.CONFLICT, RMCode.CONTEXT_PARENT_DOES_NOT_EXIST); } }
Local Code Exceptions
gxRest provides another type of exception called LocalCodeException
. A local code exception holds an error code and a message and it is intended to be used internally in the webapp to propagate such information whenever the developer feels it's not the right time to terminate the execution of the REST request (thus using a GXOutboundErrorResponse). The same enums extending the ErrorCode
used for the web exceptions can be used to create a local code exception.
Intentionally, they do not extend RuntimeException because of their declared scope: the error code inside the local exception is intended to be consumed by the webapp, not by the caller (at least, not yet). The method that throws them must declare this behavior in the signature and the method caller must explicitly catch it (just as any other standard Java exception).
At any time, a local code exception can be converted into a web code exception and therefore it is immediately returned to the caller.
The conversion is very simple.
import org.gcube.common.gxrest.response.outbound.LocalCodeException; import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; import org.gcube.resourcemanagement.manager.io.rs.RMCode; public class Test { private void doSomething() throws LocalCodeException { throw new LocalCodeException(RMCode.INVALID_METHOD_REQUEST); } private void callDoSomething() { try { doSomething(); } catch (LocalCodeException lce) { //do something with lce switch (lce.getId()) { case 3: //manage the error with code 3 (CONTEXT_ALREADY_EXISTS, see RMCode) break; case 4: //manage the error with code 4 (CONTEXT_PARENT_DOES_NOT_EXIST, see RMCode) break; default: //can't manage the other cases break; } //and then return it to the webapp's client GXOutboundErrorResponse.throwErrorCode(lce); } } }
It's not mandatory to convert a local exception into a web exception. Local exceptions can also just be used as a mean to propagate and handle errors within the webapp. For instance the switch statement in the example above could convert and return the local exception only in the default case (i.e. when the method doesn't know how to recover from a case not managed before).
HTTP Error Statuses
GXOutboundErrorResponse also supports returning just an HTTP error status (>= 400) with an associated message.
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; @Path("context") public class RMContext { @POST public Response create(...) { // ops, the operation is not permitted GXOutboundErrorResponse.throwHTTPErrorStatus(Status.UNAUTHORIZED, "You don't have the permission to create the context."); } }
As for the previous error responses, also this one can be thrown at any downstream execution point of the resource method.
Inbound Responses
Inbound responses are the information received by a client following the invocation of a resource method on a web application.
They can be obtained as returned object from a GXRequest:
import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.common.gxrest.request.GXHTTPRequest; GXInboundResponse response = GXHTTPRequest.newRequest(...).post();
... or by wrapping a javax.ws.rs.core.Response
object:
import javax.ws.rs.core.Response; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; // invoke the create method and get a response. Response create = target("context").queryParam(...).request() .post(Entity.entity(ISMapper.marshal(newContext), MediaType.APPLICATION_JSON + ";charset=UTF-8")); //wrap the response GXInboundResponse response = new GXInboundResponse(create);
The code above makes usage a WebTarget (e.g. the one provided by the Jersey testing framework) to invoke the post method.
Check the type of Responses received
The following code demonstrates how to check what type of response has been received. An inbound response can be generated by gxRest or not. The difference between the two is in the error handling.
- a response coming from a GXOutboundResponse can rebuild the Exception, the ErroCode, get the HTTP code or read the content of the response;
- a response coming from a service that does not use gxRest can get get the HTTP code or read the content of the response.
Either cases, the client can always fetch original ws.Response
.
import javax.ws.rs.core.Response; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; GXInboundResponse response = //request if (response.hasGXError()) { //this means that the error response has been generated at service side with gxRest as well if (response.hasException()) { //use the exception in the response (see below) } else if (responce.hasErrorCode()) { //use the error code in the response (see below) } } else { //assuming a created (200) code was expected if (response.hasCREATEDCode()) { System.out.println("Resource successfully created!"); System.out.println("Returned content: " + response.getStreamedContentAsString()); } else { System.out.println("Resource creation failed. Returned status:" + response.getHTTPCode()); } }
Success Responses
Once the client gets the response object, it can check if it is a success or error response and reads the associated message (if any).
For instance, a POST request to a resource collection (that in REST means requesting the creation of a new resource in that collection) would verify that the HTTP CREATED (201) status is returned with the response and then retrieve the URI of the new resource:
import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import java.net.URI; GXInboundResponse response = //invoke post method with a request if (!response.hasGXError() { if (response.hasCREATEDCode()) { logger.info("The web app says: " + response.getMessage()); } } URI location = response.getResourceURI();
DELETE, PUT, GET requests would instead check that the HTTP OK (200) is returned:
import org.gcube.common.gxrest.response.inbound.GXInboundResponse; GXInboundResponse response = //invoke delete method if (response.hasOKCode()) { logger.info("The web app says: " + response.getMessage()); }
Access to the Content
The content of a response is similar to the returned value of a method of function: the client must know what is the expected content (in the same way it knows the formal parameters the request).
Therefore, GXInboundResponse provides convenient methods to access to the content, but it's up to client to invoke the correct interpretation of the content, based on the expected result.
Content as String
GXInboundResponse response = //request String value = response.getStreamedContentAsString();
Content as Json serialization
If the serialization does not contain processor-specific annotations (like Jackson annotations), the response can deserialize the Json content.
import org.gcube.informationsystem.model.entity.Context; GXInboundResponse response = //request Context returnedContext = response.tryConvertStreamedContentFromJson(Context.class);
If processor-specific annotations are in the request, the content can be retrieved as String (see above) and then deserialized with the preferred Json processor.
Content from original Response
Available only if GXWebtGXWebTargetAdapterRequest is used for the request.
import javax.ws.rs.core.Response; GXInboundResponse response = GXWebTargetAdapterRequest.newRequest(..); Response rsResponse = response.getSource(); ExpectedType content = response.readEntity(...);
[TBP]
Error Responses
If the has*Code() returns false, the response is a failure. Depending on the implementation of the resource method called, the response holds an Exception or an Error Code.
With Exceptions
Let's suppose the delete method throws a custom Exception if the resource being deleted does not exist:
import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; import org.gcube.resourcemanagement.manager.io.rs.RMContextDoesNotExistException; @Path("context") public class RMContext { @DELETE public Response delete(...) { //Oh no, the context does not exist GXOutboundErrorResponse.throwException(new RMContextDoesNotExistException("The context with id "+ id +" does not exist. It can't be deleted.")); //Context successfully deleted return GXOutboundSuccessResponse.newOKResponse() .withMessage("Context successfully deleted.") .ofType(ResourceInitializer.APPLICATION_JSON_CHARSET_UTF_8) .build(); } }
At the client side, we can retrieve the exception from the response and then it can be thrown and dealt with upstream:
import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.resourcemanagement.manager.io.rs.RMContextDoesNotExistException; public void delete() throws RMContextDoesNotExistException { GXInboundResponse response = //invoke delete method if (response.hasException()) { try { throw response.getException(); } catch (Exception e) { logger.error("An exception (of type "+e.getClass().getName()+") was returned by the RM service."); logger.error("Error message in the exception " + e.getMessage()); throw e; } } }
The deal to support this use case is that the custom Exception class (RMContextDoesNotExistException
in this example) is available on the classpath both at client and webapp side (e.g. it is part of a shared library).
Built-in exceptions (such as IOException) do not need shared code as they are available in standard Java libraries.
With Error Codes
If the resource method returns with error codes instead of Exceptions, GXInboundResponse provides methods to access to these codes. In turn, they can be then converted into the original enum value.
The following resource method throws error codes when certain conditions occur:
import javax.ws.rs.core.Response; import org.gcube.resourcemanagement.manager.io.rs.RMCreateContextCode; import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; @Path("context") public class RMContext { @POST public Response create(...) { //Error condition: we detect that the requested context already exists GXOutboundErrorResponse.throwErrorCode(RMCreateContextCode.CONTEXT_ALREADY_EXISTS); //Error condition: we detect that the parent does not exist GXOutboundErrorResponse.throwErrorCode(RMCreateContextCode.CONTEXT_PARENT_DOES_NOT_EXIST); } }
On the client side, we can fetch and convert back the error code as follows:
import org.gcube.common.gxrest.response.outbound.CodeFinder; import org.gcube.common.gxrest.response.outbound.ErrorCode; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.resourcemanagement.manager.io.rs.RMCreateContextCode; GXInboundResponse response = //invoke create method if (response.hasErrorCode()) { ErrorCode code = response.getErrorCode(); RMCreateContextCode realCode = CodeFinder.findAndConvert(code, RMCreateContextCode.values()); switch (realCode) { case CONTEXT_ALREADY_EXISTS: //handle the error break; case CONTEXT_PARENT_DOES_NOT_EXIST: //handle the error break; default: //handle the other cases break; } }
CodeFinder
is an utility provided gxRest. Similarly to the case of custom Exception, this behavior is possible only if the enum with the ErrorCodes is available in a component shared between the webapp and the client.
If the Enum is not available at client side, it's not mandatory to convert the error code into its original value. It can be treated as a plain returned integer value:
import org.gcube.common.gxrest.response.outbound.ErrorCode; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; GXInboundResponse response = //invoke the resource method ErrorCode code = response.getErrorCode(); switch (code.getId()) { case 3: //manage the error with code 3 (CONTEXT_ALREADY_EXISTS, see org.gcube.resourcemanagement.manager.io.rs.RMCreateContextCode) break; case 4: //manage the error with code 4 (CONTEXT_PARENT_DOES_NOT_EXIST, see org.gcube.resourcemanagement.manager.io.rs.RMCreateContextCode) break; default: //can't manage the other cases break; }
Do note that this code IS NOT the HTTP status (see next section).
Fetch the HTTP information
Regardless the type of response received from the web application, it is always possible to retrieve the information related to the HTTP code as follows:
import org.gcube.common.gxrest.response.inbound.GXInboundResponse; GXInboundResponse response = //invoke the resource method //access the HTTP code logger.info("Returned HTTP status: " + response.getHTTPCode()); logger.info("Returned message: " + response.getMessage());
Dedicate methods are available to check if it holds commonly used codes:
GXInboundResponse response = //invoke the resource method if (response.hasCREATEDCode()) { //response has a CREATED (201) HTTP code } if (response.hasOKCode()) { //the response has a OK (200) HTTP code } if (response.hasNOT_ACCEPTABLECode()) { //the response has a NOT_ACCEPTABLE (406) HTTP code } if (response.hasBAD_REQUESTCode()) { //the response has a BAD_REQUEST (400) HTTP code }