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 as my-decorator.

ExpressionPlugin

An ExpressionPlugin adds a node to the Expression context tree, alongside env (for environment variables), and system (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, the getKey() method returns the name of the node, and the getObject() method returns the unified expression language context object that contains the values needed to resolve the expression. The plugins for env and system 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, under META-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 the src/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 a filter() method, which takes a Context, a Request, and the Handler, which is the next filter or handler to dispatch to. The filter() 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 its handle() method to dispatch the request to the rest of the chain.

Handler

A Handler generates a response for a request.

The Handler interface exposes a handle() 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 a resolve(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:

    1. Unpack IG-7.0.1.war

    2. Include the .jar library in WEB-INF/lib

    3. Create a new .war file

    The following example adds the .jar file sample-filter to custom.war:

    $ mkdir root && cd root
    $ jar -xf ~/Downloads/IG-7.0.1.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 deploy IG-7.0.1.war.

Read a different version of :