Edit Page

Develop Core Plugins

Introduction

Plugins allow to extend RESTHeart:

  • Services extend the API adding web services.

  • Interceptors snoop and modify requests and responses at different stages of the request lifecycle.

  • Initializers execute initialization logic at system startup time.

  • Providers provide object to other plugins via the @Inject annotation.

It is also possible developing security plugins to customize the security layer. Refer to Develop Security Plugins for more information.

For code examples of Plugins please refer to RESTHeart Examples repository on GitHub.

Tip
Watch Introduction

Dependency

The only required dependency to develop a plugin is restheart-commons.

With maven:

<dependency>
    <groupId>org.restheart</groupId>
    <artifactId>restheart-commons</artifactId>
    <version>VERSION</version>
</dependency>

@RegisterPlugin annotation

All plugins must be a annotated with @RegisterPlugin to: - allow RESTHeart to find plugins' implementation classes in deployed jars (see How to Deploy Plugins) - specify parameters such us the URI of a Service or the intercept point of an Interceptor.

An example follows:

@RegisterPlugin(name = "foo",
    description = "just an example service",
    defaultUri="/foo",      // optional, default /<service-name>
    secure=false,           // optional, default false
    enabledByDefault=false) // optional, default true
public class MyPlugin implements JsonService {
...
}

The following table describes the arguments of the annotation:

param plugin description mandatory default value

name

all

the name of the plugin

yes

none

description

all

description of the plugin

yes

none

enabledByDefault

all

true to enable the plugin; can be overridden by the plugin configuration option enabled

no

true

defaultURI

service

the default URI of the Service; can be overridden by the service configuration option uri

no

/<srv-name>

matchPolicy

service

PREFIX to match request paths starting with /<uri>,EXACT to only match the request path /<uri>

no

PREFIX

secure

service

true to require successful authentication and authorization to be invoked; can be overridden by the service configuration option secure

no

false

dontIntercept

service

list of interceptPoints to be executed on requests handled by the service, e.g. dontIntercept = { InterceptPoint.REQUEST_BEFORE_AUTH, InterceptPoint.RESPONSE }

no

{}

interceptPoint

interceptor

the intercept point: REQUEST_BEFORE_AUTH, REQUEST_AFTER_AUTH, RESPONSE, RESPONSE_ASYNC

no

REQUEST_AFTER_AUTH

initPoint

initializer

specify when the initializer is executed: BEFORE_STARTUP, AFTER_STARTUP

no

AFTER_STARTUP

requiresContent

proxy interceptor

Only used by Interceptors of proxied resources (the content is always available to Interceptor of Services) Set it to true to make available the content of the request (if interceptPoint is REQUEST_BEFORE_AUTH or REQUEST_AFTER_AUTH) or of the response (if interceptPoint is RESPONSE or RESPONSE_ASYNC)

no

false

priority

interceptor, initializer

the execution priority (less is higher priority)

no

10

blocking

service

With blocking = false the execution of the service is not dispatched to a working thread and executed by the io-thread, thus avoiding the overhead of the thread handling and switching.

no

true

authorizerType

authorizer

ALLOWER can authorize a request unless no VETOER vetoes it.

no

ALLOWER

Plugin Configuration

A plugins has a name as defined by the the @RegisterPlugin annotation. To define a configuration object for a plugin just use its name in the configuration:

ping:
    enabled: true
    secure: false
    uri: /ping
    msg: 'Ping!'

enabled secure and uri are special configuration options that are automatically managed by RESTHeart:

  • enabled: for enabling or disabling the plugin via configuration overwriting the enabledByDefault property of @RegisterPlugin

  • uri: applies to Services to bind them to the URI overwriting the defaultUri property of @RegisterPlugin

  • secure: applies to Services, with secure: true the service request goes thought the authentication and authorization phases, with secure: false the service is fully open.

Warning
secure is false by default. If you don’t specify secure=true your Service is fully open. If your service needs to be protected either define secure=true in the @RegisterPlugin annotation of add a configuration for it with secure: true

The plugin consumes the configuration with a field annotated with @Inject("conf"):

@Inject("config")
Map<String, Object> config;

@OnInit
public void init() throws ConfigurationException {
    this.msg = argValue(this.config, "msg");
}

argValue() is an helper method to simplify retrieving the value of the configuration argument.

