|
| 1 | +--- |
| 2 | +docs_version: 9 |
| 3 | +title: Plugin Core Concepts |
| 4 | +layout: docs-adoc |
| 5 | +menu: framework |
| 6 | +applies_to: restheart |
| 7 | +--- |
| 8 | + |
| 9 | +This page covers the essential concepts you need to understand when developing RESTHeart plugins. |
| 10 | + |
| 11 | +TIP: New to plugins? Start with the link:/docs/framework/overview[Plugin Development Overview] and link:/docs/framework/tutorial[Tutorial] first. |
| 12 | + |
| 13 | +== Project Setup and Dependencies |
| 14 | + |
| 15 | +The only required dependency to develop a plugin is `restheart-commons`. |
| 16 | + |
| 17 | +With maven: |
| 18 | + |
| 19 | +[source,xml] |
| 20 | +---- |
| 21 | +<dependency> |
| 22 | + <groupId>org.restheart</groupId> |
| 23 | + <artifactId>restheart-commons</artifactId> |
| 24 | + <version>VERSION</version> |
| 25 | +</dependency> |
| 26 | +---- |
| 27 | + |
| 28 | +With Gradle: |
| 29 | + |
| 30 | +[source,gradle] |
| 31 | +---- |
| 32 | +dependencies { |
| 33 | + implementation 'org.restheart:restheart-commons:VERSION' |
| 34 | +} |
| 35 | +---- |
| 36 | + |
| 37 | +TIP: Use the link:https://github.com/SoftInstigate/restheart-plugin-skeleton[plugin skeleton project] for a ready-to-go Maven setup. |
| 38 | + |
| 39 | +== The @RegisterPlugin Annotation |
| 40 | + |
| 41 | +Every plugin must be annotated with `@RegisterPlugin`. This annotation: |
| 42 | + |
| 43 | +- Allows RESTHeart to discover your plugin at startup |
| 44 | +- Defines the plugin's name, description, and behavior |
| 45 | +- Specifies configuration like URIs, security settings, and execution priorities |
| 46 | + |
| 47 | +**Basic example:** |
| 48 | + |
| 49 | +[source,java] |
| 50 | +---- |
| 51 | +@RegisterPlugin(name = "foo", |
| 52 | + description = "just an example service", |
| 53 | + defaultUri="/foo", // optional, default /<service-name> |
| 54 | + secure=false, // optional, default false |
| 55 | + enabledByDefault=false) // optional, default true |
| 56 | +public class MyPlugin implements JsonService { |
| 57 | +... |
| 58 | +} |
| 59 | +---- |
| 60 | + |
| 61 | +**Annotation Parameters:** |
| 62 | + |
| 63 | +[options="header"] |
| 64 | +|=== |
| 65 | +|param |plugin |description |mandatory |default value |
| 66 | +|`name` |
| 67 | +|all |
| 68 | +|the name of the plugin |
| 69 | +|yes |
| 70 | +|*none* |
| 71 | +|`description` |
| 72 | +|all |
| 73 | +|description of the plugin |
| 74 | +|yes |
| 75 | +|*none* |
| 76 | +|`enabledByDefault` |
| 77 | +|all |
| 78 | +|`true` to enable the plugin; can be overridden by the plugin configuration option `enabled` |
| 79 | +|no |
| 80 | +|`true` |
| 81 | +|`defaultURI` |
| 82 | +|service |
| 83 | +|the default URI of the Service; can be overridden by the service configuration option `uri` |
| 84 | +|no |
| 85 | +|/<srv-name> |
| 86 | +|`matchPolicy` |
| 87 | +|service |
| 88 | +|`PREFIX` to match request paths starting with `/<uri>`,`EXACT` to only match the request path `/<uri>` |
| 89 | +|no |
| 90 | +|`PREFIX` |
| 91 | +|`secure` |
| 92 | +|service |
| 93 | +|`true` to require successful authentication and authorization to be invoked; can be overridden by the service configuration option `secure` |
| 94 | +|no |
| 95 | +|`false` |
| 96 | +|`dontIntercept` |
| 97 | +|service |
| 98 | +|list of interceptPoints to be executed on requests handled by the service, e.g. `dontIntercept = { InterceptPoint.ANY, InterceptPoint.RESPONSE }` |
| 99 | +|no |
| 100 | +|`{}` |
| 101 | +|`interceptPoint` |
| 102 | +|interceptor |
| 103 | +|the intercept point: `REQUEST_BEFORE_AUTH`, `REQUEST_AFTER_AUTH`, `RESPONSE`, `RESPONSE_ASYNC` |
| 104 | +|no |
| 105 | +|REQUEST_AFTER_AUTH |
| 106 | +|`initPoint` |
| 107 | +|initializer |
| 108 | +|specify when the initializer is executed: `BEFORE_STARTUP`, `AFTER_STARTUP` |
| 109 | +|no |
| 110 | +|`AFTER_STARTUP` |
| 111 | +|`requiresContent` |
| 112 | +|proxy interceptor |
| 113 | +|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) |
| 114 | +|no |
| 115 | +|`false` |
| 116 | +|`priority` |
| 117 | +|interceptor, initializer |
| 118 | +|the execution priority (less is higher priority) |
| 119 | +|no |
| 120 | +|`10` |
| 121 | +|`blocking` |
| 122 | +|service |
| 123 | +|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. |
| 124 | +|no |
| 125 | +|`true` |
| 126 | +|`authorizerType` |
| 127 | +|authorizer |
| 128 | +|`ALLOWER` can authorize a request unless no `VETOER` vetoes it. |
| 129 | +|no |
| 130 | +|ALLOWER |
| 131 | +|=== |
| 132 | + |
| 133 | + |
| 134 | +== Configuration |
| 135 | + |
| 136 | +Plugins are configured in `restheart.yml` using the plugin name from `@RegisterPlugin`: |
| 137 | + |
| 138 | +[source,yml] |
| 139 | +---- |
| 140 | +ping: |
| 141 | + enabled: true |
| 142 | + secure: false |
| 143 | + uri: /ping |
| 144 | + msg: 'Ping!' |
| 145 | +---- |
| 146 | + |
| 147 | +**Special configuration options** automatically managed by RESTHeart: |
| 148 | + |
| 149 | +- **enabled**: Enable/disable the plugin (overrides `enabledByDefault` in `@RegisterPlugin`) |
| 150 | +- **uri**: Set the service URI (overrides `defaultUri` in `@RegisterPlugin`) |
| 151 | +- **secure**: Require authentication/authorization (`true`) or allow open access (`false`) |
| 152 | + |
| 153 | +WARNING: Services have `secure: false` by default! Always set `secure: true` for production services that need protection. |
| 154 | + |
| 155 | +**Accessing configuration in code:** |
| 156 | + |
| 157 | +[source,java] |
| 158 | +---- |
| 159 | +@Inject("conf") |
| 160 | +Map<String, Object> conf; |
| 161 | +
|
| 162 | +// Use helper method to get values |
| 163 | +String msg = argValue(conf, "msg"); |
| 164 | +---- |
| 165 | + |
| 166 | + |
| 167 | +== Dependency Injection |
| 168 | + |
| 169 | +Use `@Inject` to access RESTHeart's built-in objects and services. Available providers: |
| 170 | + |
| 171 | +[cols="2,3,3", options="header"] |
| 172 | +|=== |
| 173 | +|Injection |Type |Use For |
| 174 | + |
| 175 | +|`@Inject("conf")` |
| 176 | +|`Map<String, Object>` |
| 177 | +|Plugin's configuration |
| 178 | + |
| 179 | +|`@Inject("rh-config")` |
| 180 | +|`Configuration` |
| 181 | +|RESTHeart's global configuration |
| 182 | + |
| 183 | +|`@Inject("mclient")` |
| 184 | +|`MongoClient` |
| 185 | +|MongoDB database access |
| 186 | + |
| 187 | +|`@Inject("registry")` |
| 188 | +|`PluginsRegistry` |
| 189 | +|Access to other plugins |
| 190 | + |
| 191 | +|`@Inject("acl-registry")` |
| 192 | +|`ACLRegistry` |
| 193 | +|Programmatic permission management |
| 194 | + |
| 195 | +|`@Inject("gql-app-definition-cache")` |
| 196 | +|`LoadingCache<String, GraphQLApp>` |
| 197 | +|GraphQL app definition cache |
| 198 | +|=== |
| 199 | + |
| 200 | +**Example:** |
| 201 | + |
| 202 | +[source,java] |
| 203 | +---- |
| 204 | +public class MyPlugin implements JsonService { |
| 205 | + @Inject("mclient") |
| 206 | + private MongoClient mclient; |
| 207 | + |
| 208 | + @Inject("conf") |
| 209 | + private Map<String, Object> conf; |
| 210 | + |
| 211 | + @Override |
| 212 | + public void handle(JsonRequest req, JsonResponse res) { |
| 213 | + // Use injected MongoClient |
| 214 | + var db = mclient.getDatabase("mydb"); |
| 215 | + // Use injected configuration |
| 216 | + var setting = argValue(conf, "mySetting"); |
| 217 | + } |
| 218 | +} |
| 219 | +---- |
| 220 | + |
| 221 | +TIP: See link:/docs/framework/providers[Providers] for how to create your own injectable objects. |
| 222 | + |
| 223 | +== Request and Response Types |
| 224 | + |
| 225 | +Services and Interceptors are **generic classes** that use type parameters to define how they handle request and response data. |
| 226 | + |
| 227 | +RESTHeart provides specialized Request/Response pairs for different data formats: |
| 228 | + |
| 229 | +[cols="2,3,3", options="header"] |
| 230 | +|=== |
| 231 | +|Type Pair |Content Format |Best For |
| 232 | + |
| 233 | +|`JsonRequest` / `JsonResponse` |
| 234 | +|JSON objects |
| 235 | +|REST APIs, JSON services |
| 236 | + |
| 237 | +|`BsonRequest` / `BsonResponse` |
| 238 | +|BSON (MongoDB format) |
| 239 | +|MongoDB operations |
| 240 | + |
| 241 | +|`MongoRequest` / `MongoResponse` |
| 242 | +|MongoDB-specific |
| 243 | +|Advanced MongoDB features |
| 244 | + |
| 245 | +|`ByteArrayRequest` / `ByteArrayResponse` |
| 246 | +|Raw bytes |
| 247 | +|Binary data, files |
| 248 | + |
| 249 | +|`StringRequest` / `StringResponse` |
| 250 | +|Plain text |
| 251 | +|Text processing |
| 252 | + |
| 253 | +|`BsonFromCsvRequest` |
| 254 | +|CSV to BSON |
| 255 | +|CSV imports |
| 256 | +|=== |
| 257 | + |
| 258 | +**Why this matters:** |
| 259 | + |
| 260 | +- The type you choose determines how request content is parsed and cached |
| 261 | +- Each type provides helper methods for its specific use case (e.g., `MongoRequest.getPageSize()`) |
| 262 | +- Content is parsed once and cached for efficiency |
| 263 | +- Type safety helps prevent errors at compile time |
| 264 | + |
| 265 | +**Example:** |
| 266 | + |
| 267 | +[source,java] |
| 268 | +---- |
| 269 | +@RegisterPlugin(name = "myService", defaultUri = "/myapi") |
| 270 | +public class MyService implements JsonService { |
| 271 | + @Override |
| 272 | + public void handle(JsonRequest req, JsonResponse res) { |
| 273 | + // req and res are strongly typed for JSON |
| 274 | + JsonObject content = req.getContent(); |
| 275 | + res.setContent(Json.object().put("status", "ok")); |
| 276 | + } |
| 277 | +} |
| 278 | +---- |
| 279 | + |
| 280 | +TIP: Start with `JsonService` for most REST APIs. See link:/docs/framework/services[Services] for detailed examples of each type. |
| 281 | + |
| 282 | +== How Content Parsing Works |
| 283 | + |
| 284 | +Understanding how RESTHeart handles request content is important for efficient plugin development. |
| 285 | + |
| 286 | +**The parsing lifecycle:** |
| 287 | + |
| 288 | +1. When a request arrives, RESTHeart determines which Service will handle it |
| 289 | +2. The Service creates typed Request and Response objects |
| 290 | +3. On first call to `request.getContent()`, the content is parsed using `parseContent()` |
| 291 | +4. The parsed content is cached in the request object |
| 292 | +5. Subsequent calls to `getContent()` return the cached object (no re-parsing) |
| 293 | + |
| 294 | +**Why this matters:** |
| 295 | + |
| 296 | +- Content is only parsed when you actually need it (lazy evaluation) |
| 297 | +- Multiple plugins can access the same content without overhead |
| 298 | +- You can safely call `getContent()` multiple times |
| 299 | + |
| 300 | +**Custom parsing:** |
| 301 | + |
| 302 | +If you create a custom ServiceRequest, implement the `parseContent()` method: |
| 303 | + |
| 304 | +[source,java] |
| 305 | +---- |
| 306 | +public abstract class ServiceRequest<T> { |
| 307 | + /** |
| 308 | + * Parses the content from the exchange and converts it into type T. |
| 309 | + * Called automatically by getContent() on first invocation. |
| 310 | + */ |
| 311 | + public abstract T parseContent() throws IOException, BadRequestException; |
| 312 | +} |
| 313 | +---- |
| 314 | + |
| 315 | +== Next Steps |
| 316 | + |
| 317 | +- **Apply these concepts:** link:/docs/framework/services[Build a Service] |
| 318 | +- **See it in action:** link:/docs/framework/tutorial[Plugin Development Tutorial] |
| 319 | +- **Advanced topics:** link:/docs/framework/providers[Create Custom Providers] |
0 commit comments