Writing EPL plug-ins in Java

EPL plug-ins can be written in Java. This section describes the steps required to develop and deploy EPL plug-ins written in Java.

Creating an EPL plug-in using Java

To create a Java class to use as an EPL plug-in

  1. Create a public class for your plug-in. To mark the class as a plug-in, add the @com.apama.epl.plugin.annotation.EPLPlugin annotation. Specify the import name to be used from EPL, and a description for human consumption. For example:

    @com.apama.epl.plugin.annotation.EPLPlugin(
        name="TestPlugin",
        description="A test plugin"
    )
    public class TestPlugin
    {
        ...
    }
    
  2. Add one or more public static methods to the plug-in class that should be callable from EPL. These methods must match the permitted signatures described in Permitted signatures for methods. All calls from an Apama application are made to these static methods from all contexts. As the plug-in author you are responsible for ensuring correct concurrency.

  3. Compile the source code for your plug-in class(es), including lib/ap-correlator-extension-api.jar on the classpath so that the annotations can be found and processed. Package the compilation output into a JAR file (including the META-INF/jmon-jar.xml descriptor file that is automatically generated from the annotations alongside the classes), ready for including in the YAML initialization list for your project. You can perform these steps using a build script, or directly from the command line:

    cd src
    javac -classpath "$APAMA_HOME/lib/ap-correlator-extension-api.jar" -d ../bin *.java
    
    cd ../bin
    jar -cf ../MyPlugin.jar *
    

Configuring the classpath and application name for EPL plug-in JARs

Each Java plug-in JAR is loaded in its own dedicated Java classloader, which by default has access only to its own classes, and those available globally in the correlator’s system classloader (which includes some standard Apama libraries such as the ap-correlator-extension-api.jar and ap-util.jar).

If your plug-in JAR depends on any third-party libraries, configure them by adding an @ApplicationJar annotation. This annotation can be added into one (but not more than one) of the Java source files in your JAR:

@com.apama.epl.plugin.annotation.ApplicationJar(
   name = "MyApplicationJar",
   classpath = "${env:APAMA_PROJECT_DIR}/lib/foo.jar;${env:MY_THIRD_PARTY_DIR}/lib/bar.jar"
)

The name is used to identify this JAR in log messages and in case you need to use engine_delete.

The classpath string consists of any number of classpath entries, delimited by the semicolon ; character. Note that the semicolon should be used even on platforms that typically use a colon or other character to separate path entries.

Avoid using absolute paths in the classpath, as this makes it difficult to use the plug-in JAR on different machines. Instead, use ${...} placeholders to identify the first part of each path, for example, the installation directory of a third party whose libraries you wish to use. Currently, two types of placeholder are supported:

  • ${env:MY_ENV_VAR_NAME} is replaced by an environment variable called MY_ENV_VAR_NAME
  • ${sys:MY_SYS_PROP_NAME} is replaced by a Java system property called MY_SYS_PROP_NAME

If your YAML configuration file is located in the project root directory and defines an environment variable APAMA_PROJECT_DIR with the value ${PARENT_DIR} you can access the project directory using that environment variable, for example: ${env:APAMA_PROJECT_DIR}/lib/foo.jar;${env:APAMA_PROJECT_DIR}/lib/bar.jar. See Setting environment variables for Apama components.

If needed, the values for Java system property placeholders can be specified on the correlator command line as: -J-DMY_SYS_PROP_NAME=path

The correlator logs a warning for any path that cannot be found, but fails to inject the plug-in entirely if the classpath includes any ${...} placeholders that are not defined.

Permitted signatures for methods

For a method to be exposed to EPL, it must be public, must be static and every argument plus the return type must be one of the following:

Java Entry

EPL Type

Notes

int

integer

Truncated when passed in, for compatibility.

long

integer

String

string

Copy in / copy out.

boolean

boolean

double

float

java.lang.Number

decimal

Is either java.lang.Double type for NaN or infinity, or java.math.BigDecimal for a value.

java.math.BigDecimal

decimal

Passing in either NaN or infinity throws an exception that kills the monitor instance if not caught. Deprecated, use java.lang.Number instead.