Dependency injection

Available providers allow to inject the following objects:

  • @Inject("config") - injects the plugins configuration as a Map<String, Object>

  • @Inject("rh-config") - injects the RESTHeart org.restheart.configuration.Configuration object.

  • @Inject("registry") - injects the PluginsRegistry singleton that allows a plugin to get the reference of other plugins.

  • @Inject("mclient") - injects the MongoClient object that has been already initialized and connected to MongoDB by the mongo-client-provider.

  • @Inject("mclient-reactive") - injects the reactive MongoClient object that has been already initialized and connected to MongoDB by the mongo-client-provider.

@Inject("registry")
private PluginsRegistry registry;
@Inject("mclient")
private MongoClient mclient;

Request and Response Generic Classes

Services and Interceptor are generic classes. They use type parameters for Request and Response classes.

Many concrete implementations of specialized Request and Response exist in the org.restheart.exchange package to simplify development:

  • JsonRequest and JsonResponse

  • BsonRequest and BsonResponse

  • MongoRequest and MongoResponse

  • ByteArrayRequest and ByteArrayResponse

  • StringRequest and StringResponse

  • BsonFromCsvRequest

  • UninitializedRequest and UninitializedResponse

Those implementations differ on the data type used to hold the request and response content. For example, ByteArrayRequest and BsonRequest hold content as byte[] and BsonValue respectively.

Different implementation can also provide some helper methods to cope with specific request parameter. For instance, the MongoRequest, i.e. the request used by the MongoService, has the method getPageSize() because this is a query parameter used by that service.

When a request hits RESTHeart, it determines which service will handle it. The Service implementation is responsible of instantiating the correct Request and Response objects that will be used along the whole exchange processing chain.

Services

Depending on the content type, the Service class implements one of the specialized org.restheart.plugins.Service interfaces. The following implementation are provided by restheart-commons:

  • ByteArrayService

  • JsonService

  • BsonService

The code of example mongo-status-service implementing BsonService and using the MongoClient obtained via @Inject("mclient") follows:

@RegisterPlugin(
        name = "serverstatus",
        description = "returns MongoDB serverStatus",
        enabledByDefault = true,
        defaultURI = "/status")
public class MongoServerStatusService implements BsonService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MongoServerStatusService.class);

    @Inject("mclient")
    private MongoClient mongoClient;

    private static final BsonDocument COMMAND = document().put("serverStatus", 1);

    @Override
    public void handle(BsonRequest request, BsonResponse response) throws Exception {
        if (request.isGet()) {
            var serverStatus = mongoClient.getDatabase("admin").runCommand(COMMAND, BsonDocument.class);

            response.setContent(serverStatus);
            response.setStatusCode(HttpStatus.SC_OK);
            response.setContentTypeAsJson();
        } else {
            // Any other HTTP verb is a bad request
            response.setStatusCode(HttpStatus.SC_BAD_REQUEST);
        }
    }
}

The key method is handle() that is executed when a request to the service URI hits RESTHeart.

Create Service with custom generic type

To implement a Service that handles different types of Request and Response, it must implement the base Service interface.

The base Service interface requires to implement methods to initialize and retrieve the Request and Response objects.

The following example shows how to handle XML content:

@RegisterPlugin(name = "myXmlService",
    description = "example service consuming XML requests",
    enabledByDefault = true,
    defaultURI = "/xml")
public class MyXmlService implements Service<XmlRequest, XmlResponse> {
    @Override
    default Consumer<HttpServerExchange> requestInitializer() {
        return e -> XmlRequest.init(e);
    }

    @Override
    default Consumer<HttpServerExchange> responseInitializer() {
        return e -> XmlResponse.init(e);
    }

    @Override
    default Function<HttpServerExchange, JsonRequest> request() {
        return e -> XmlRequest.of(e);
    }

    @Override
    default Function<HttpServerExchange, JsonResponse> response() {
        return e -> XmlResponse.of(e);
    }
}

The example follows a pattern that delegates the actual initialization (in requestInitializer() and responseInitializer()) and retrieval of the object from the exchange (in request() and response()) to the concrete class, as follows:

public class XmlRequest extends ServiceRequest<Document> {
    private XmlRequest(HttpServerExchange exchange) {
        super(exchange);
    }

