Creating a connection handler

Use the Handler interface to create a new connection handler.

For an overview of connection handlers and their configuration, see Configuring connection handlers.

Overview

The handler interface

Connection handler implementations must implement the zero.core.connection.handlers.Handler interface. This interface has two methods that must be implemented.

  • void onRequest()
  • void onResponse()

When a connection handler is configured for a destination, the onRequest() method is invoked during request processing and the onResponse() method is invoked during response processing.

For more information about destination configuration and an overview of destination processing, see Configuring destinations. See the API reference for zero.core for a description of the Handler interface.

The connection context

The onRequest() and onResponse() methods interact directly with the /connection zone of the GlobalContext. The contents of the outbound request can be accessed using the following keys:

/connection/request/target
Destination resource name.
/connection/request/operation
Operation name.
/connection/request/headers/ header_name
List of values for request header.
/connection/request/protocol/_name
Name of the protocol requested by the application.
/connection/request/protocol/ parameter_name
Protocol configuration value.
/connection/request/body
Request body.

The object type of the request body depends on the body supplied by the caller of the Connection API.

The contents of the response can be accessed using the following keys:

/connection/response/headers/ header_name
List of values for response header.
/connection/response/status
Response status String.
/connection/response/body
Response body.

The response body can be any object type, including InputStream and Reader.

Considerations when streaming

Caution should be taken if a connection handler needs to examine a request or response body in streaming scenarios. If the connection handler begins to read an InputStream or Reader body, then the read data is effectively removed from the body. If necessary, replace the request body with a buffer containing the contents of the consumed the InputStream or Reader, or provide a wrapper stream.

If a connection handler replaces a request or response body with an instance of InputStream or Reader, the implementation of the new body can reference the /connection global context zone. The connection infrastructure will ensure that the correct /connection is made available when the application reads the stream. However the implementation of any other object used to represent a response body must not reference the /connection global context zone.

Connection handlers do not support OutputStream processing. If the caller requested a Connection.getRequestBodyOutputStream() and a handler is configured, the request body is automatically collected in a buffer, and /connection/request/body references an InputStream from which the request body it can be read.

Reading handler configuration

If a handler requires configuration, it can be specified when a handler is registered with a connection destination, as described in Configuring connection handlers. When the Handler is invoked, the configuration is made available in the GlobalContext as a Map under the key /connection/configuration.

In the following example, the configuration associates the handler implementation class examples.handler.Map2JSONHandler with the destination, http://localhost:8080/target.groovy, and supplies some handler configuration. When the Handler is invoked, it locates the configuration in the global context as /connection/configuration#contentType:

/config/connection/destinations += {
    "http://localhost:8080/target.groovy" : {
        "handlers" : [{
            "class" : "examples.handler.SimpleMap2JSONHandler",
            "config" : {
                "contentType" : "text/json; charset=UTF-8"
            }
        }]
    }
}

For more information about destination configuration, see Configuring destinations.

Suppressing request processing

An onRequest() implementation can choose to prevent further request processing. If a request is suppressed in this way, no further onRequest() handlers will be executed and the request will not be sent to the target resource. Response phase processing will begin, starting with the preceding connection handler.

To suppress request processing, the onRequest() method sets any global context key starting with /connection/response/

If the onRequest() method sets any part of the response in the global context, it is deemed to have determined the complete response. For example, it may be necessary for the connection handler to set several values, such as /connection/response/body, /connection/response/status and values under /connection/response/headers, to simulate a full response for the applicable protocol.

Exception handling

Either onRequest() or onResponse() can signal an exception situation by placing an instance of Exception in /connection/response/body.

Implementations of onRequest() and onResponse() should be designed to use this mechanism to signal exceptions rather than throwing a RuntimeException. If an unexpected RuntimeException is thrown during onRequest() or onResponse() processing, it is caught by the connection infrastructure and placed in /connection/response/body on behalf of the Handler implementation.

If /connection/response/body contains an Exception when onRequest() or onResponse() returns, response processing is carried out starting with the preceding connection handler. If onRequest() sets and an Exception response, then any following connection handlers are not invoked and the request is not sent to the resource. The Exception contained in /connection/response/body can be examined or replaced by the onReponse() method of any previous connection handlers.

