Extending IG Through the Java API
Important
When you are writing scripts or Java extensions, never use a Promise
blocking method, such as get()
, getOrThrow()
, or getOrThrowUninterruptibly()
, to obtain the response.
A promise represents the result of an asynchronous operation. Therefore, using a blocking method to wait for the result can cause deadlocks and/or race issues.
IG includes a complete Java application programming interface to allow you to customize IG to perform complex server interactions or intensive data transformations that you cannot achieve with scripts or the existing handlers, filters, and expressions described in "Expressions". The following sections describe how to extend IG through the Java API:
Key Extension Points
Interface Stability: Evolving, as defined in "ForgeRock Product Stability Labels".
The following interfaces are available:
- Decorator
A
Decorator
adds new behavior to another object without changing the base type of the object.When suggesting custom
Decorator
names, know that IG reserves all field names that use only alphanumeric characters. To avoid clashes, use dots or dashes in your field names, such asmy-decorator
.- ExpressionPlugin
An
ExpressionPlugin
adds a node to theExpression
context tree, alongsideenv
(for environment variables), andsystem
(for system properties). For example, the expression${system['user.home']}
yields the home directory of the user running the application server for IG.In your
ExpressionPlugin
, thegetKey()
method returns the name of the node, and thegetObject()
method returns the unified expression language context object that contains the values needed to resolve the expression. The plugins forenv
andsystem
return Map objects, for example.When you add your own
ExpressionPlugin
, you must make it discoverable within your custom library. You do this by adding a services file named after the plugin interface, where the file contains the fully qualified class name of your plugin, underMETA-INF/services/org.forgerock.openig.el.ExpressionPlugin
in the .jar file for your customizations. When you have more than one plugin, add one fully qualified class name per line. For details, see the reference documentation for the Java class ServiceLoader. If you build your project using Maven, then you can add this under thesrc/main/resources
directory. Add custom libraries, as described in "Embedding the Customization in IG".Be sure to provide some documentation for IG administrators on how your plugin extends expressions.
- Filter
A
Filter
serves to process a request before handing it off to the next element in the chain, in a similar way to an interceptor programming model.The
Filter
interface exposes afilter()
method, which takes a Context, a Request, and the Handler, which is the next filter or handler to dispatch to. Thefilter()
method returns a Promise that provides access to the Response with methods for dealing with both success and failure conditions.A filter can elect not to pass the request to the next filter or handler, and instead handle the request itself. It can achieve this by merely avoiding a call to
next.handle(context, request)
, creating its own response object and returning that in the promise. The filter is also at liberty to replace a response with another of its own. A filter can exist in more than one chain, therefore should make no assumptions or correlations using the chain it is supplied. The only valid use of a chain by a filter is to call itshandle()
method to dispatch the request to the rest of the chain.- Handler
A
Handler
generates a response for a request.The
Handler
interface exposes ahandle()
method, which takes a Context, and a Request. It processes the request and returns a Promise that provides access to the Response with methods for dealing with both success and failure conditions. A handler can elect to dispatch the request to another handler or chain.- ClassAliasResolver
A
ClassAliasResolver
makes it possible to replace a fully qualified class name with a short name (an alias) in an object declaration's type.The
ClassAliasResolver
interface exposes aresolve(String)
method to do the following:Return the class mapped to a given alias
Return
null
if the given alias is unknown to the resolver
All resolvers available to IG are asked until the first non-null value is returned or until all resolvers have been contacted.
The order of resolvers is nondeterministic. To prevent conflicts, don't use the same alias for different types.
Implementing a Customized Sample Filter
The SampleFilter
class implements the Filter
interface to set a header in the incoming request and in the outgoing response.
In the following example, the sample filter adds an arbitrary header:
package org.forgerock.openig.doc.examples; import org.forgerock.http.Filter; import org.forgerock.http.Handler; import org.forgerock.http.protocol.Request; import org.forgerock.http.protocol.Response; import org.forgerock.openig.heap.GenericHeaplet; import org.forgerock.openig.heap.HeapException; import org.forgerock.services.context.Context; import org.forgerock.util.promise.NeverThrowsException; import org.forgerock.util.promise.Promise; /** * Filter to set a header in the incoming request and in the outgoing response. */ public class SampleFilter implements Filter { /** Header name. */ String name; /** Header value. */ String value; /** * Set a header in the incoming request and in the outgoing response. * A configuration example looks something like the following. * * <pre> * { * "name": "SampleFilter", * "type": "SampleFilter", * "config": { * "name": "X-Greeting", * "value": "Hello world" * } * } * </pre> * * @param context Execution context. * @param request HTTP Request. * @param next Next filter or handler in the chain. * @return A {@code Promise} representing the response to be returned to the client. */ @Override public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, final Handler next) { // Set header in the request. request.getHeaders().put(name, value); // Pass to the next filter or handler in the chain. return next.handle(context, request) // When it has been successfully executed, execute the following callback .thenOnResult(response -> { // Set header in the response. response.getHeaders().put(name, value); }); } /** * Create and initialize the filter, based on the configuration. * The filter object is stored in the heap. */ public static class Heaplet extends GenericHeaplet { /** * Create the filter object in the heap, * setting the header name and value for the filter, * based on the configuration. * * @return The filter object. * @throws HeapException Failed to create the object. */ @Override public Object create() throws HeapException { SampleFilter filter = new SampleFilter(); filter.name = config.get("name").as(evaluatedWithHeapProperties()).required().asString(); filter.value = config.get("value").as(evaluatedWithHeapProperties()).required().asString(); return filter; } } }
The corresponding filter configuration is similar to this:
{ "name": "SampleFilter", "type": "org.forgerock.openig.doc.examples.SampleFilter", "config": { "name": "X-Greeting", "value": "Hello world" } }
Note how type
is configured with the fully qualified class name for SampleFilter
. To simplify the configuration, implement a class alias resolver, as described in "Implementing a Class Alias Resolver".
Implementing a Class Alias Resolver
To simplify the configuration of a customized object, implement a ClassAliasResolver
to allow the use of short names instead of fully qualified class names.
In the following example, a ClassAliasResolver
is created for the SampleFilter
class:
package org.forgerock.openig.doc.examples; import java.util.HashMap; import java.util.Map; import org.forgerock.openig.alias.ClassAliasResolver; /** * Allow use of short name aliases in configuration object types. * * This allows a configuration with {@code "type": "SampleFilter"} * instead of {@code "type": "org.forgerock.openig.doc.examples.SampleFilter"}. */ public class SampleClassAliasResolver implements ClassAliasResolver { private static final Map<String, Class<?>> ALIASES = new HashMap<>(); static { ALIASES.put("SampleFilter", SampleFilter.class); } /** * Get the class for a short name alias. * * @param alias Short name alias. * @return The class, or null if the alias is not defined. */ @Override public Class<?> resolve(final String alias) { return ALIASES.get(alias); } }
With this ClassAliasResolver
, the filter configuration in "Implementing a Customized Sample Filter" can use the alias instead of the fully qualified class name, as follows:
{ "name": "SampleFilter", "type": "SampleFilter", "config": { "name": "X-Greeting", "value": "Hello world" } }
To create a customized ClassAliasResolver
, add a services file with the following characteristics:
Name the file after the class resolver interface.
Store the file under
META-INF/services/org.forgerock.openig.alias.ClassAliasResolver
, in the customization .jar file.If you build your project using Maven, you can add the file under the
src/main/resources
directory.In your ClassAliasResolver file, add a line for the fully qualified class name of your resolver as follows:
org.forgerock.openig.doc.examples.SampleClassAliasResolver
If you have more than one resolver in your .jar file, add one line for each fully qualified class name.
Configuring the Heap Object for the Customization
Objects are added to the heap and supplied with configuration artifacts at initialization time. To be integrated with the configuration, a class must have an accompanying implementation of the Heaplet interface. The easiest and most common way of exposing the heaplet is to extend the GenericHeaplet class in a nested class of the class you want to create and initialize, overriding the heaplet's create()
method.
Within the create()
method, you can access the object's configuration through the config
field.
Embedding the Customization in IG
After building your customizations into a .jar file, add it to the configuration as follows:
For IG installed in standalone mode, create the directory
$HOME/.openig/extra
, where$HOME/.openig
is the instance directory, and add the .jar file to the directory.For IG installed in web container mode, include the .jar file in the IG .war file, as follows:
Unpack
IG-7.0.2.war
Include the .jar library in
WEB-INF/lib
Create a new .war file
The following example adds the .jar file
sample-filter
tocustom.war
:$
mkdir root && cd root
$jar -xf ~/Downloads/IG-7.0.2.war
$cp ~/Documents/sample-filter/target/sample-filter-1.0.0-SNAPSHOT.jar WEB-INF/lib
$jar -cf ../custom.war *
Deploy
custom.war
in the same way as you deployIG-7.0.2.war
.