com.apama.epl.plugin.Context

context

New type defined for plug-ins.

com.apama.epl.plugin.Channel

com.apama.Channel

New type defined for plug-ins.

Object

chunk

Any Java object can be held in EPL via a chunk.

TYPE[]

sequence<TYPE>

Any above type except int can be passed in as an arbitrary-depth nested array->sequence. The sequence is strictly copy-in, non-modifiable, but can be returned as copy-out.

void

N/A

Permitted as a return type only.

Any method not matching this signature is ignored and logged at DEBUG.

Overloaded functions

Any function with multiple overloads is ignored (none of them are exposed) and this is logged once at WARN and once per method at DEBUG.

Supported Java version

This Apama release uses Java version 17.

Using EPL plug-ins written in Java

After you create an EPL plug-in in Java, it must be injected into a Java-enabled correlator before it is available for use in Apama applications. Applications that will use the plug-in also need to import the plug-in by name.

Injecting a Java EPL plug-in

To use Java EPL plug-ins, the correlator must be started with the --java (or -j) option.

To inject the JAR into the correlator, add it to the initialization section of the YAML configuration file or inject the plug-in JAR file from the command line with the -j (--java) option:

engine_inject –j plugin_name.jar

Each Java plug-in JAR is loaded into its own separate classloader. This means that they have no access to any classes loaded in other JAR files. If your plug-in requires any other Java libraries, they must be listed in the classpath with an ApplicationJar annotation, included in the correlator’s global classpath, or injected in the same plug-in JAR file as the plug-in. See Configuring the classpath and application name for EPL plug-in JARs for more details.

Importing

Once a Java plug-in has been injected it is available for import using the name (including the package) defined in the @ApplicationJar annotation. The correlator will automatically introspect the class and make available any suitable, public methods that can be called directly from EPL. For example, the following code imports a plug-in named mypackage.TestPlugin and calls its dosomething method:

monitor m {
   import "mypackage.TestPlugin" as test;
   action onload()
   {
      test.dosomething();
   }
}

Note, if the plug-in JAR file has been incorrectly injected, the correlator will try to load the plug-in as a C++ plug-in and may give an error such as Error opening plug-in library libfoo.so: libfoo.so: cannot open shared object file: No such file or directory. If this happens and you were trying to load a plug-in written in Java, then check that the JAR file was created and injected correctly before your EPL file was injected.

Deleting

An EPL plug-in can be explicitly deleted by calling engine_delete with the application name defined in the @ApplicationJar annotation. EPL monitors using the plug-in depend on the plug-in type in the normal fashion. The plug-in will not be deleted until the application and all dependent monitors are deleted.

As each plug-in is loaded in its own classloader, once the application has been deleted, the plug-in can be re-injected and it will be loaded into a new classloader.

Deleting a plug-in written in Java can only be done when it is not used by any EPL monitors. If the --force option of the engine_delete tool is used, then all monitors importing the plug-in are also deleted.

Interacting with contexts

EPL plug-ins can be passed context objects using the com.apama.epl.plugin.Context type. The Context object is defined as:

package com.apama.epl.plugin;
public class Context
{
  public String toString();
  public Context();
  public String getName();
  public int hashCode();
  public boolean isPublic();
  public boolean equals(Context other);
  public static native Context getCurrent();
}

The getCurrent method returns the context that this method was called from.

Interacting with the correlator

EPL plug-ins can use the com.apama.epl.plugin.Correlator class to send an event, subscribe to a channel, or to specify blocking behavior. The Correlator class is defined as:

package com.apama.epl.plugin;
public class Correlator
{
  public static native void sendTo(String evt, String chan);
  public static native void sendTo(String evt, Context ctx);
  public static native void sendTo(String evt, Context[] ctxs);
  public static native void sendTo(String evt, Channel c);

  public static native void subscribe(EventHandler handler, String[] channels);
  public static native void unsubscribe(EventHandler handler, String[] channels);
  public static native void unsubscribe(EventHandler handler);

  public static native void pluginMethodBlocking();
}