If /connection/response/body contains an Exception after the connection handlers complete the processing, it is eventually thrown to the application that is using the Connection API. The Exception is wrapped in an ConnectionException if it is not an instance of ConnectionException or IOException.

Example handler implementation

The following example demonstrates the principles involved in writing a connection handler, called SimpleMap2JSONHandler. An application using the Connection API works with request and response bodies as instances of Map, but the target resources is a REST service using JSON syntax. During onRequest() the connection handler converts the request Map sent by the application to a JSON request for the target service. In onResponse(), the JSON response from the service is decoded into a Map to be delivered to the application.

The following example code contains the implementation of SimpleMap2JSONHandler.onRequest():

public void onRequest() {
    try {
        // Examine request body
        Object body = GlobalContext.zget(GlobalContextURIs.Connection.Request.body, null);
        if (body instanceof Map) {

            // Replace Map body with JSON encoded string
            String jsonString = Json.encode(body);
            GlobalContext.zput(GlobalContextURIs.Connection.Request.body, jsonString);
                        
            // Set Content-Type header
            String contentType = GlobalContext.zget(GlobalContextURIs.Connection.configuration + "#contentType");
            GlobalContext.zput(GlobalContextURIs.Connection.Request.headers + "/Content-Type",
                ListFactory.create(contentType)); 
                        
        }                
    } catch (Exception e) {
        GlobalContext.zput(GlobalContextURIs.Connection.Response.body, e);
    }
}

The following list summarizes the function of the onRequest() method:

  1. The request body, if any, /connection/request/body key is read from the GlobalContext.
  2. If the request body is an instance of Map:
    • /connection/request/body is replaced with a String representation of the Map using JSON syntax.
    • /connection/request/headers/Content-Type is set, using the contentType configuration property supplied to the Handler.
  3. If an exception occurs, an Exception is placed in /connection/response/body.

The following example code contains the implementation of SimpleMap2JSONHandler.onResponse():

public void onResponse() {
    try {
        // Look for content type header
        List<Object> contentTypeHdr = (List<Object>) 
            GlobalContext.zget(GlobalContextURIs.Connection.Response.headers + "/Content-Type", null);
        String contentType = GlobalContext.zget(GlobalContextURIs.Connection.configuration + "#contentType");
        if (contentTypeHdr != null && contentTypeHdr.contains(contentType)) {

            Object body = GlobalContext.zget(GlobalContextURIs.Connection.Response.body, null);
            String jsonString = DataUtilities.convertToString(body, "UTF8");
            Object decoded = Json.decode(jsonString);
                        
            GlobalContext.zput(GlobalContextURIs.Connection.Response.body, decoded);
                        
        }
                
    } catch (Exception e) {
        GlobalContext.zput(GlobalContextURIs.Connection.Response.body, e);
    }
}

The following list summarizes the function of the onResponse() method:

  1. /connection/response/headers/Content-Type is examined and compared to the value of the contentType configuration property supplied to the Handler.
  2. If the content type matches, /connection/response/body is replaced with a decoded form of the JSON representation using a utility method in zero.core.connection.utils.DataUtilities to obtain a String value.
  3. If an exception occurs, an Exception is placed in /connection/response/body.

The following example is the remainder of the SimpleMap2JSONHandler:

package examples.handler;

import java.util.List;
import java.util.Map;
import zero.core.context.GlobalContext;
import zero.core.context.GlobalContextURIs;
import zero.core.connection.handlers.Handler;
import zero.core.connection.utils.DataUtilities;
import zero.json.Json;
import zero.util.ListFactory;

public class SimpleMap2JSONHandler implements Handler {
        
    // onRequest() and onResponse() go here
        
}

To use the simple connection handler, configure the handler for a destination as shown in following example:

/config/connection/destinations += {
    "http://localhost:8080/target.groovy" : {
        "handlers" : [{
            "class" : "examples.handler.SimpleMap2JSONHandler",
        "config" : {
            "contentType" : "text/json; charset=UTF-8"
            }
        }]
    }
}

The SimpleMap2JSONHandler example is intended to illustrate the principles of writing a Handler implementation, and might not be suitable for deployment in this form. In particular, it relies on the "Content-Type" header having the exact value specified by the "contentType" configuration property and is case sensitive. It cannot cope with responses containing "CONTENT-TYPE" or "content-type" headers.

Version 1.1.0.0.21442