Creating a custom protocol transport
This topic describes how to write a custom protocol implementation which can be used with the Connection API.
Extending connectivity
The connection infrastructure includes a variety of protocol implementations but there may be scenarios in which additional forms of connectivity are required. There are two alternative approaches that may be used to extend connectivity:
- Write a simple
transmithandler for use with the Event protocol. - Implement and registering a new protocol implementation using the
Transportinterface.
The first approach is designed to simplify the custom implementation of an individual connection destination but the
Transport interface should be used for implementing reusable protocol implementations. The advantages of writing a new
Transport implementation include:
- Implementing a new protocol which can be named and configured under
/config/connection/destinations. - Automatic selection of the
Transportwhen the name of the protocol is a valid URL scheme name. - Support for non-blocking protocol implementations in which the request and response phases are carried out at different times.
- The ability to replace the default implementation of one of the supplied protocols, such as FTP or SMTP.
This article outlines how to implement a simple Transport and register it with the
connection infrastructure so it may be invoked using the Connection API.
See the API reference for zero.core
for more information.
The Transport interface
Custom protocol implementations must implement the zero.core.connection.transports.Transport interface. This interface has
three methods which must be implemented.
-
void connect() -
void respond() -
void discard()
In addition, a Transport will normally need to provide a public no-argument constructor in order to allow the connection
infrastructure to create a new instance for each request.
The logic required in each method will depend on the protocol you are implementing and the way you choose to implement it. There are three basic approaches to writing a Transport:
- Simple blocking transport
- A simple blocking transport carries out all the activities necessary to send the request and open the response in its
connect()method. The implementation ofrespond()in a simple blocking transport will normally be an empty method. - Non-blocking transport
- A non-blocking transport separates the activities of sending the request and opening the response.
The
connect()method opens connection and completes the activities required to send the request but does not wait for the response. Therespond()method waits for the response and opens it for the application. - OutputStream transport
- An
OutputStreamtransport is an advanced form of a non-blocking transport that can also provide the application with anOutputStreaminto which the request body can be written without local buffering.
The discard() method can be used in advanced scenarios to allow a Transport to release resources it may have allocated. Many Transport implementations will have an empty discard() method.
The connection context
The connect() and respond() methods interact directly with the /connection zone of the
GlobalContext. Under normal operation, connect() will read the contents of the /connection/request/
keys to send the request and either connect() or respond() will populate the /connection/response
keys with the response.
Details of the outbound request can be found using the following keys:
-
/connection/request/target - Destination resource name.
-
/connection/request/operation - Operation name.
-
/connection/request/headers/header_name -
Listof 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 will depend on the body supplied by the caller of the Connection API. Note that if
the caller requested a Connection.getRequestBodyOutputStream() and the Transport does not support
OutputStream processing, then /connection/request/body will reference an InputStream from which
the request body can be read.
Details of the response can be passed back to the application using the following keys:
-
/connection/response/headers/header_name -
Listof values for response header. -
/connection/response/status - Response status
String. -
/connection/response/body - Response body.
The response body may be any object type, including InputStream and Reader. It is good practice to include
a Content-Type response header with a charset value where known, to allow the Connection API to
convert between character and binary data as the application requests.
Implementations of the discard() method must not reference the GlobalContext.
Reading protocol configuration
Protocol configuration can be applied to the connection request using various mechanism, as described in
Configuring protocols. By the time the Transport implementation is invoked,
the protocol configuration will have been made available under keys beginning with /connection/request/protocol/.
For example, consider the following destination configuration:
/config/connection/destinations += {
"testDestination" : {
"connection" : {
"protocol" : "testProtocol",
"config" : {
"prop1" : "value1",
"prop2" : "value2",
"map1" : {
"prop3" : "value3",
"prop4" : "value4"
}
}
}
}
}
This configuration associates the protocol called "testProtocol" with the destination, "testDestination", and supplies some protocol
configuration. When the Transport is invoked, it will be able to find this configuration in the GlobalContext as follows:
/connection/request/protocol/_name = testProtocol /connection/request/protocol/prop1 = value1 /connection/request/protocol/prop2 = value2 /connection/request/protocol/map1/prop3 = value3 /connection/request/protocol/map1/prop4 = value4
For more information about destination configuration, see Configuring destinations.
Exception handling
Either connect() or respond() can signal an exception situation by placing an instance of
Exception in /connection/response/body. The Exception contained in
/connection/response/body can then be examined or replaced any configured connection handlers.
Implementations of connect() and respond() should be designed to use this mechanism to signal exceptions
rather than throwing a RuntimeException. However, if an unexpected RuntimeException is thrown during
connect() or respond() processing, it will be caught by the connection infrastructure and placed in
/connection/response/body on behalf of the Transport implementation.
If /connection/response/body contains an Exception after the connection handlers have completed their
processing, it will eventually be thrown to the application when it uses the Connection API. The Exception will be
wrapped in an ConnectionException if it is not an instance of ConnectionException or IOException.
Discarding resources
Implementations of the Transport interface must not assume that the respond() method will always be called.
For example, the respond() method will not be called if the connect() method signals an exception condition or if
the application does not attempt to read the response.
In some cases, it may be necessary for a Transport implementation to allocate a resource during connect()
processing and maintain a instance variable reference to it for use in respond(). The implementation should release such
resources before the completion of respond() or before the completion of connect() if connect() sets
an Exception.
Even though it cannot be guaranteed that the application will examine the response and so cause respond() to be called,
a Transport implementation can ensure any resources are released through the implementation of the discard() method.
The discard() method will called after application request processing has completed, regardless of whether or not
respond() was previously called. The discard() must not access the GlobalContext.
Example transport implementations
Writing a simple blocking transport
To demonstrate how to write a simple blocking transport, consider a protocol that might be used to access a simple in-memory stack.
In this section we write a Transport that implements two operations, GET and PUT, and that interacts
with the request and response body elements.
The example StackTransport class implements the Transport interface and uses a static variable
Stack myStack to implement the in-memory stack. The first method of the Transport interface that we must implement
is connect():
public void connect() {
try {
switch (Connection.toOperation(GlobalContext.zget(GlobalContextURIs.Connection.Request.operation))) {
case GET:
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, myStack.pop());
break;
case PUT:
myStack.push(GlobalContext.zget(GlobalContextURIs.Connection.Request.body));
break;
default:
throw new Exception("Unsupported operation for StackTransport");
}
} catch (Exception e) {
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, e);
}
}
The following is a summary of what this connect() method is doing:
- The
/connection/request/operationkey is read from theGlobalContextand validated. TheConnection.toOperation()method is used for convenience; to convert the operation name to a validConnection.Operationvalue. - The appropriate action for the current operation is performed:
- For
GET, a value is popped frommyStackand placed in/connection/response/body. - For
PUT, the value of/connection/request/bodyis pushed ontomyStack.
- For
- In any exceptional situation arises, an
Exceptionis placed in/connection/response/body.
Being a simple blocking transport, StackTransport carries out all of its request and response processing in
the connect() method, so the respond() method can be an empty implementation.
public void respond() {
// Nothing to do as all processing is carried out in connect()
}
Here is the remainder of the StackTranport implementation:
package examples.transport;
import java.util.*;
import zero.core.context.GlobalContext;
import zero.core.context.GlobalContextURIs;
import zero.core.connection.Connection;
import zero.core.connection.transports.Transport;
public class StackTransport implements Transport {
private static Stack<Object> myStack = new Stack<Object>();
// connect() and respond() implementations go here
public void discard() {
// Nothing to do
}
}
This completes the implementation of our simplistic StackTransport example but before it can be used with the
Connection API, it must be registered as a protocol transport with the connection infrastructure.
Writing a non-blocking transport
When writing a non-blocking transport, we must separate the operation of the protocol into connection and response phases and implement
the logic for each in the connect() and respond() method.
To demonstrate these concerns we will consider a simple transport that can either read data from, or write data to, a network socket.
The implementation of SimpleSocketTransport needs to:
- Open a
Socketin theconnect()method and if the operation isPOSTsend the request body using it. - If the operation is
GET, obtain the responseInputStreamfrom the socket in therespond()method. - Close the
Socketwhen the application has finished with it, in thediscard()method.
Here is the implementation of connect():
public void connect() {
try {
// Use request target as hostname and get port from protocol configuration
String hostname = GlobalContext.zget(GlobalContextURIs.Connection.Request.target);
int port = GlobalContext.zget(GlobalContextURIs.Connection.Request.Protocol._self + "/port", 8081);
// Get request operation
operation = Connection.toOperation(GlobalContext.zget(GlobalContextURIs.Connection.Request.operation));
switch (operation) {
case GET:
// Establish connection to server but dont start reading stream
socket = new Socket(hostname, port);
break;
case POST:
// Establish connection and open output stream
socket = new Socket(hostname, port);
outputStream = socket.getOutputStream();
// Write request body to output stream
Object body = GlobalContext.zget(GlobalContextURIs.Connection.Request.body, null);
DataUtilities.writeToOutputStream(outputStream, body, null);
outputStream.close();
break;
default:
throw new Exception("Unsupported operation for SimpleSocketTransport");
}
} catch (Exception e) {
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, e);
}
}
There is a summary of what this connect() method is doing:
- The hort name is read from the
/connection/request/targetkey in theGlobalContextand the port number is read from a protocol configuration parameter,port. - The operation is read from
/connection/request/operation:- For the
GEToperation, a socket connection is established to the server but no data is read or sent. - For the
POSToperation, a socket connection is established and the data specified in/connection/request/bodyis written to theOutputStream(using a utility method inzero.core.connection.utils.DataUtilities).
- For the
- In any exceptional situations arise, an
Exceptionis placed in/connection/response/body. If this happens, then the connection infrastructure will not call the correspondingrespond()method.
Here is the corresponding respond() method:
public void respond() {
try {
switch (operation) {
case GET:
// Open response body stream
inputStream = socket.getInputStream();
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, inputStream);
break;
case POST:
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, null);
break;
}
} catch (Exception e) {
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, e);
}
}
For the GET operation, the respond() method opens the Socket input stream and puts it
in /connection/response/body.
The following example contains the remainder of the SimpleSocketTransport implementation.
The discard() method contains code to close the stream and socket resources, if they are still open.
package examples.transport;
import java.io.*;
import java.net.*;
import zero.core.connection.Connection;
import zero.core.connection.transports.Transport;
import zero.core.connection.utils.DataUtilities;
import zero.core.context.GlobalContext;
import zero.core.context.GlobalContextURIs;
public class SimpleSocketTransport implements Transport {
Connection.Operation operation;
Socket socket;
OutputStream outputStream;
InputStream inputStream;
// connect() and respond() implementations go here
public void discard() {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
// Log exception if required
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Log exception if required
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// Log exception if required
}
}
}
}
Once again, the new Transport implementation must be registered as a protocol transport before it can be used with
the Connection API.
It should be remembered that the SimpleSocketTransport example is intended to illustrate the principles of writing a
non-blocking Transport implementation and may not be suitable for deployment in this form. In particular, it is worth noting
the following limitations:
- There is no consideration for conversion between binary and character data.
POSTassumes the system encoding when it converts from character data (usingDataUtilities.writeToOutputStream()) andGETdoes not annotate the response with any encoding information for theConnectionAPI. One solution to this problem would be to make use of aContent-Typeheader for thePOSTandGEToperations in a similar manner to the File protocol. - The implementation uses instance variables to pass the
operationandurlConnectionvalues betweenconnect()andrespond(). This is safe provided theTransportis registered in a way that ensures a new instance is used for each request.
Writing a non-blocking OutputStream transport
A non-blocking protocol implementation can optionally provide the application with an instance of OutputStream in
which the request body should be written. If the underlying protocol implementation uses an OutputStream you should consider
adding this support to allow an application using Connection.getRequestOutputStream() to obtain access to the stream without
buffering.
If a Transport does not provide direct OutputStream for the request body, it does not
prevent an application from calling Connection.getRequestBodyOutputStream(). In this case, the connection infrastructure will present the application with an OutputStream buffer and wait for the application to call close(). Once the buffer
is ready the Transport will be invoked and the request body supplied to it as an InputStream.
The SimpleSocketTransport example can be modified to provide OutputStream by making some simple changes to
the connect() method:
@zero.core.connection.transports.TransportProperties(supportsRequestOutputStream=true)
public void connect() {
try {
// Use request target as hostname and get port from protocol configuration
String hostname = GlobalContext.zget(GlobalContextURIs.Connection.Request.target);
int port = GlobalContext.zget(GlobalContextURIs.Connection.Request.Protocol._self + "/port", 8081);
// Get request operation
operation = Connection.toOperation(GlobalContext.zget(GlobalContextURIs.Connection.Request.operation));
switch (operation) {
case GET:
// Establish connection to server but dont start reading stream
socket = new Socket(hostname, port);
break;
case POST:
// Establish connection and open output stream
socket = new Socket(hostname, port);
outputStream = socket.getOutputStream();
// Examine request body
Object body = GlobalContext.zget(GlobalContextURIs.Connection.Request.body, null);
if (body == zero.core.connection.context.ConnectionContextConstants.OUTPUTSTREAM) {
// Set request output stream
GlobalContext.zput(GlobalContextURIs.Connection.Request.outputStream, outputStream);
} else {
// Write request body to output stream
DataUtilities.writeToOutputStream(outputStream, body, null);
outputStream.close();
}
break;
default:
throw new Exception("Unsupported operation for SimpleSocketTransport");
}
} catch (Exception e) {
GlobalContext.zput(GlobalContextURIs.Connection.Response.body, e);
}
}
There are two small but important changes to note:
- The
connect()method is now annotated with the annotation,TransportProperties(supportsRequestOutputStream=true). - The value of the
/connection/request/bodyis compared withConnectionContextConstants.OUTPUTSTREAMusing the equality (==) operator and,- if equal, the
OutputStreamis placed in/connection/request/outputStreamandconnect()completes leaving the stream open. - if not equal, the original non-blocking behavior is used; the request body is written to the
OutputStream, which is then closed.
- if equal, the
The TransportProperties(supportsRequestOutputStream=true) annotation is used to declare that the
Transport is able to provide an OutputStream for the request body. If this annotation is not included,
the /connection/request/body value will never be ConnectionContextConstants.OUTPUTSTREAM.
The connect() method is normally responsible for completing the work to send the request to the target. However,
when connect() provides an OutputStream, this responsibility is deferred to the close() method of
the OutputStream.
Registering a protocol Transport implementation
A new Transport implementation must be registered with the connection infrastructure
before an application can make requests using the Connection API.
After the protocol to be used the request has been selected,
the connection infrastructure fires a getTransport event in order to discover a suitable Transport implementation.
By providing a handler for the this event, the developer can determine the Transport implementation that should be used for
requests that match a set of criteria.
When the getTransport event is fired, the GlobalContext contains the following information that may be used as
conditions when registering the event hander.
-
/event/target -
/event/operation -
/event/protocol
For more information about how protocols are configured and selected, see Configuring protocols.
Using GenericGetTransportHandler
The simplest way to provide a getTransport event handler is to use the supplied GenericGetTransportHandler
implementation. This allows the Transport class to be named directly in zero.config as part of the handler
configuration. The name of the selected implementation class is specified as a transport value in the event
instanceData.
For example, to register SimpleSocketTransport for use with all requests for the simpleSocket protocol, you can use
the following configuration:
/config/handlers += [{
"events" : "getTransport",
"handler" : "zero.core.connection.transports.GenericGetTransportHandler.class",
"conditions" : "/event/protocol =~ simpleSocket",
"instanceData" : {"transport" : "examples.transport.SimpleSocketTransport"}
}]
Assuming the above configuration, the following Groovy extract shows how
an application could use the SimpleSocketTransport to open port 1234 on localhost
and write some data:
Connection conn = new Connection("localhost", Connection.Operation.POST);
conn.setProtocolConfiguration("simpleSocket", [ port: 1234 ]);
conn.setRequestBody("Some data to write to the socket");
conn.getResponse();
Using GenericGetTransportHandler implies that a new instance of the Transport will be automatically created
for each connect() invocation, using the public no-argument constructor.
Writing a custom handler for getTransport
If the GenericGetTransportHandler does not offer sufficient flexibility you can write your getTransport
handler. For example, you may want to further validate the /connection/request/ context before selecting the transport or
select a specific Transport instance rather than allowing the connection infrastructure to create one using a public
no-argument constructor.
If an onGetTransport() implementation wishes to determine the transport implementation to be used, it must set the
/event/transport key. The value set must be either:
- a
Classwhich implements theTransportinterface and which has a public, no-argument constructor, or - an instance of an object which implements the
Transportinterface.
In the former case, the connection infrastructure will create a new instance of the Class to handle the request. In the
latter case, the Transport instance set in /event/transport will be used.
If multiple getTransport handlers are eligible for a given request, the first to set /event/transport
will prevail. If /event/transport is not set by any handlers then the connection infrastructure will look for a default
protocol implementation and return a ConnectionException if none is available.
Registering a new URL protocol
Registering a new Transport implementation using the getTransport event
allows applications to make requests using the Connection API but the connection
infrastructure still needs to determine the protocol associated with a request before it can
find the transport.
In many cases, such as the event and jms protocols,
the protocol must be explicitly identified using Connection.setProtocolConfiguration()
or a destination configuration.
Howevever, in cases where the target resource name contains a URL for a known protocol,
such as the http or file protocols, the connection infrastructure is able
use the target resource name to select the protocol without explicit configuration.
For more information about how protocols are configured and selected, see Configuring protocols.
If a Transport implementation accepts a target resource name in the form
of a URL, the associated protocol can be also registered with the connection infrastructure.
A registered protocol allows allow automatic protocol selection from the target resource URLs
and the use of normalized names for connection destination matching.
For more information about destination configuration and selection, see Configuring destinations.
Protocol registrations are made under the global context key
/config/connection/protocols/
protocolName.
The following configuration extract contains example protocol registrations
for the http and file protocols:
/config/connection/protocols/http = { "defaultPort" : 80 }
/config/connection/protocols/file = { }
The optional defaultPort property identifies the default port number
to be used in normalized names when matching a request against the destination configurations.
The default value is -1, which indicates that the protocol has no
default port.