The Correlator methods are:

  • sendTo(String, String) – Sends the event represented in the first String to the channel specified in the second String. Any contexts and external receivers that are subscribed to the specified channel receive the event. If there are no subscribers the event is discarded.
  • sendTo(String, Context) – Sends the event represented in String to the context referred to by the com.apama.epl.plugin.Context argument. An exception is thrown if the context reference is invalid.
  • sendTo(String, Context[]) – Sends the event represented in String to the array of contexts referred to by the com.apama.epl.plugin.Context[] argument. If one context reference is invalid an exception is thrown and the event is not sent to any context.
  • sendTo(String, Channel) — Sends the event represented in String. If the specified com.apama.epl.plugin.Channel object contains a string then the event is sent to the channel that has that name. If Channel contains a context then the event is sent to that context.
  • subscribe(EventHandler, String[]) – Subscribes the handler object to the channels listed in the string array. If the handler is already subscribed to some channels then the channels listed in the array are added to the list of existing subscriptions. Subscribing to the same channel multiple times results in a single subscription. However, to completely remove a channel subscription that has been added multiple times you must unsubscribe from that channel the same number of times that it was subscribed to.
  • unsubscribe(EventHandler, String[]) – For the channels specified in the string array, this method removes the subscriptions from the specified handler. It is possible for the result of this method to be that the handler is not subscribed to any channels. Unsubscription from a channel that the handler is not subscribed is ignored.
  • unsubscribe(EventHandler) – Removes all subscriptions from the specified handler. If this handler is not subscribed to any channels the method is ignored.
  • pluginMethodBlocking() – Informs the correlator that the plug-in is potentially blocking for the rest of this call and the correlator is free to spin up additional threads on which to run other contexts.

For more information on com.apama.epl.plugin.Context and com.apama.epl.plugin.Correlator, see the API reference for Java (Javadoc).

Receiving events from named channels

A Java plug-in can register callbacks to receive events that are sent to named channels. This is similar to the monitor.subscribe() method in EPL. Events are delivered in string form by means of a method on a known interface.

To register a callback, the plug-in must define a class that implements the com.apama.epl.plugin.EventHandler interface:

public interface EventHandler
{
   void handleEvent(String event, String channel);
}

The handleEvent() method is called once for each event sent to a channel that this handler is subscribed to, with the channel on which it was received. To manage EventHandler object channel subscriptions, use the subscribe() and unsubscribe() methods on com.apama.epl.plugin.Correlator. When a handler is unsubscribed from all channels any in-progress callbacks will complete, but no further callbacks will be made to that handler.

Working with Channel objects

Similar to context objects, you can pass EPL com.apama.Channel objects into a Java plug-in. The equivalent Java class is com.apama.epl.plugin.Channel and you can use objects of this class to send events to channels. Like the EPL Channel type, the Java Channel class has three constructors:

Channel (String name)
Channel (com.apama.epl.plugin.Context c)
Channel ()

A Channel object can contain a string that is the name of a channel or it can contain a context. The no-argument constructor creates a Channel object that contains an empty context. If you try to send an event to an empty context the sendTo() method throws an exception.

You can call the empty() method on a Java Channel object. It returns true only if the object contains an empty context.

Exceptions

If a method throws an exception, that exception is passed up to the calling EPL and can be caught by the calling monitor. If an exception is not caught it will terminate the monitor instance. Details on catching exceptions in EPL can be found in Exception handling.

If a Java plug-in throws a java.lang.RuntimeException, or subclass, which is in the java. namespace (for example, java.lang.NullPointerException) then it will be logged at ERROR with a stacktrace before being rethrown. Unchecked exceptions from other sources (for example client exception types) will not be logged.

Load, unload, and shutdown hooks

If a plug-in needs to run anything when it is loaded, you can do this in a static initializer:

public class Plugin
{
   static {
    ... // initialization code here
   }
}

It is not natively possible for a plug-in to run anything when it is unloaded. If you need this functionality you can declare a method to be called when the plug-in is unloaded using annotations:

public class Plugin
{
   @com.apama.epl.plugin.annotation.Callback(
      type=com.apama.epl.plugin.annotation.Callback.CBType.SHUTDOWN)
   public static void shutdown()
   {
    ... // shutdown code here
   }
}

