Difference between revisions of "GxRest/GxJRS/Responses"

From Gcube Wiki
Jump to: navigation, search
(HTTP Error Statuses)
(HTTP Error Statuses)
Line 256: Line 256:
  
 
=== HTTP Error Statuses ===
 
=== HTTP Error Statuses ===
GXOutboundErrorResponse also supports returning just an HTTP error status (>= 400).
+
GXOutboundErrorResponse also supports returning just an HTTP error status (>= 400) with an associated message.
  
 
<source lang="Java">
 
<source lang="Java">

Revision as of 03:18, 17 February 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.

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. POJE brings this behavior into web applications.

Let's revise the create method previously shown to demonstrate how to send to a client an Exception (MyException 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;
 
@Path("context")
public class RMContext {
 
  @POST
  public Response create(...) {
 
    //Oh no, something failed here
    GXOutboundErrorResponse.throwException(new MyException("Context was not created."));
 
 
    //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();
 
  }
}

POJE with stacktrace (to be implemented)

POJEs arev rebuilt at client side with the Java reflection API. Because of that, the class of the Exception is the same (and this is good enough for a try-catch statement or re-thrown the Exception upstream), but the stacktrace is not. To keep (at least) part of the original stacktrace, we can use the following method of GXOutboundErrorResponse:

import org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse;
 
    //Oh no, something failed here
    GXOutboundErrorResponse.throwExceptionWithStacktrace(new MyException("Context was not created."),5);

The second paramente (an int) allows to specify how many lines 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).

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 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 org.gcube.common.gxrest.response.outbound.GXOutboundErrorResponse;
 
@Path("context")
public class RMContext {
 
  @POST
  public Response create(...) {
 
    // ops, the operation is not permitted
    GXOutboundErrorResponse.throwErrorStatus(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.GXRequest;
 
GXInboundResponse response = GXRequest.newRequest(...).submit();

... 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 of the Jersey testing framework to invoke the create method.

Success Responses

Once the client gets the response object, it can checks if it is a success 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 create method
 
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());
}

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 that 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 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, the 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:

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;
}

Just keep in mind that this code IS NOT the HTTP status that can be eventually also retrieved from the response as follows:

  response.getHTTPStatus();