    public static XmlRequest init(HttpServerExchange exchange) {
        var ret = new XmlRequest(exchange);

        try {
            ret.injectContent();
        } catch (Throwable ieo) {
            ret.setInError(true);
        }

        return ret;
    }

    public static XmlRequest of(HttpServerExchange exchange) {
        return of(exchange, XmlRequest.class);
    }

    public void injectContent() throws SAXException, IOException {
        var dBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        var rawContent = ChannelReader.read(wrapped.getRequestChannel());

        setContent(dBuilder.parse(rawContent)ml);
    }
}

In the constructor a call to super(exchange) attaches the object to the HttpServerExchange. The object is retrieved using the inherited of() method that gets the instance attachment from the HttpServerExchange. This is fundamental for two reasons: first the same request and response objects must be shared by the all handlers of the processing chain. Second, this avoid the need to parse the content several times for performance reasons.

Tip
Watch Services

Interceptors

Interceptors allow to snoop and modify requests and responses at different stages of the request lifecycle as defined by the interceptPoint parameter of the annotation @RegisterPlugin.

An interceptor can intercept either proxied requests or requests handled by Services.

An interceptor, but WildcardInterceptor, can intercept requests handled by a Service when its request and response types are equal to the ones declared by the Service.

An interceptor can intercept a proxied request, when its request and response types extends BufferedRequest and BufferedResponse.

The following implementation are provided by restheart-commons:

  • WildcardInterceptor intercepts requests handled by any service

  • ByteArrayInterceptor intercepts requests handled by services implementing ByteArrayService

  • JsonInterceptor intercepts requests handled by services implementing JsonService

  • BsonInterceptor intercepts requests handled by services implementing BsonService

  • MongoInterceptor intercepts requests handled by the MongoService

The last one is particularly useful as it allows intercepting requests to the MongoDb API.

@RegisterPlugin(name = "secretFilter",
    interceptPoint = InterceptPoint.RESPONSE,
    description = "removes the property 'secret' from GET /coll")
public class ReadOnlyPropFilter implements MongoInterceptor {
    @Override
    public void handle(MongoRequest request, MongoResponse response) throws Exception {
        if (response.getContent().isDocument()) {
            response.getContent().asDocument().remove("secret");
        } else if (request.getContent().isArray()) {
            response.getContent().asArray().stream()
                .map(doc -> doc.asDocument())
                .forEach(doc -> doc.remove("secret"));
        }
    }

    @Override
    public boolean resolve(MongoRequest request, MongoResponse response) {
        return request.isGet()
            && response.getContent() != null
            && "coll".equalsIgnoreCase(request.getCollectionName());
    }
}

The handle() method is invoked only if the resolve() method returns true.

Tip
Watch Interceptors

Initializers

An Initializer allows executing custom logic at startup time.

The Initializer implementation class must extend the org.restheart.plugins.Initializer interface:

public interface Initializer extends ConfigurablePlugin {
    public void init();
}

With the following code the Initializer hangs restheart startup until the user confirms.

@RegisterPlugin(name = "confirmStartupInitializer",
    description = "hangs restheart startup until the user hits <enter>"
    priority = 100,
    initPoint = InitPoint.BEFORE_STARTUP)
public class confirmStartupInitializer implements Initializer {
    public void init() {
        System.out.println("Hit <enter> to start RESTHeart");
        System.console().readLine();
    }
}
Tip
Watch Initializers

Providers

@Inject works together with the plugin type Provider, as in the following example:

Given the following Provider:

RegisterPlugin(name="hello-world-message", description="a dummy provider")
class MyProvider implements Provider<String> {
    @Override
    public String get(PluginRecord<?> caller) {
        return "Hello World!";
    }
}

We can inject it into a Plugin with the @Inject annotation:

@RegisterPlugin(name = "greetings", description = "just another Hello World")
public class GreeterService implements JsonService {
    @Inject("hello-world-message")
    private String message;

    @OnInit
    public void init() {
        // called after all @Inject fields are resolved
    }

    @Override
    public void handle(JsonRequest req, JsonResponse res) {
        switch(req.getMethod()) {
            case GET -> res.setContent(object().put("message", message));
            case OPTIONS -> handleOptions(req);
            default -> res.setStatusCode(HttpStatus.SC_METHOD_NOT_ALLOWED);
        }
    }
}