The method must be a public static function which takes no arguments and returns void. Currently, Apama does not support callbacks other than SHUTDOWN.

Non-blocking plug-ins and methods

In a correlator some threads have the potential to block and others do not. If a thread might block, the correlator starts new threads if it has additional runnable contexts. By default the correlator assumes that a plug-in call may block and will start additional threads on which to run other contexts. In situations where the plug-in call can never block, the additional overhead of starting new threads when all CPUs are busy is unnecessary. If you know that a plug-in or an individual method is non-blocking, you can improve efficiency by annotating either entire plug-ins or individual methods as non-blocking.

Note, however, if a method declared as non-blocking does block, the correlator can block all threads waiting for them to finish, resulting in a deadlocked correlator. For methods that are normally non-blocking, but may block in predictable situations, see “Sometimes-blocking functions” below.

  • Annotations. You can apply the annotation @com.apama.epl.plugin.annotation.NoBlock with no arguments to either a plug-in class, or to a method on a class:

    @com.apama.epl.plugin.annotation.NoBlock()
    public class Plugin
    {
        ...
    }
    

    When applied to a class, the annotation indicates that no method on the plug-in can ever block.

    public class Plugin
    {
      @com.apama.epl.plugin.annotation.NoBlock()
      public static String getValue() {... }
    }
    

    When applied to a method, the annotation indicates that this method will never block, but other methods may block.

  • Sometimes-blocking functions. If you have a function that usually will not block, but under some known conditions may block, then the method can be declared as NoBlock as long as it then uses a callback to indicate when it is starting the potentially-blocking behavior. The callback is a static method on com.apama.epl.plugin.Correlator called pluginMethodBlocking. This function takes no arguments, returns no value and is idempotent. When it is called, the correlator will then assume that the plug-in is potentially blocking for the rest of this call and is free to spin up additional threads on which to run other contexts.

    public class Plugin
    {
       @com.apama.epl.plugin.annotation.NoBlock()
       public static String getValue()
       {
          if (null != localValue) return localValue;
          else {
             com.apama.epl.plugin.Correlator.pluginMethodBlocking();
             localValue = getRemoteValue();
             return localValue;
          }
       }
    }
    

Logging

EPL plug-ins written in Java can log to the correlator’s log file, using the open-source SLF4J API.

Each plug-in must create a static instance of Logger, which provides methods such as debug(...), info(...), warn(...) and error(...). These result in a message at that log level in the correlator log file. See also Setting correlator and plug-in log files and log levels in a YAML configuration file.

For more information on using the Logger class, including how to override the default log level, see the API reference for Java (Javadoc).

The following is an example of logging in an EPL plug-in:

package test;

public class MyPlugin
{
   private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger("plugins.test.MyPlugin");
   public static void foo()
   {
      logger.info("A string that's logged at INFO");
   }
}

This will produce entries in the correlator log file like this:

2019-06-24 14:22:21.974 INFO [1167792448:processing] - <test.MyPlugin> A string that's logged at INFO

It is recommended to put “plugins.” at the beginning of the category name (as shown in the above example).

If your plug-in uses a third-party library that logs with SLF4J or Log4j 2, then the log output goes to the main correlator log file automatically.

When using a library which uses some other logging implementation, such as the JDK logger, or Apache Java commons logging (JCL), then add a bridging JAR to convert it to SLF4J where possible.

Samples

Apama provides sample EPL plug-ins written in Java, located in the correlator_plugin/java directory of the Apama Samples repository. The samples are:

  • SimplePlugin – a basic plug-in with one method that takes a string, and returns another string.
  • ComplexPlugin – a plug-in that has several methods and handles more complex types.
  • SendPlugin – a plug-in that demonstrates passing contexts around and sending events.
  • SubscribePlugin – a plug-in that shows how to subscribe to receive events sent on a particular channel.

The samples include Java code, EPL code for the Apama applications that call each of the plug-ins, and an Ant build.xml file for building all of the samples. See the README.md in that directory for more information.