Apiman Gateway Developer’s Implementation Guide
At the heart of any Apiman gateway implementation is the flexible, lightweight apiman-core.
The core serves to execute policies upon the traffic passing through it, determining whether a given conversation should continue or not.
A set of simple, asynchronous interfaces are provided which an implementor should fulfill using the platform’s native functionality to allow apiman to interact with its various components and services.
Implementing IApimanBuffer
Before you can send any data through Apiman, you must implement the IApimanBuffer
interface. It provides a set of methods which allow Apiman to access your platform’s native buffer format as effectively as possible.
Any data you pass into Apian must be wrapped in your implementation of IApimanBuffer
, whilst any data returned to you by Apiman will be an IApimanBuffer
from which you can extricate your native buffer.
Implementation is fairy self-explanatory, but a few points are worth noting:
public class YourApimanBufferImpl implements IApimanBuffer {
private YourNativeBuffer nativeBuffer;
public VertxApimanBuffer(YourNativeBuffer nativeBuffer) {
this.nativeBuffer = nativeBuffer;
}
// This is your mechanism to efficiently yank your native buffer back
@Override
public Object getNativeBuffer() {
return nativeBuffer;
}
@Override
public int length() {
return nativeBuffer.length();
}
@Override
public void insert(int index, IApimanBuffer buffer) {
nativeBuffer.setBuffer(index, (Buffer) buffer.getNativeBuffer());
}
// <...>
}
Implementors of IApimanBuffer should ensure that the native format is preserved within the instance, this allows it to be retrieved again using getNativeBuffer . Any mutation should be on the native buffer.
|
Executing apiman-core
Let’s consider the following snippet:
IEngine engine = new <your engine>.createEngine();
// Request executor, through which we can send chunks and indicate end.
final IApiRequestExecutor requestExecutor = engine.executor(request,
new IAsyncResultHandler<IEngineResult>() {
public void handle(IAsyncResult<IEngineResult> result) { ... }
});
// streamHandler called when back-end connector is ready to receive data.
requestExecutor.streamHandler(new IAsyncHandler<IApiConnection>() {
public void handle(final IApiConnection writeStream) { ... }
});
// Execute the request
executor.execute();
After instantiating your engine implementation, you can call execute
.
This is the main point through which you pipe data into and out of Apiman.
In order to avoid any buffering you must write body data through streamHandler’s `IApiConnection
which will be called when the connection to the backend API is ready to receive.
The result is provided to executor’s `IAsyncResultHandler
, which can be evaluated to determine the result of the call, and, if successful, retrieve a ApiResponse
and attach handlers to receive response data.
Streaming data
Exploring streamHandler
further:
requestExecutor.streamHandler(new IAsyncHandler<IApiConnection>() {
@Override
public void handle(final IApiConnection writeStream) {
// Just for illustrative purposes
IApimanBuffer apimanBuffer =
new YourApimanBufferImpl(nativeBuffer);
// Call #write as many times as desired.
writeStream.write(apimanBuffer);
// Call #end only once.
writeStream.end();
}
});
Any data flowing into the executor must first be wrapped in your implementation of IApimanBuffer
before being passed to write
.
You may call write
an unlimited number of times, and indicate that transmission has completed by signalling end
.
No further calls to write should occur after end has been called.
|
Handling results
An excerpt of the executor’s result handler and considering a successful result:
engine.executor(request, new IAsyncResultHandler<IEngineResult>() {
public void handle(IAsyncResult<IEngineResult> result) {
// Did an exception occur?
if (result.isSuccess()) {
IEngineResult engineResult = result.getResult();
if (engineResult.isResponse()) {
// Our successfully returned API response.
ApiResponse response = engineResult.getApiResponse();
// Set a bodyHandler to receive the response's body chunks.
engineResult.bodyHandler(new IAsyncHandler<IApimanBuffer>() {
@Override
public void handle(IApimanBuffer chunk) {
// Important: for efficiency, retrieve native buffer format directly if possible.
if(chunk.getNativeBuffer() instanceof YourNativeBuffer) {
YourNativeBuffer buffer = (YourNativeBuffer) chunk.getNativeBuffer();
}
}
});
// Set an endHandler to receive the end signal.
engineResult.endHandler(new IAsyncHandler<Void>() {
@Override
public void handle(Void flag) {
// Transmission has now completed.
}
});
} else {
// Handle policy failure.
}
} else {
// Handle exception.
}
}
});
After testing IAsyncResult.isSuccess
, we can be certain that the request completed without an exception occurring.
Next, we verify IEngineResult.isFailure
, which indicates whether there was a policy failure or the response returned successfully.
Upon success the ApiResponse
can be extracted, and a bodyHandler
and endHandler
can be attached in order to receive the response’s associated data as it arrives.
At this point the data has exited Apiman, and can handled as makes sense for your implementation. For instance, you may wish to translate the ApiResponse
into its native equivalent and return it to the requestor.
|
Handling Failures
In the case of errors or policy failures, a variety of information is provided which can be used to construct a sensible response:
if (result.isSuccess()) {
IEngineResult engineResult = result.getResult();
if (!engineResult.isFailure()) {
<...>
} else {
PolicyFailure policyFailure = engineResult.getPolicyFailure();
log.info("Failure type: " + policyFailure.getType());
log.info("Failure code: " + policyFailure.getFailureCode());
log.info("Failure Message: " + policyFailure.getMessage());
log.info("Failure Headers: " + policyFailure.getHeaders());
}
} else {
Throwable throwable = engineResult.getError();
log.error("Something bad happened: " + throwable);
}
The appropriate response to failures will vary widely depending upon implementation.
For instance, a RESTful platform may wish to transmit an appropriate HTTP error code, message and possibly body.
Creating an API Connector
Connectors enable Apiman to transmit and receive data from the backend APIs under management. For instance, should your system need to connect to an HTTP API, an HTTP connector must be created.
The following samples illustrate in general terms how an implementor may go about creating a connector, and although the specifics will vary extremely widely depending upon the platform some general principles should be obeyed.
Connector basics
Inside your IConnectorFactory
implementation you must return an IApiConnector
corresponding to the type of request and API being interacted with:
public class ConnectorFactory implements IConnectorFactory {
public IApiConnector createConnector(ApiRequest request, Api api) {
return new IApiConnector() {
// ...
}
}
}
Inspecting the IApiConnector
more closely, we can see the key interface of a connector:
public IApiConnection request(ApiRequest request,
IAsyncResultHandler<IApiConnectionResponse> resultHandler) {
// ...
}
}
The IApiConnection
you must return is used by Apiman to write request chunks; hence, it will be read by your connector.
Conversely, the IApiConnectionResponse
handler must be called in order to send the ApiResponse
and its associated data chunks back to Apiman once a response has returned from the API; hence, you will write data to it.
The IAsyncResultHandler
is also used to indicate whether an exception has occurred during the conversation with the backend.
Creating the IApiConnection
Generally, an implementor must attempt to return their IApiConnection
as soon as it is valid for Apiman to write data to the backend.
Until you respond, Apiman will not fire IApiRequestExecutor.streamHandler
, and hence no data will arrive prematurely to your connector.
Following this guideline should help to minimise or eliminate any buffering requirements in your connectors.
Looking at an example:
// Native platform's connector (e.g. HTTP)
ImaginaryBackendConnector imaginaryConnector = ...; (1)
Connection c = imaginaryConnector.establishConnection(api.getEndpoint(), ...);
// Prepare in advance to do something sensible with the response
// See next section for more detail.
c.responseHandler(<Handle the response; return an IApiConnectionResponse>);
// From our perspective IApiConnection is
// *inbound data* (i.e. the user writes to us).
return new IApiConnection() {
boolean finished = false;
@Override
public void write(IApimanBuffer chunk) {
// Handle arriving data chunk
YourNativeBuffer nativeBuffer =
(YourNativeBuffer) chunk.getNativeBuffer();
imaginaryConnector.write(nativeBuffer);
}
@Override
public void end() {
// Handle the signal to indicate stream has completed
imaginaryConnector.finish_connection();
finished = true;
}
@Override
public void abort() {
// Handle immediate abort, for instance by closing your connection.
imaginaryConnector.abort();
finished = true;
}
@Override
public boolean isFinished() {
return finished;
}
};
1 | Your platform’s backend connector. |
imaginaryConnector
represents your platform’s backend connector. After establishing a connection that can accept data, you should return an IApiConnection
, allowing data to be written to your connector.
You can extract your native buffer format using getNativeBuffer
, plus a cast.
Although we haven’t yet explored how to handle a response, we can imagine that the platform’s ImaginaryBackendConnector
would allow us to set a responseHandler
, which will be fired when a response has arrived; this is the point at which we can build an IApiConnectionResponse
.
Creating the IApiConnectionResponse
Handling a successful response
Apiman’s resultHandler
should be called with an IApiConnectionResponse
when your connector has received a response from the API.
Let’s imagine that responseHandler
is called when the platform’s response has arrived, and looks like this:
c.responseHandler(new Handler<ImaginaryResponse> {
public void handle(ImaginaryResponse response) {
...
}
});
This is where we must build our Apiman response, using the data returned to the platform’s response, and attaching appropriate handlers to capture any data that arrives.
In the following example, we expand the response handle
method to build an IApiConnectionResponse
:
void handle(final ImaginaryResponse response) {
IApiConnectionResponse readStream = new IApiConnectionResponse() {
IAsyncHandler<IApimanBuffer> bodyHandler;
IAsyncHandler<IApimanBuffer> endHandler;
boolean finished = false;
ApiResponse response = YourResponseBuilder.build(response);
public IApiConnectionResponse() {
doConnection();
}
private void doConnection() {
// We stop any data arriving
response.pause();
// This will be called when we resume transmission
response.bodyHandler(new Handler<NativeDataChunk>() {
void handle(NativeDataChunk chunk) {
IApimanBuffer apimanBuffer =
new YourApimanBufferImpl(nativeBuffer);
bodyHandler.handle(apimanBuffer);
}
});
// Transmission has finished
response.endHandler(new Handler<Void>() {
void handle(Void flag) {
endHandler.handle((Void) null);
// You may want to close your backend connection here.
}
});
}
@Override
public void bodyHandler(IAsyncHandler<IApimanBuffer> bodyHandler) {
this.bodyHandler = bodyHandler;
}
@Override
public void endHandler(IAsyncHandler<Void> endHandler) {
this.endHandler = endHandler;
}
@Override
public ApiResponse getHead() {
return apiResponse;
}
@Override
public boolean isFinished() {
return finished;
}
@Override
public void abort() {
// Abort
}
// We explicitly resume transmission
@Override
public void transmit() {
response.resume();
}
};
// We're ready to transmit the response, let Apiman know.
IAsyncResult result = AsyncResultImpl.
<IApiConnectionResponse> create(readStream);
resultHandler.handle(result);
}
We imagine that our response
object contains what we need to build a ApiResponse
, and that handlers can be attached in order to retrieve body data and an end signal.
It can be paused using pause
, which prevents any data from arriving until resume
is called.
Importantly, data transmission must not begin until transmit
has been called, otherwise the appropriate handlers may not yet have been set, and data will be liable to disappear.
Hence, in this example, resume
is called in transmit
where we are certain that it’s safe to send data.
After end
has been signalled, clean up on the native connection can be performed, such as closing it.
In this example we assume the connection is closed for us.
Once we are sure our stream is ready, we pass it to Apiman using resultHandler.handle
wrapped inside an IAsyncResult indicating we were successful.
Some helpful create
methods are available in AsyncResultImpl
.
Whilst a given platform’s implementation may look very different, implementors must be careful to preserve the same external behaviour; some platforms may require buffering of data if pause-like functionality is not available.
In many cases it may be possible to implement IApiConnectionResponse
and IApiConnection
in the same class.
Do not transmit any response data into Apiman until transmit has been signalled.
|
Handling an error
If an error occurs, you must return a failure IAsyncResult
, which may be caused, for instance, by an endpoint being unresolvable. The simplest way to share this is by using AsyncResultImpl
:
try { ... }
catch(Exception e) {
IAsyncResult errorResult =
AsyncResultImpl.<IApiConnectionResponse> create(e);
resultHandler.handle(errorResult);
}
Remember to clean up any resources you may have left open. |
Implementation strategies
Implementors may notice that the only overlap between the IApiConnection
and IApiConnectionResponse
interfaces is the isFinished
method. Hence, it is often possible to implement both interfaces using the same class, which may be a cleaner way to orchestrate the process.
Implementation examples:
-
Servlet HTTP Connector is a more traditional synchronous implementations.
-
Vert.x 3 HTTP Connector is an asynchronous HTTP implementation.