GxRest/GxJRS/Responses

From Gcube Wiki
Revision as of 14:17, 2 April 2019 by Manuele.simi (Talk | contribs) (POJEs outside a Response)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

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.

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
}