Develop Core Plugins
Introduction
This page provides detailed information on how to develop core plugins for the RESTHeart Platform.
Core plugins are executed by restheart-platform-core
service.
Registering Plugins
Plugins must be registered to be available using the @RegisterPlugin
annotation:
@RegisterPlugin(name = "foo", description = "a fantastic plugin")
public class FooPlugin implements Service {
...
}
Additional attributes of @RegisterPlugin
annotation:
attribute | description | default value |
---|---|---|
priority | defines execution order for Intializer (less is higher priority) | 10 |
uri | sets the URI of a Service overwriting its default value specified with the Service.defaultUri() method. |
none |
enabledByDefault | true to enable the plugin. If false it can be enabled setting the plugin configuration argument ‘enabled’ |
true |
Until RESTHeart version 3.x, registering a plugin involved declaring it in the configuration file. Thanks to the @RegisterPlugin
annotation, this is not needed anymore.
Configuration
All Plugins accept the argument confArgs
. Set confArgs
defining an object in the yml configuration file with the same name of the plugin (as defined in its @RegisterPlugin annotation) under the plugins-args
configuration section:
#### Plugins configuration
plugins-args:
logMessageInitializer:
enabled: false
message: Hello World!
log-level: INFO
addBodyToWriteResponsesInitializer:
enabled: false
pingService:
uri: "/hello"
msg: Hello World!
Packaging
Plugins implementation classes must be added to the java classpath.
A convenient method is packaging the classes in the restheart-platform-core.jar
file as suggested in Packaging Plugins section.
Initializers
An initializer is a class with the method init()
that is invoked at RESTHeart startup time.
It can be used to perform initialization logic. For instance, it can programmatically add Transformers and Checkers or initialize the db.
The Initializer class must implement the org.restheart.plugins.Initializer
interface and use the ResiterPlugin
annotation.
The Initializer
interface:
public interface Initializer extends Plugin {
/**
*
* @param confArgs arguments optionally specified in the configuration file
*/
void init(Map<String, Object> confArgs);
}
Services
Services are a simple yet powerful way of implementing custom Web Services.
The Service implementation class must extend handler must extend the
abstract class org.restheart.plugins.Service
public abstract class Service extends PipedHttpHandler implements Plugin {
/**
* The configuration properties passed to this handler.
*/
protected final Map<String, Object> confArgs;
/**
* Creates a new instance of the Service
*
* @param confArgs arguments optionally specified in the configuration file
*/
public Service(Map<String, Object> confArgs) {
super(new ResponseSenderHandler());
this.confArgs = confArgs;
}
/**
*
* @return the default uri of the service, used if not specified in plugin
* configuration
*/
public String defaultUri() {
return null;
}
/**
* helper method to handle OPTIONS requests
*
* @param exchange
* @param context
* @throws Exception
*/
protected void handleOptions(
HttpServerExchange exchange,
RequestContext context)
throws Exception {
exchange.getResponseHeaders()
.put(HttpString.tryFromString("Access-Control-Allow-Methods"), "GET, PUT, POST, PATCH, DELETE, OPTIONS")
.put(HttpString.tryFromString("Access-Control-Allow-Headers"), "Accept, Accept-Encoding, Authorization, Content-Length, Content-Type, Host, If-Match, Origin, X-Requested-With, User-Agent, No-Auth-Challenge");
exchange.setStatusCode(HttpStatus.SC_OK);
exchange.endExchange();
}
}
It requires to override the method handleRequest()
inherited from PipedHttpHandler
.
public abstract void handleRequest(HttpServerExchange exchange, RequestContext context) throws Exception;
The two arguments of handleRequest()
are:
- HttpServerExchange the exchange
- RequestContext context (that is the suggested way to retrieve the information of the request such as the payload)
The method defaultUri()
can return a String such as /foo
that is the URI when the service will be bound. The URI can also be specified in the plugin configuration, see Plugin Configuration
Constructor
The Service implementation class must have a constructor with confArgs
argument; this is optionally set from the configuration file.
public MyService(final Map<String, Object> args) {
}
A complete example: pingService
The following is the code of the PingService that implements a simple ping service.
@RegisterPlugin(name = "pingService",
description = "Ping service")
public class PingService extends Service {
private final String msg;
/**
*
* @param confArgs arguments optionally specified in the configuration file
*/
public PingService(Map<String, Object> confArgs) {
super(confArgs);
this.msg = confArgs != null && confArgs.containsKey("msg")
? (String) confArgs.get("msg")
: "ping";
}
@Override
public String defaultUri() {
return "/ping";
}
/**
*
* @param exchange
* @param context
* @throws Exception
*/
@Override
public void handleRequest(HttpServerExchange exchange, RequestContext context) throws Exception {
if (context.isOptions()) {
handleOptions(exchange, context);
} else if (context.isGet()) {
context.setResponseContent(new BsonDocument("msg",
new BsonString(msg)));
context.setResponseStatusCode(HttpStatus.SC_OK);
} else {
context.setResponseStatusCode(HttpStatus.SC_NOT_IMPLEMENTED);
}
next(exchange, context);
}
}
pingService Configuration (to set the msg
argument)
plugins-args:
pingService:
msg: Hello World!
Transformers
Transformers allow to transform the request or the response.
Examples of Transformers are:
- filtering out from the response sensitive properties;
- adding the
filter={"visibility":"public"}
query parameter to requests limiting the client visibility on documents.
For implementation examples refer to the package org.restheart.plugins.transformers
A transformer is a java class that implements the interface org.restheart.plugins.Transformer.
/**
* contentToTransform can be directly manipulated or
* RequestContext.setResponseContent(BsonValue value) for response phase and
* RequestContext.setContent(BsonValue value) for request phase can be used
*
* @param exchange the server exchange
* @param context the request context
* @param contentToTransform the content data to transform
* @param args the args sepcified in the collection metadata via args
* property property
*/
void transform(
final HttpServerExchange exchange,
final RequestContext context,
BsonValue contentToTransform,
final BsonValue args);
/**
*
* @param exchange the server exchange
* @param context the request context
* @param contentToTransform the content data to transform
* @param args the args sepcified in the collection metadata via args
* property
* @param confArgs the args specified in the configuration file via args
* property
*/
default void transform(
HttpServerExchange exchange,
RequestContext context,
BsonValue contentToTransform,
final BsonValue args,
BsonValue confArgs) {
transform(exchange, context, contentToTransform, args);
}
}
The default, 5 arguments, method transform()
can be used to store the argument confArgs
in a instance variable when the Transformer needs the arguments specified via the configuration file
The following code, is an example transformer that adds the property _timestamp to the response body.
import io.undertow.server.HttpServerExchange;
import org.bson.BsonInt64;
import org.bson.BsonValue;
import org.restheart.handlers.RequestContext;
import org.restheart.plugins.Transformer;
package com.whatever;
@RegisterPlugin(name = "myTransformer",
description = "Add _timestamp to the body")
public class MyTransformer implements Transformer {
tranform(final HttpServerExchange exchange,
final RequestContext context,
BsonValue contentToTransform,
final BsonValue args) {
if (contentToTransform != null && contentToTransform.isDocument()){
contentToTransform.asDocument().put("_timestamp",
new BsonInt64(System.currentTimeMillis()));
}
}
}
Checkers
Checkers allows to check the request so that, if it does not fulfill some conditions, it returns 400 BAD REQUEST response code thus enforcing a well defined structure to documents.
For implementation examples refer to the package org.restheart.plugins.checkers
A checker is a java class that implements the interface org.restheart.plugins.Checker.
public interface Checker extends Plugin {
enum PHASE {
BEFORE_WRITE,
AFTER_WRITE // for optimistic checks, i.e. document is inserted and in case rolled back
};
/**
*
* @param exchange the server exchange
* @param context the request context
* @param contentToCheck the contet to check
* @param args the args sepcified in the collection metadata via args
* @return true if check completes successfully
*/
boolean check(
HttpServerExchange exchange,
RequestContext context,
BsonDocument contentToCheck,
BsonValue args);
/**
*
* @param exchange the server exchange
* @param context the request context
* @param args the args sepcified in the collection metadata via args property
* @param confArgs the args specified in the configuration file via args property
* @return true if check completes successfully
*/
default boolean check(
HttpServerExchange exchange,
RequestContext context,
BsonDocument contentToCheck,
BsonValue args,
BsonValue confArgs) {
return check(exchange, context, contentToCheck, args);
}
/**
* Specify when the checker should be performed: with BEFORE_WRITE the
* checkers gets the request data (that may use the dot notation and update
* operators); with AFTER_WRITE the data is optimistically written to the db
* and rolled back eventually. Note that AFTER_WRITE helps checking data
* with dot notation and update operators since the data to check is
* retrieved normalized from the db.
*
* @param context
* @return BEFORE_WRITE or AFTER_WRITE
*/
PHASE getPhase(RequestContext context);
/**
*
* @param context
* @return true if the checker supports the requests
*/
boolean doesSupportRequests(RequestContext context);
}
The default, 5 arguments, method check()
can be used to store the argument confArgs
in a instance variable when the Checker needs the arguments specified via the configuration file
If the checker cannot process the request, the method doesSupportRequests()
should return false. This allows to skip executing the checker. The class CheckersUtils
provides some helper method to check the type of the request, e.g CheckersUtils.isBulkRequest()
.
When a checker does not support a request, the outcome depends on the attribute skipNotSupported
of the checker definition (see Apply a Checker via metadata and Apply a Checker programmatically); when skipNotSupported=true
, it just skips the checker; otherwise the request is not processed further and BAD REQUEST is returned.
The following code, is an example checker that checks if the number property in PATCH request body is between 0 and 10.
package com.whatever;
@RegisterPlugin(
name = "checkNumber",
description = "Checks if number property is between 0 and 10 on PATCH requests")
public class MyChecker implements Checker {
@Override
boolean check(HttpServerExchange exchange, RequestContext context, BsonValue args) {
// return true if request is not a PATCH or request body does not contain the property number
if (context.getMethod() != RequestContext.METHOD.PATCH
|| context.getContent() == null
|| !context.getContent().isDocument()
|| !context.getContent().asDocument().containsKey("number")) {
return true;
}
BsonValue _value = context.getContent().asDocument().get("number");
if (_value != null && _value.isNumber()) {
Integer value = _value.asInt32().getValue();
return value < 10 && value > 0;
} else {
return false; // BAD REQUEST
}
}
@Override
public PHASE getPhase(RequestContext context) {
return PHASE.BEFORE_WRITE;
}
@Override
public boolean doesSupportRequests(RequestContext context) {
return true;
}
}
Hooks
Request Hooks allow to execute custom code after a request completes.
For example, request hooks can be used:
- to send a confirmation email when a user registers
- to send push notifications when a resource is updated so that its properties satisfy a given condition.
For implementation examples refer to the package org.restheart.plugins.hooks
Hooks are developed implementing the java interface org.restheart.plugins.Hook.
The Hook interface requires to implement the following interface:
public interface Hook extends Plugin {
/**
*
* @param exchange the server exchange
* @param context the request context
* @param args the args sepcified in the collection metadata via args property
* @return true if completed successfully
*/
default boolean hook(
HttpServerExchange exchange,
RequestContext context,
BsonValue args) {
return hook(exchange, context, args, null);
}
/**
*
* @param exchange the server exchange
* @param context the request context
* @param args the args sepcified in the collection metadata via args property
* @param confArgs args specified in the configuration file via args property
* @return true if completed successfully
*/
default boolean hook(
HttpServerExchange exchange,
RequestContext context,
BsonValue args,
BsonDocument confArgs) {
return hook(exchange, context, args);
}
/**
*
* @param context
* @return true if the hook supports the requests
*/
boolean doesSupportRequests(RequestContext context);
}
The default, 4 arguments, method hook()
can be used to store the argument confArgs
in a instance variable when the Checker needs the arguments specified via the configuration file
The method doesSupportRequests()
determines if the hook()
method
should be executed against the RequestContext
object that
encapsulates all information about the request.
For instance, the following implementation returns true
if the request
actually created a document (either POSTing the collection or PUTing
the document):
@Override
public boolean doesSupportRequests(RequestContext rc) {
if (rc.getDbOperationResult() == null) {
return false;
}
int status = rc.getDbOperationResult().getHttpCode();
return (status == HttpStatus.SC_CREATED
&& (rc.isCollection() && rc.isPost())
|| rc.isDocument() && rc.isPut());
}
Note the following useful RequestContext
getters:
getDbOperationResult() | returns the OperationResult object that encapsulates the information about the MongoDB operation, including the resource status (properties) before and after the request execution. |
getType() | returns the request resource type, e.g. DOCUMENT, COLLECTION, etc. |
getMethod() | returns the request method, e.g. GET, PUT, POST, PATCH, DELETE, etc. |