Difference between revisions of "GxRest/GxJRS/Responses"
Manuele.simi (Talk | contribs) (→Inbound Responses) |
Manuele.simi (Talk | contribs) (→POJEs outside a Response) |
||
(96 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | There are two classes of responses in | + | There are two classes of responses in [[GxRest/GxJRS|gxJRS]]. They differ for the side of the communication they serve: |
− | * outbound responses, that model messages, statuses | + | * outbound responses, that model messages, statuses or errors returned from the web application |
− | * inbound responses, that provide facilities to interpret the response and access to its content at the client side. | + | * 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) gxJRS. 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. | ||
= Outbound Responses = | = Outbound Responses = | ||
− | Outbound responses are the information returned from a webapp to its client. They | + | Outbound responses are the information returned from a webapp to its client. They model a success status or an error condition. With [[GxRest/GxJRS/Responses#POJEs_outside_a_Response|only one exception]], gxJRS responses are instances of ''javax.ws.rs.core.Response''. Therefore, it is expected that resource (in the REST sense) methods declare to return a Response in their signature. |
== Success Responses == | == Success Responses == | ||
Line 16: | Line 17: | ||
<source lang="Java"> | <source lang="Java"> | ||
import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; | import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; | ||
+ | import javax.ws.rs.core.MediaType; | ||
@Path("context") | @Path("context") | ||
Line 28: | Line 30: | ||
return GXOutboundSuccessResponse.newCREATEResponse(location) | return GXOutboundSuccessResponse.newCREATEResponse(location) | ||
.withMessage("Context successfully created.") | .withMessage("Context successfully created.") | ||
− | .ofType( | + | .ofType(MediaType.APPLICATION_JSON) |
.build(); | .build(); | ||
} | } | ||
Line 38: | Line 40: | ||
<source lang="Java"> | <source lang="Java"> | ||
import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; | import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; | ||
+ | import javax.ws.rs.core.MediaType; | ||
@Path("context") | @Path("context") | ||
Line 49: | Line 52: | ||
return GXOutboundSuccessResponse.newOKResponse() | return GXOutboundSuccessResponse.newOKResponse() | ||
.withMessage("Context successfully deleted.") | .withMessage("Context successfully deleted.") | ||
− | .ofType( | + | .ofType(MediaType.APPLICATION_JSON) |
.build(); | .build(); | ||
} | } | ||
} | } | ||
</source> | </source> | ||
− | |||
− | |||
== Error Responses == | == Error Responses == | ||
Line 65: | Line 66: | ||
* throw an error response wherever the developer feels it is safe to terminate the execution | * 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). | * 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 === | === Plain Old Java Exceptions === | ||
− | Any Java programmer is used to manage error conditions by throwing and catching 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 ( | + | 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. |
<source lang="Java"> | <source lang="Java"> | ||
import javax.ws.rs.core.Response; | import javax.ws.rs.core.Response; | ||
+ | import javax.ws.rs.core.MediaType; | ||
import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; | import org.gcube.common.gxrest.response.outbound.GXOutboundSuccessResponse; | ||
import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; | import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; | ||
+ | import org.gcube.resourcemanagement.manager.io.rs.RMContextAlreadyExistException; | ||
@Path("context") | @Path("context") | ||
Line 84: | Line 89: | ||
//Oh no, something failed here | //Oh no, something failed here | ||
− | GXOutboundErrorResponse.throwException(new | + | GXOutboundErrorResponse.throwException(new RMContextAlreadyExistException("Context already exists.")); |
− | + | ||
//Resource successfully created | //Resource successfully created | ||
Line 92: | Line 96: | ||
return GXOutboundSuccessResponse.newCREATEResponse(uri) | return GXOutboundSuccessResponse.newCREATEResponse(uri) | ||
.withMessage("Context successfully created.") | .withMessage("Context successfully created.") | ||
− | .ofType( | + | .ofType(MediaType.APPLICATION_JSON).build(); |
− | + | ||
} | } | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | <code>GXOutboundErrorResponse.throwException</code> allows to throw the exception and its message back to the remove REST client. The client can then retrieve the exception from the [[GxRest/Responses#Inbound_Responses|GXInboundResponse]]. | ||
+ | |||
+ | === POJEs outside a Response === | ||
+ | In some cases (legacy code, Services not fully compliant with the REST approach), throwing a POJE across a JAX-RS call is useful even if the method does not return an instance of ''javax.ws.rs.core.Response''. | ||
+ | |||
+ | JAX-RS api has introduced generic and pluggable interfaces called ''MessageBodyWriter'' (for doing the marshalling) and ''MessageBodyReader'' (for doing the unmarshalling) to support the conversion of a Java type to a stream. gxJRS implements these two interfaces to throw exceptions back to the client in resource methods that return String objects. | ||
+ | |||
+ | These are the two implementations: | ||
+ | <source lang="Java"> | ||
+ | org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextWriter | ||
+ | org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextReader | ||
+ | </source> | ||
+ | |||
+ | |||
+ | At service side, the marshaller must be registered along with the other components of the JAX-RS application: | ||
+ | |||
+ | <source lang="Java"> | ||
+ | import javax.ws.rs.core.Application; | ||
+ | import org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextWriter; | ||
+ | |||
+ | @Path("app_path") | ||
+ | public class MyWebApp extends Application { | ||
+ | |||
+ | @Override | ||
+ | public Set<Class<?>> getClasses() { | ||
+ | final Set<Class<?>> classes = new HashSet<Class<?>>(); | ||
+ | |||
+ | // register resources and features | ||
+ | classes.add(MyResource1.class); | ||
+ | classes.add(MyResource2.class); | ||
+ | |||
+ | // register the marshaller | ||
+ | classes.add(SerializableErrorEntityTextWriter.class); | ||
+ | |||
+ | return classes; | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | At client side, the unmarshaller must be registered as custom JAX-RS component in the request: | ||
+ | |||
+ | <source lang="Java"> | ||
+ | import org.gcube.common.gxrest.request.GXWebTargetAdapterRequest; | ||
+ | import org.gcube.common.gxrest.response.inbound.GXInboundResponse; | ||
+ | import org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextReader; | ||
+ | |||
+ | |||
+ | GXWebTargetAdapterRequest requestAdapter = TargetFactory.stubFor(service).getAsGxRest(address); | ||
+ | |||
+ | // register the unmarshaller | ||
+ | requestAdapter.register(SerializableErrorEntityTextReader.class); | ||
+ | |||
+ | // send the request and get the response back | ||
+ | GXInboundResponse response = requestAdapter.put(...); | ||
+ | |||
+ | //handle the response in the usual way | ||
+ | |||
+ | </source> | ||
+ | |||
+ | Besides the registration of the two JAX-RS components, throwing and handling the exception is the same as with the Response. See [[GxRest/GxJRS/Responses#With_Exceptions|Responses with Exceptions]]. | ||
+ | |||
+ | === POJEs 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. <code>GXOutboundErrorResponse</code> offers a way to add a configurable number of trace elements to the exception. The following example shows how to invoke the <code>throwExceptionWithTrace</code> method that does the job: | ||
+ | |||
+ | <source lang="Java"> | ||
+ | |||
+ | 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); | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <source lang="Java"> | ||
+ | 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) | ||
+ | </source> | ||
+ | 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 === | === 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 <code lang="Java">WebApplicationException</code> (from <code lang="Java">javax.ws.rs</code>) that in turn extends <code lang="Java">RuntimeException</code>. | ||
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. | 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. | ||
Line 109: | Line 219: | ||
* remove the need to declare exceptions that sometimes aren’t going to be handled anyway. | * remove the need to declare exceptions that sometimes aren’t going to be handled anyway. | ||
− | On the other hand, Code Exceptions require more coding than POJEs. | + | On the other hand, Code Exceptions require a bit of more coding than POJEs. |
− | === Declare your Error Codes === | + | ==== Declare your Error Codes ==== |
The first step is to declare an enumeration of the error codes and associated messages. The enum must extend the <code>ErrorCode</code> interface. | The first step is to declare an enumeration of the error codes and associated messages. The enum must extend the <code>ErrorCode</code> interface. | ||
Line 148: | Line 258: | ||
Different enums can be created for different resource methods or per REST resource. The granularity is up to the developer. | Different enums can be created for different resource methods or per REST resource. The granularity is up to the developer. | ||
− | === Throw | + | ==== Throw Error Codes ==== |
− | The following method | + | The following method fails to create a Context given certain parameters (not shown). |
<source lang="Java"> | <source lang="Java"> | ||
import javax.ws.rs.core.Response; | import javax.ws.rs.core.Response; | ||
− | + | import javax.ws.rs.core.Response.Status; | |
import org.gcube.resourcemanagement.manager.io.rs.RMCode; | import org.gcube.resourcemanagement.manager.io.rs.RMCode; | ||
import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; | import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse; | ||
Line 163: | Line 273: | ||
public Response create(...) { | public Response create(...) { | ||
− | // | + | //Error condition: we detect that the requested context already exists |
GXOutboundErrorResponse.throwErrorCode(RMCode.CONTEXT_ALREADY_EXISTS); | GXOutboundErrorResponse.throwErrorCode(RMCode.CONTEXT_ALREADY_EXISTS); | ||
− | |||
− | |||
} | } | ||
} | } | ||
</source> | </source> | ||
− | Do note that <code>GXOutboundErrorResponse</code> can be thrown by any object invoked within the create method without the need of declaring them (this is because they | + | Do note that <code>GXOutboundErrorResponse</code> can be thrown by any object invoked within the create method without the need of declaring them (this is because they use <code>RuntimeExceptions</code>). |
By default, a GXOutboundErrorResponse has the <code>NOT ACCEPTABLE(406)</code> status. It is possible to assign a different status to the response by using overloaded methods of the class as shown below. | By default, a GXOutboundErrorResponse has the <code>NOT ACCEPTABLE(406)</code> status. It is possible to assign a different status to the response by using overloaded methods of the class as shown below. | ||
Line 194: | Line 302: | ||
</source> | </source> | ||
− | === Local Code Exceptions === | + | ==== Local Code Exceptions ==== |
gxRest provides another type of exception called <code>LocalCodeException</code>. 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 <code>ErrorCode</code> used for the web exceptions can be used to create a local code exception. | gxRest provides another type of exception called <code>LocalCodeException</code>. 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 <code>ErrorCode</code> used for the web exceptions can be used to create a local code exception. | ||
Line 239: | Line 347: | ||
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). | 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. | ||
+ | |||
+ | <source lang="Java"> | ||
+ | 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."); | ||
+ | |||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | 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 = | ||
− | Inbound responses are the information received by a client following | + | 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: | + | They can be obtained as returned object from a [[GxRest/Requests|GXRequest]]: |
<source lang="Java"> | <source lang="Java"> | ||
import org.gcube.common.gxrest.response.inbound.GXInboundResponse; | import org.gcube.common.gxrest.response.inbound.GXInboundResponse; | ||
− | import org.gcube.common.gxrest.request. | + | import org.gcube.common.gxrest.request.GXHTTPRequest; |
− | GXInboundResponse response = | + | GXInboundResponse response = GXHTTPRequest.newRequest(...).post(); |
</source> | </source> | ||
− | ... or by wrapping a < | + | ... or by wrapping a <code> javax.ws.rs.core.Response</code> object: |
<source lang="Java"> | <source lang="Java"> | ||
Line 267: | Line 397: | ||
GXInboundResponse response = new GXInboundResponse(create); | GXInboundResponse response = new GXInboundResponse(create); | ||
</source> | </source> | ||
− | The code above makes usage | + | 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 <code>ws.Response</code>. |
+ | <source lang="Java"> | ||
+ | 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()); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | </source> | ||
+ | |||
+ | === 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: | ||
+ | |||
<source lang="Java"> | <source lang="Java"> | ||
− | + | 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(); | ||
+ | |||
+ | </source> | ||
+ | DELETE, PUT, GET requests would instead check that the HTTP OK (200) is returned: | ||
+ | |||
+ | <source lang="Java"> | ||
+ | import org.gcube.common.gxrest.response.inbound.GXInboundResponse; | ||
+ | |||
+ | GXInboundResponse response = //invoke delete method | ||
+ | |||
+ | if (response.hasOKCode()) { | ||
+ | logger.info("The web app says: " + response.getMessage()); | ||
+ | } | ||
+ | |||
+ | </source> | ||
+ | ==== Access to the Content ==== | ||
+ | The content of a response is similar to the returned value of a method or a function: the client must know what is the expected type of the 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 ==== | ||
+ | <source lang="Java"> | ||
+ | GXInboundResponse response = //request | ||
+ | String value = response.getStreamedContentAsString(); | ||
+ | </source> | ||
+ | |||
+ | ==== Content as Json serialization ==== | ||
+ | If the serialization does not contain processor-specific annotations (like Jackson annotations), the response can deserialize the Json content. | ||
+ | <source lang="Java"> | ||
+ | import org.gcube.informationsystem.model.entity.Context; | ||
+ | |||
+ | GXInboundResponse response = //request | ||
+ | Context returnedContext = response.tryConvertStreamedContentFromJson(Context.class); | ||
+ | </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 as byte array ==== | ||
+ | This method reads all the bytes in the input stream of the response and collects them in the returned array. | ||
+ | <source lang="Java"> | ||
+ | GXInboundResponse response = //request | ||
+ | byte[] bytes = response.getStreamedContent() | ||
+ | </source> | ||
+ | |||
+ | ==== Content from original Response ==== | ||
+ | Available only if GXWebtGXWebTargetAdapterRequest is used for the request. | ||
+ | <source lang="Java"> | ||
+ | import javax.ws.rs.core.Response; | ||
+ | |||
+ | GXInboundResponse response = GXWebTargetAdapterRequest.newRequest(..); | ||
+ | Response rsResponse = response.getSource(); | ||
+ | ExpectedType content = response.readEntity(...); | ||
+ | </source> | ||
+ | |||
+ | === 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: | ||
+ | <source lang="Java"> | ||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | At the client side, we can retrieve the exception from the response and then it can be thrown and dealt with upstream: | ||
+ | <source lang="Java"> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | The deal to support this use case is that the custom Exception class (<code>RMContextDoesNotExistException</code> 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: | ||
+ | <source lang="Java"> | ||
+ | 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); | ||
+ | |||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | On the client side, we can fetch and convert back the error code as follows: | ||
+ | |||
+ | <source lang="Java"> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | <code>CodeFinder</code> 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: | ||
+ | <source lang="Java"> | ||
+ | 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()) { | switch (code.getId()) { | ||
case 3: | case 3: | ||
− | //manage the error with code 3 (CONTEXT_ALREADY_EXISTS, see | + | //manage the error with code 3 (CONTEXT_ALREADY_EXISTS, see org.gcube.resourcemanagement.manager.io.rs.RMCreateContextCode) |
break; | break; | ||
case 4: | case 4: | ||
− | //manage the error with code 4 (CONTEXT_PARENT_DOES_NOT_EXIST, see | + | //manage the error with code 4 (CONTEXT_PARENT_DOES_NOT_EXIST, see org.gcube.resourcemanagement.manager.io.rs.RMCreateContextCode) |
break; | break; | ||
default: | default: | ||
Line 291: | Line 630: | ||
} | } | ||
</source> | </source> | ||
+ | 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: | ||
− | |||
<source lang="Java"> | <source lang="Java"> | ||
− | import org.gcube. | + | 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()); | |
− | + | ||
− | + | </source> | |
− | + | Dedicate methods are available to check if it holds commonly used codes: | |
− | + | <source lang="Java"> | |
− | + | 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 | ||
} | } | ||
</source> | </source> | ||
− |
Latest revision as of 14:17, 2 April 2019
There are two classes of responses in gxJRS. 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) gxJRS. 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. With only one exception, gxJRS responses are instances of javax.ws.rs.core.Response. Therefore, it is expected that resource (in the REST sense) methods declare to return a Response in their signature.
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; import javax.ws.rs.core.MediaType; @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(MediaType.APPLICATION_JSON) .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; import javax.ws.rs.core.MediaType; @Path("context") public class RMContext { @DELETE public Response delete(...) { //Resource successfully deleted return GXOutboundSuccessResponse.newOKResponse() .withMessage("Context successfully deleted.") .ofType(MediaType.APPLICATION_JSON) .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 javax.ws.rs.core.MediaType; 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(MediaType.APPLICATION_JSON).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.
POJEs outside a Response
In some cases (legacy code, Services not fully compliant with the REST approach), throwing a POJE across a JAX-RS call is useful even if the method does not return an instance of javax.ws.rs.core.Response.
JAX-RS api has introduced generic and pluggable interfaces called MessageBodyWriter (for doing the marshalling) and MessageBodyReader (for doing the unmarshalling) to support the conversion of a Java type to a stream. gxJRS implements these two interfaces to throw exceptions back to the client in resource methods that return String objects.
These are the two implementations:
org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextWriter org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextReader
At service side, the marshaller must be registered along with the other components of the JAX-RS application:
import javax.ws.rs.core.Application; import org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextWriter; @Path("app_path") public class MyWebApp extends Application { @Override public Set<Class<?>> getClasses() { final Set<Class<?>> classes = new HashSet<Class<?>>(); // register resources and features classes.add(MyResource1.class); classes.add(MyResource2.class); // register the marshaller classes.add(SerializableErrorEntityTextWriter.class); return classes; } }
At client side, the unmarshaller must be registered as custom JAX-RS component in the request:
import org.gcube.common.gxrest.request.GXWebTargetAdapterRequest; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.common.gxrest.response.entity.SerializableErrorEntityTextReader; GXWebTargetAdapterRequest requestAdapter = TargetFactory.stubFor(service).getAsGxRest(address); // register the unmarshaller requestAdapter.register(SerializableErrorEntityTextReader.class); // send the request and get the response back GXInboundResponse response = requestAdapter.put(...); //handle the response in the usual way
Besides the registration of the two JAX-RS components, throwing and handling the exception is the same as with the Response. See Responses with Exceptions.
POJEs 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 or a function: the client must know what is the expected type of the 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 as byte array
This method reads all the bytes in the input stream of the response and collects them in the returned array.
GXInboundResponse response = //request byte[] bytes = response.getStreamedContent()
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(...);
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 }