User Guide


Table of Contents

1. Introduction

Sponge is a polyglot system that allows creating knowledge bases in several scripting languages.

For the purpose of clarity, examples in this chapter are written in Python (Jython) as one of the supported scripting languages and in Java. All examples written in Python may have equivalent ones written in any of the other supported script languages.

2. Architecture

The figure below presents the architecture of the Sponge system.

engine architecture
Figure 1. The system architecture

The Sponge engine consists of the following components.

Table 1. Engine components
Engine component Description

Configuration Manager

The module providing an access to a configuration.

Plugin Manager

The module that manages plugins.

Knowledge Base Manager

The module that for manages knowledge bases.

Event Scheduler

The scheduler of future events that are to be added into the Input Event Queue.

Input Event Queue

The input queue of events that are sent to Sponge. Events can get to this queue form different sources: plugins, Event Scheduler or knowledge bases.

Filter Processing Unit

The module providing the filtering of events. It is also a registry of enabled filters.

Main Event Queue

The queue of events that passed all filters and are to be processes by other event processors in the Main Processing Unit.

Main Processing Unit

The module that manages the processing of events by triggers, rules and correlators. It is also a registry of such event processors.

Output Event Queue

The queue of ignored events, i.e. events that haven’t been listened to by any trigger, rule or correlator. Events rejected by filters don’t go to the Output Event Queue. The default behavior is to log and forget ignored events.

Processor Manager

The module responsible for enabling and disabling processors, i.e. actions, filters, triggers, rules and correlators.

Action Manager

The registry of enabled actions.

Thread Pool Manager

The module responsible for thread pool management.

3. Configuration

Sponge can be configured:

  • in an XML configuration file,

  • using the Engine Builder API.

3.1. XML configuration file

In a general form an XML configuration file is built as in the following example:

Example XML configuration file
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">

    <!-- Properties configuration section -->
    <properties>
        <property name="sponge.home" system="true">.</property>
        <property name="server.name">sponge.openksavi.org</property>
        <property name="filterThreshold" variable="true">10</property>
    </properties>

    <!-- Engine configuration section -->
    <engine name="SampleSponge" label="Sample Sponge">
        <description>The sample Sponge engine.</description>
        <mainProcessingUnitThreadCount>6</mainProcessingUnitThreadCount>
    </engine>

    <!-- Knowledge bases configuration section -->
    <knowledgeBases>
        <knowledgeBase name="pythonKb" label="Python-based processors">
            <description>The Python-based knowledge base.</description>
            <file>kb_file_1.py</file>
            <file required="false">${sponge.home}/examples/script/py/kb_file_2.py</file>
            <file>${sponge.configDir}/kb_file_3.py</file>
        </knowledgeBase>
        <knowledgeBase name="kotlinKb" class="org.openksavi.sponge.kotlin.examples.HelloWorld" />
    </knowledgeBases>

    <!-- Plugins configuration section -->
    <plugins>
        <plugin name="connectionPlugin" class="org.openksavi.sponge.examples.ConnectionPlugin" label="Connection plugin">
            <description>The connection plugin provides the connection related code.</description>
            <configuration>
                <connection>
                    <name>Example connection</name>
                </connection>
            </configuration>
        </plugin>
    </plugins>
</sponge>

The specification of a configuration XML file is provided by the schema file config.xsd.

The configuration file is looked up using the default strategy provided by Apache Commons Configuration, e.g. first in the file system as a relative or absolute path and then in the classpath. If not found, the configuration file is looked up in the file system relative to the Sponge home directory.

3.1.1. Properties configuration

The properties configuration section (properties) allows setting configuration properties. Configuration properties may be used in other places in the configuration file. Moreover the properties can be used as Java system properties if the attribute system is set to true. Java system properties passed to the JVM take precedence over the ones defined in the properties configuration section of the configuration file. So, for example passing -Dsponge.home=/opt/sponge to the JVM will override the corresponding property configuration.

Properties can also be used as Sponge variables in the engine scope. In that case you have to set the attribute variable to true. The type of such variables is always String.

Table 2. Predefined properties
Property Description

sponge.home

The Sponge home directory. The Sponge home directory may also be provided as a Java system property sponge.home.

sponge.configDir

The configuration file directory. This property is read only. It may be null if there is no Sponge configuration file.

Properties may be loaded from an optional properties file located in the same directory as the configuration file, which name follows the convention <XML configuration file basename>.properties. This properties file is supposed to be encoded in UTF-8.

3.1.2. Engine configuration

The engine configuration section (<engine>) contains engine configuration parameters and an optional name of the Sponge engine. The most important parameters are mainProcessingUnitThreadCount, asyncEventSetProcessorExecutorThreadCount and eventQueueCapacity.

Table 3. Engine parameters
Parameter Description Default value

mainProcessingUnitThreadCount

The number of Main Processing Unit worker threads. You could increase this parameter if your knowledge bases require concurrent processing of many events by triggers, rules or correlators.

10

asyncEventSetProcessorExecutorThreadCount

The number of threads used by an event set processor thread pool (executor) for asynchronous processing of events (by rules and correlators). You could increase this parameter if your knowledge bases create many instances of asynchronous rules or correlators. In such case, for better performance, this parameter should be equal to or greater than mainProcessingUnitThreadCount.

10

eventQueueCapacity

The capacity (i.e. maximum size) of the Input Event Queue. Value -1 means infinity. You should set eventQueueCapacity to a reasonable value taking into account the amount and intensity of events your system needs to process, the typical event size, the number of threads in thread pools, etc. When the system is overloaded with events and this parameter is set to the value other than infinity, every attempt to send a new event will throw QueueFullException. However, when the system is overloaded and this parameter is to too high or infinite, you risk OutOfMemoryError.

-1

eventClonePolicy

Event clone policy (shallow or deep).

shallow

autoEnable

Auto-enable script-based processors.

true

durationThreadCount

The number of threads used by the event set processors duration thread pool (executor). The default implementation uses these threads only to send control events. In most cases there should be no need to change this parameter, because sending a new event is relatively fast.

2

eventSetProcessorDefaultSynchronous

The event set processor default synchronous flag. If this parameter is set to true then all rules and correlators that have no synchronous flag specified in their configuration would be assumed as synchronous. If an event set processor is synchronous it means that an event will be processed sequentially (in one thread) for all instances of this event set processor. If an event set processor is asynchronous then an event will be processed by the instances of this event set processor concurrently (in many threads). The default behavior is asynchronous. In most cases you wouldn’t need to change this parameter.

false

executorShutdownTimeout

The thread pool (executor) shutdown timeout (in milliseconds). You could, for example, increase this parameter to guarantee a graceful shutdown if event processors need more time to finish processing when the engine is shutting down. The actual shutting down of the entire engine may take longer than executorShutdownTimeout because this parameter is applied separately to several thread pools in the engine.

60000

3.1.3. Knowledge bases configuration

The knowledge bases configuration section (<knowledgeBases>) lists all script knowledge bases that are to be loaded into the engine.

Each <knowledgeBase> tag contains:

Table 4. Knowledge base configuration
Tag Type Description

name

Attribute

The name of the knowledge base.

label

Attribute

The knowledge base label.

type

Attribute

The type of the script knowledge base corresponding to the scripting language. Allowed values: python, ruby, groovy, javascript. The type is required only for knowledge bases that specify no files so their type can’t be inferred from the file extensions.

class

Attribute

The class of the non script knowledge base. In that case you don’t have to specify a type and you must not specify files. A knowledge base class should define a non-parameterized constructor.

description

Element

The description of the knowledge base.

file

Element

The filename of the knowledge base. A single knowledge base may use many files but all of them have to be written in one language.

The file element may have the following optional attributes.

  • charset - sets the file encoding.

  • required - if set to false, the non existing files are ignored. The default value is true so when the file doesn’t exist, the exception is thrown.

3.1.4. Plugins configuration

The plugins configuration section (<plugins>) contains plugin definitions (<plugin>) built as follows:

Table 5. Plugin configuration attributes
Tag Type Description

name

Attribute

The unique name of the plugin (mandatory). A text without white spaces and special symbols. Also used as a variable name in order to access a given plugin in the knowledge base.

label

Attribute

The plugin label.

class

Attribute

The name of the plugin class (Java class or a class defined in the scripting language in the script knowledge base (mandatory).

knowledgeBaseName

Attribute

The name of the knowledge base containing the class of the plugin (optional). If not set then the default Java-based knowledge base is used.

description

Element

The plugin description.

configuration

Element

The specific configuration of the plugin.

You may provide a custom plugin configuration section inside a <configuration> element. The contents of this plugin configuration depend on the given plugin implementation. Usually it would be a hierarchy of plugin specific sub tags.

3.2. Engine Builder API

The Engine Builder API is provided by DefaultSpongeEngine.builder() static method that returns the EngineBuilder instance. This API follows a builder design pattern.

Example configuration using the Engine Builder API
EchoPlugin plugin = new EchoPlugin();
plugin.setName("testPlugin");
plugin.setEcho("Echo text!");

SpongeEngine engine = DefaultSpongeEngine.builder()
        .systemProperty("sponge.home", "..")
        .property("test.property", "TEST")
        .plugin(plugin)
        .knowledgeBase("helloWorldKb", "examples/script/py/triggers_hello_world.py")
        .knowledgeBase(new TestKnowledgeBase())
        .build();

engine.getConfigurationManager().setMainProcessingUnitThreadCount(25);
engine.getConfigurationManager().setEventClonePolicy(EventClonePolicy.DEEP);

engine.startup();

The Engine Builder API provides the method config() to read an XML configuration file as well.

Example of using the XML configuration file in the Engine Builder API
SpongeEngine engine = DefaultSpongeEngine.builder().config("examples/core/engine_parameters.xml").build();
engine.startup();

The Engine Builder API preserves the load order of knowledge bases, including knowledge bases specified in the configuration file.

You may set engine parameters via ConfigurationManager but only after invoking build() and before starting up the engine.

4. Engine

4.1. Starting up

To startup the engine you should invoke the startup() method. After startup, the engine runs in the background (i.e. using threads other than the current one) until you shutdown it.

Example of starting up
SpongeEngine engine = DefaultSpongeEngine.builder().config("examples/script/py/triggers_hello_world.xml").build();
engine.startup();

4.2. Shutting down

When a Sponge instance is no longer needed it should be shut down by invoking shutdown() or requestShutdown() method. It instructs the engine to do some clean up, stop all managed threads, free resources, etc. The shutdown() uses the current thread to stop the engine. The requestShutdown() uses a new thread to stop the engine, thus allowing to shutdown the engine from the within, e.g. form an event processor.

Example of shutting down in Java
engine.shutdown();
Example of shutting down in a script language
class SomeTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("e1")
    def onRun(self, event):
        sponge.requestShutdown()

Shutting down doesn’t guarantee that all events sent to the engine will be processed. However, all events that have already been read from the Input Event Queue (by the Filter Processing Unit) will be fully processed by the engine, if the processing doesn’t exceed shutdown timeouts (specified by the executorShutdownTimeout configuration parameter). All newer events remaining in the Input Event Queue will not be processed at all.

5. Knowledge bases

A knowledge base is used mainly to define processors. A knowledge base may be written in one of the supported scripting languages. An alternative way of defining knowledge bases is to write them directly in Java or Kotlin. However, using a scripting knowledge base has advantages such as that a modified script knowledge base doesn’t need recompilation.

There is a global namespace for all processors, regardless of the knowledge base they are defined in. When there is more than one processor of the same name in the engine, only the last enabled one will be registered. However, you can’t enable a processor if an another one that has the same name and is of a different type has already been enabled.

Scripting knowledge bases are read by interpreters. For every knowledge base there is one instance of an interpreter.

5.1. Knowledge base file structure

Generally, script knowledge base files consist of a few parts:

  1. Import of modules and packages (from the scripting language or Java).

  2. Definitions of knowledge base processors (actions, filters, triggers, rules and correlators).

  3. Definitions of callback functions that will be invoked in particular situations.

5.2. Callback functions

Table 6. Callback functions
Function Description

onInit()

Called once on the initialization of a knowledge base after the knowledge base files have been read.

onBeforeLoad()

Called just before onLoad and also every time a knowledge base is reloaded. The implementation could for example register data types before scanning processors.

onLoad()

Called on the loading of a knowledge base and also every time a knowledge base is reloaded. Before invoking an onLoad callback method, the engine scans to auto-enable processors (if this functionality is turned on). You may manually disable some of the auto enabled processors in this callback function if necessary.

onAfterLoad()

Called just after onLoad and also every time a knowledge base is reloaded.

onStartup()

Called once after the startup of the engine (after onInit() and initial onLoad()).

boolean onRun()

Called just after an onStartup() callback function. If this function returns true for every knowledge base, then the engine will start its threads and perform an endless loop in order to process events. This is the default behavior. Otherwise, the engine will assume a run once mode and it will invoke a shutdown without starting an event processing. The latter option allows a user to, for example, just run a script and stop the engine. It could be useful when using a standalone application to perform simple, synchronized tasks. In case of scripting knowledge bases onRun() may return null which will be treated as if it returned true.

onShutdown()

Called once before the shutdown of the engine.

onBeforeReload()

Called before every reloading of the knowledge base.

onAfterReload()

Called after every reloading of the knowledge base.

Sponge follows a convention that the names of all callback functions and methods start with on, e.g. onStartup for a knowledge base or onConfigure for a processor.

You shouldn’t place more than one callback function that has the same name in the same knowledge base (event in different files of that knowledge base). If there is more than one callback function that has the same name in the same knowledge base only the last loaded function will be invoked. Furthermore it could depend on the specific scripting language.

When Sponge is starting, callback functions are invoked in the following order:

  1. executing all knowledge base files as scripts, i.e. executing the main body of the script files,

  2. onInit(),

  3. onBeforeLoad(),

  4. onLoad(),

  5. onAfterLoad(),

  6. onStartup(),

  7. onRun().

Before onStartup() is invoked you will not be able to send events or access plugins. That is because the engine hasn’t started fully yet.

When a knowledge base is reloaded, the callback functions are invoked in the following order:

  1. onBeforeReload() executed in the previous version of the reloaded knowledge base,

  2. executing all knowledge base files as scripts, i.e. executing the main body of the script files,

  3. onBeforeLoad() executed in the new version of the reloaded knowledge base,

  4. onLoad() executed in the new version of the reloaded knowledge base,

  5. onAfterLoad() executed in the new version of the reloaded knowledge base,

  6. onAfterReload() executed in the new version of the reloaded knowledge base.

5.3. Global variables

The following predefined global variables are available in all knowledge bases.

Table 7. Global variables
Global variable Description

sponge

The facade to the Sponge engine operations that provides methods to send events, manually enable or disable event processors etc. This variable represents an instance of a Java class implementing org.openksavi.sponge.kb.KnowledgeBaseEngineOperations.

For each plugin a global variable will be created. The name of this variable is the plugin name (i.e. the value configured as the plugin name attribute in the configuration).

5.4. User variables

A user variable could be defined in one of the two scopes:

  • the engine scope,

  • the knowledge base scope.

5.4.1. Engine scope

The engine scope variables could be accessed in any knowledge base.

The engine scope variable examples
sponge.setVariable("soundTheAlarm", AtomicBoolean(False))
sponge.getVariable("soundTheAlarm").set(True)
The engine scope is the same as a Sponge internal session scope. This is because currently there is only one session per a single Sponge engine instance.

5.4.2. Knowledge base scope

The knowledge base scope variables may be accessed only in the knowledge base they are defined in.

The knowledge base scope variable examples
hearbeatEventEntry = None

def onStartup():
    global hearbeatEventEntry
    hearbeatEventEntry = sponge.event("heartbeat").sendAfter(100, 1000)

5.5. Engine facade

Table 8. Important engine facade properties and methods
Property / Method Description

kb

The knowledge base to which belongs the script using this variable. This value represents an object of a Java class implementing org.openksavi.sponge.kb.KnowledgeBase (for script knowledge base it is org.openksavi.sponge.kb.ScriptKnowledgeBase).

interpreter

The knowledge base interpreter that has read the script using this variable. Generally it is an implementation of org.openksavi.sponge.kb.KnowledgeBaseInterpreter. In the case of a scripting knowledge base it returns an implementation of org.openksavi.sponge.kb.ScriptKnowledgeBaseInterpreter.

engine

The engine. This is the reference to the actual implementation of the SpongeEngine interface.

logger

The logger instance associated with the knowledge base. The name of this logger has the following format: sponge.kb.<language>.<knowledgeBaseName>.global, e.g. sponge.kb.python.kb1.global. Please note, that event processors and plugins have their own loggers (they are referenced as self.logger).

enable()

Enables the processor.

enableAll()

Enables processors.

disable()

Disables the processor.

disableAll()

Disables processors.

enableJava()

Enables the Java-based processor.

enableJavaAll()

Enables Java-based processors.

disableJava()

Disables the Java-based processor.

disableJavaAll()

Disables Java-based processors.

Object call(String actionName, List<Object> args)

Calls registered action with arguments.

ValueHolder<Object> callIfExists(String actionName, List<Object> args)

Calls the action if it exists. Returns the action result wrapped in a value holder or null if the action is not registered.

Map<String, ProvidedValue> provideActionArgs(String actionName, List<String> argNames, Map<String, Object> current)

Returns the provided values along with value sets of the action arguments.

shutdown()

Shuts down the engine using the current thread.

requestShutdown()

Shuts down the engine using another thread.

reload()

Reloads script-based knowledge bases.

requestReload()

Reloads script-based knowledge bases using another thread.

boolean removeEvent(EventSchedulerEntry entry)

Removes the scheduled event.

getPlugin(String name)

Returns the plugin that has the specified name. Throws exception if not found.

getPlugin(Class<T> cls, String name)

Returns the plugin that has the specified name and type. Throws exception if not found.

getPlugin(Class<T> cls)

Returns the plugin that has the specified type. Throws exception if not found.

EventDefinition event(String name)

Creates a new event definition.

EventDefinition event(String name, EventClonePolicy policy)

Creates a new event definition.

EventDefinition event(Event event)

Creates a new event definition.

boolean has<Processor>(String name)

A set of methods returning true if a processor named name is registered. The actual methods are: hasFilter, hasTrigger, hasRule, hasCorrelator, hasAction.

setVariable(String name, Object value)

Sets the engine scope variable.

Object getVariable(String name)

Returns the value of the engine scope variable. Throws exception if not found.

T getVariable(Class<T> cls, String name)

Returns the value of the engine scope variable. Throws exception if not found.

T getVariable(String name, T defaultValue)

Returns the value of the engine scope variable or defaultValue if not found.

T getVariable(Class<T> cls, String name, T defaultValue)

Returns the value of the engine scope variable or defaultValue if not found.

removeVariable(String name)

Removes the engine scope variable.

boolean hasVariable(String name)

Returns true if the engine scope variable named name is defined.

setVariableIfNone(String name, Supplier<T> supplier)

Sets the engine scope variable if not set already.

version

The read-only property whose value is the engine version.

description

The read-only property whose value is the engine description.

statisticsSummary

The read-only property whose value is the engine statistics summary as a text.

get<_Processor_>Meta(String processorName) A set of methods that return processor metadata.

void selectCategory(String categoryName, ProcessorType processorType, ProcessorPredicate predicate)

Dynamically selects processors that are to assigned to the category.

void selectCategory(String categoryName, ProcessorPredicate predicate)

Dynamically selects processors of supported types that are to assigned to the category.

void addEventType(String eventTypeName, RecordType dataType)

For a complete list of available methods see the EngineOperations and the KnowledgeBaseEngineOperations Javadoc.

5.6. Loading knowledge base files

The order of loading knowledge bases preserves the order specified in the configuration. Likewise the order of loading files of the same knowledge base preserves the order specified in the configuration.

A strategy of loading knowledge base files is provided by an instance of KnowledgeBaseFileProvider.

5.6.1. Default provider

The default provider is implemented by the DefaultKnowledgeBaseFileProvider.

Script knowledge base files are looked up in the file system as a relative or absolute path, then in the classpath, then in the file system relative to the XML configuration file parent directory and then in the file system relative to the Sponge home directory. A knowledge base filename may contain wildcards (for files only, not directories), according to the glob pattern.

Wildcards are not supported for classpath resources by default.

This default behavior can be changed by providing a custom implementation of KnowledgeBaseFileProvider and passing it to the setKnowledgeBaseFileProvider method of the engine.

5.6.2. Spring based provider

The Spring based provider is implemented by the SpringKnowledgeBaseFileProvider. It will be automatically set if the SpringEngineBuilder is used. It additionally supports:

  • the Spring classpath*: URL protocol for loading resources as well as other protocols supported by the Spring PathMatchingResourcePatternResolver,

  • the Sponge spar URL protocol for loading script knowledge base files from JAR archives located in a filesystem.

The spar protocol name is an acronym for *SP*onge *AR*chive. The spar protocol accepts wildcards both in an archive content path and in an archive file path itself.

Example of loading knowledge base files from a JAR archive using the spar protocol
SpongeEngine engine = SpringSpongeEngine.builder().knowledgeBase("kb", "spar:kb-archive.jar!/*.py").build();

5.7. Loading knowledge base from an additional file

Sponge gives the possibility to define a knowledge base in a few files. In order to do that, in the configuration file in the <engine> section you may define which files should be loaded by adding <file> tags to <knowledgeBase>. Additional files could also be loaded from a knowledge base level.

sponge.kb.load("triggers.py")
If the same name is used for a new processor, the previous definition will be replaced with the new one. However, this behavior could depend on the specific scripting language.

5.8. Reloading

Sometimes a situation may happen that there will be a need for a dynamic modification of processors, for example to add a new rule or modify an existing one. It is possible to do it without the need of shutting down and then starting the system again.

When variables are used in a knowledge base and you don’t want them to be changed after reloading of the knowledge base, you should place their definitions in onInit() callback functions rather than simply in the main script or in onLoad(). That is because the main script and onLoad() are always executed during reloading but onInit() function is not.

When reloading the system, the configuration file is not loaded again. If the changes in this file (e.g. registering a new plugin) are to be visible in the system, the only way is to restart.

When the Sponge engine is being reloaded, the previously defined processors will not be removed from the registry. When a processor definition has changed in the file being reloaded, it will be auto-enabled (i.e. registered) once more with the new definition. If auto-enable is off, then sponge.enable method must be invoked. In that case sponge.enable should be placed in the onLoad() callback function.

If auto-enable is on (this is the default setting), then all processors will be enabled after reloading, even processors that have been manually disabled before.

There is a limitation in reloading a knowledge base that defines event set processors (i.e. rules or correlators). When there are existing instances of event set processors, they will be dismissed.

Depending on the specific interactions and taking into account differences in the third-party implementations of scripting languages, reloading sometimes may lead to problems or side effects and it should be used carefully. For example if onLoad callback function definition is removed in the Python script file before reloading, the instance of this function that had been loaded before will still be present in the interpreter and will be invoked. That is because the scripts being reloaded run in the same interpreter instance.

5.9. Use of many knowledge base files

As mentioned before, Sponge provides the possibility to read a knowledge base from many files. Dividing a knowledge base into a few files allows in an easy way to separate some functionalities.

The order in which the files are loaded is important. The files will be loaded in such order in which they were placed in the configuration.

5.10. Synchronization of processes in a knowledge base

Sponge is a multi-threaded system. Sponge engine operations are thread-safe. However, attention should be paid that processors defined in a knowledge base access any shared resources in a thread-safe way. This could be achieved in various ways using Java or scripting language mechanisms.

5.11. Non script knowledge bases

Non script knowledge bases may be written in Java or Kotlin. Non script base processor classes follow the naming convention JAction, JTrigger, JKnowledgeBase etc for Java and KAction, KTrigger, KKnowledgeBase etc for Kotlin.

5.11.1. Java knowledge bases

Example of sending events from within a Java knowledge base
public class TestKnowledgeBase extends JKnowledgeBase { (1)

    public static class TestTrigger extends JTrigger { (2)

        @Override
        public void onConfigure() {
            withEvent("e1");
        }

        @Override
        public void onRun(Event event) {
            getLogger().debug("Run");
        }
    }

    @Override
    public void onStartup() {
        getSponge().event("e1").set("mark", 1).sendAfter(1000); (3)
    }
}
1 The definition of the Java-based knowledge base class.
2 The definition of the Java trigger.
3 Makes an event of type (name) e1 with an attribute mark set to 1 and schedules it to be sent after 1 second.
Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-core</artifactId>
    <version>1.12.0</version>
</dependency>

5.11.2. Kotlin knowledge bases

Kotlin-based knowledge bases are currently supported only as non script knowledge bases.

Example of a Kotlin knowledge base
class Filters : KKnowledgeBase() {

    class ColorFilter : KFilter() {
        override fun onConfigure() {
            withEvent("e1")
        }
        override fun onAccept(event: Event): Boolean {
            logger.debug("Received event {}", event)
            val color: String? = event.get("color", null)
            if (color == null || color != "blue") {
                logger.debug("rejected")
                return false
            } else {
                logger.debug("accepted")
                return true
            }
        }
    }

    class ColorTrigger : KTrigger() {
        override fun onConfigure() {
            withEvent("e1")
        }
        override fun onRun(event: Event) {
            logger.debug("Received event {}", event)
        }
    }

    override fun onStartup() {
        sponge.event("e1").send()
        sponge.event("e1").set("color", "red").send()
        sponge.event("e1").set("color", "blue").send()
    }
}

In Kotlin knowledge bases there is no global variable sponge. Instead you have to use the sponge property.

See more examples of Kotlin-based knowledge bases in the sponge-kotlin project.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-kotlin</artifactId>
    <version>1.12.0</version>
</dependency>

5.12. Scripting knowledge bases interoperability

There are some limitation in the interoperability between scripting knowledge bases:

  • You shouldn’t pass knowledge base interpreter scope variables from one knowledge base to an another. Even if they are written in the same scripting language. This is because each knowledge base has its own instance of an interpreter.

  • Data structures used for communicating between different knowledge bases should by rather Java types or simple types that would be handled smoothly by Java implementations of scripting languages. For example you shouldn’t use a script-based plugin in knowledge bases other than the one in which this plugin has been defined.

  • Using more than one knowledge base written in the same scripting language may, in certain situations, also cause problems, due to the internal implementations of scripting language interpreters.

5.13. Useful knowledge base commands

Make and send a new event.
sponge.event("alarm").set("severity", 10).send()
Print registered (i.e. enabled) triggers.
print sponge.engine.triggers
Print registered rule groups.
print sponge.engine.ruleGroups
Print instances of the first rule group.
print sponge.engine.ruleGroups[0].rules
Print registered correlator groups.
print sponge.engine.correlatorGroups
Shutdown using a new thread.
sponge.requestShutdown()
Print the engine statistics summary.
print sponge.engine.statisticsManager.summary

For more information see Sponge Javadoc.

5.14. Predefined knowledge base libraries

Sponge provides a few predefined script files that may be used as one of files in your compatible (i.e. written in the same language) knowledge bases. For example you may use the Jython library in your XML configuration file: <file>classpath*:org/openksavi/sponge/jython/jython_library.py</file>. The classpath* notation is available only for Spring aware engines and allows to use Ant style (*) specifications for directories and files.

5.15. Knowledge base versioning

You may specify a version number for a knowledge base as an integer. It could be useful for example to enforce version checking when calling actions via the REST API. You should set the version in the onLoad callback function. After editing the knowledge base file and before reloading the engine, you could increase the version number.

Example of setting the knowledge base version.
def onLoad():
    sponge.kb.version = 1

5.16. Naming conventions

The builder-style methods in the metadata classes follow the naming convention with<Property>, e.g. BinaryType().withMimeType("image/png").

5.17. Categories

Processors may be assigned to registered categories. Categories could be used in a client code to group processors independently of knowledge bases. This feature can be useful if you don’t want to group processor by knowledge bases which requires more resources because each knowledge base has its own interpreter.

A registered category can be manually assigned to a processor in the processor configuration callback method onConfigure or dynamically by selecting processors that are to assigned to the category. The latter option could assign for example all processors defined in several knowledge bases to a single category in one line. Dynamic selection overwrites manual assigning.

The dynamic selection of processors is supported only for actions, filters and triggers. It is not supported for event set processors.
Example of registering categories and dynamically selecting processor categories by knowledge bases.
def onInit():
    sponge.addCategories(CategoryMeta("basic").withLabel("Basic"), CategoryMeta("extra").withLabel("Extra"))

def onLoad():
    sponge.selectCategory("basic", lambda processor: processor.kb.name in ("demo", "engine"))
    sponge.selectCategory("extra", ProcessorType.ACTION, lambda processor: processor.kb.name in ("demoExtra"))
Example of manually assigning a processor to a category in a processor configuration.
class MyAction1(Action):
    def onConfigure(self):
        self.withLabel("MyAction 1").withCategory("myActions")
    def onCall(self, text):
        return None

5.18. Extending Java-based processors and plugins in non-Java knowledge bases

Processors and plugins defined in non-Java languages can extend respectively Java-based processors and Java-based plugins. However this functionality is limited in different scripting languages.

Table 9. Support for extending Java-based processors and plugins in non-Java knowledge bases
Entity Python

Ruby

Groovy

JavaScript

Kotlin

Action

Yes

Yes

Yes

No

Yes

Filter

Yes

Yes

Yes

No

Yes

Trigger

Yes

Yes

Yes

No

Yes

Rule

No

No

No

No

No

Correlator

Yes

Yes

Yes

No

Yes

Plugin

Yes

Yes

Yes

No

Yes

6. Data types

6.1. Supported data types

Data types are represented by instances of classes. The type classes are located in the org.openksavi.sponge.type package. Types are used for example as action arguments and result metadata.

Table 10. Basic type properties
Property Description

name

A type location name. It is required if a type is used to specify an action argument metadata. In that case a type name is a name of an action argument. It is also required if a type is used to specify a record type field. A type location name has to be set in a type constructor.

label

An optional type location label. For example an action argument label or a record field label.

description

An optional type location description.

annotated

A flag that tells if a value of this type is annotated, i.e. wrapped by an instance of AnnotatedValue. Defaults to false. An annotated value allows passing a value label, a value description and value features along with the value, e.g. AnnotatedValue(imageBytes).withLabel("Image1").withFeatures({"filename":imageFilename}), where the first property is the annotated value, the second is the label and the third is the features map that may be used in a client code.

format

An optional format.

defaultValue

An optional default value.

nullable

Tells if a value of this type can be null. The default is that a value must not be null, i.e. it is not nullable.

features (or feature)

Optional features as a map of names to values (as the Object type).

optional

A flag specifying if a value in a location corresponding to this type is optional. Defaults to false. It is used in action arguments.

provided

The provided type specification as an instance of the ProvidedMeta class. Defaults to null. A provided value has to be an instance of ProvidedValue.

A type properties should be set using the builder-style methods, e.g. StringType("id").withLabel("Identifier").

Table 11. Provided type specification properties
Property Description

value

A flag specifying if this type value is provided.

valueSet

Metadata specifying if a value set is provided. Defaults to null. A value set is a list of allowed values. This list of values may be limited (the default setting) or not limited. A limited set means that the type value can be chosen only from the value set. To configure a not limited value set use withValueSet(ValueSetMeta().withNotLimited()).

elementValueSet

A flag specifying if the list element value set is provided. Applicable only for list types. Defaults to false. Along with ListType.unique it can represent multichoice.

depends

A list of type names of name paths that this type depends on.

readOnly

A flag specifying if this type is read only. Defaults to false.

overwrite

A flag specifying if the provided value of this type should overwrite the value set in a client code. Defaults to false. This flag should be handled by a client code.

See the provided action arguments as a use case of provided types.

Table 12. Data types
Type Description

AnyType

An any type. It may be used in situations when type is not important.

BinaryType

A binary (byte array) type. Provides an optional property mimeType.

BooleanType

A boolean type.

DateTimeType

A date/time type. This type requires a DateTimeKind parameter, which is is an enumeration of DATE_TIME (a value of this type in Java has to be an instance of LocalDateTime), DATE_TIME_ZONE (an instance of ZonedDateTime), DATE (an instance of LocalDate), TIME (an instance of LocalTime), INSTANT (an instance of Instant). To ensure interoperability with other systems, defining a type format (especially for DATE and TIME) is recommended and in some cases required. A type format uses the ICU/JDK date/time pattern specification. The Sponge REST API uses a type format (if defined) to serialize a value of DateTimeType to JSON. The default date/time kind is DATE_TIME.

DynamicType

An dynamic type representing dynamically typed values. A value of this type has to be an instance of DynamicValue.

IntegerType

An integer type (commonly used integer type or long). Provides optional properties minValue, maxValue, exclusiveMin and exclusiveMax, e.g.: IntegerType().withMinValue(1).withMinValue(100).

ListType

A list type. This type requires a DataType parameter, which is is a type of list elements. Provides optional property unique specifying if the list should contain unique values (defaults to false). For example: ListType().withElement(ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject")).

MapType

A map type. This type requires two DataType parameters: a type of keys and a type of values in the map. For example: MapType().withKey(StringType()).withValue(ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject")).

NumberType

A number type, that include both integer and floating-point numbers. Provides optional properties minValue, maxValue, exclusiveMin and exclusiveMax, e.g.: NumberType().

ObjectType

An object. This type requires a class name (typically a Java class name). For example: ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject"). It also supports an array notation: ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject[]").

RecordType

A record type. This type requires a list of named record field types. A value of this type has to be an instance of Map with elements corresponding to the field names and values. E.g.: RecordType("book").withFields([StringType("author").withLabel("Author"), StringType("title").withLabel("Title")]). A record type supports inheritance.

StreamType

A stream type. Supports only output streams. It can be used only as a result of an action. A value of this type has to be an instance of OutputStreamValue.

StringType

A string type. Provides optional properties minLength and maxLength, e.g.: StringType().withMaxLength(10).

TypeType

A type representing a data type. A value of this type has to be an instance of DataType.

VoidType

A void type that may be used to specify that an action returns no result.

Type examples
StringType().withMaxLength(10).withFormat("ipAddress")
StringType("ip").withMaxLength(10).withFormat("ipAddress")
IntegerType().withMinValue(1).withMaxValue(100).withDefaultValue(50)
AnyType().withNullable(True)
ListType(StringType())
ListType(ObjectType().withClassName("java.math.BigDecimal"))
ObjectType().withClassName("java.lang.String[]")
ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject")
ListType(ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject"))
BinaryType().withMimeType("image/png").withFeatures({"width":28, "height":28, "color":"white"})
OutputStreamValue(lambda output: IOUtils.write("Sample text file\n", output, "UTF-8")).withContentType("text/plain; charset=\"UTF-8\"").withHeaders({})

6.2. Registering data types

A data type can be registered in the engine in order to be accessed later by its registered type name.

Table 13. Engine facade methods for registered types
Method Description

addType(String registeredTypeName, DataTypeSupplier<T> typeSupplier)

Registers a data type by providing a type supplier that will create a new instance of the registered type each time, e.g. sponge.addType("Author", lambda: RecordType([StringType("firstName").withLabel("First name"), StringType("surname").withLabel("Surname")])).

getType(String registeredTypeName)

Returns a new instance of the registered data type.

getType(String registeredTypeName, String locationName)

Returns a new instance of the registered data type setting the returned type location name as well.

Map<String, DataType> getTypes()

Returns the unmodifiable map of registered data types.

Data types should be registered in the onBeforeLoad, because it is invoked before scanning processors.

The registered data type example
def onBeforeLoad():
    sponge.addType("Author", lambda: RecordType([
                StringType("firstName").withLabel("First name"),
                StringType("surname").withLabel("Surname")
            ]))
    sponge.addType("Book", lambda: RecordType([
                sponge.getType("Author", "author").withLabel("Author"),
                StringType("title").withLabel("Title")
            ]))

class GetBookAuthorSurname(Action):
    def onConfigure(self):
        self.withLabel("Get a book author").withArg(sponge.getType("Book").withName("book")).withResult(sponge.getType("Author"))
    def onCall(self, book):
        return book["author"]["surname"]

6.3. Record type inheritance

A record type supports inheritance by setting its base type. Fields in a base type can’t be overwritten in a sub-type. Other record type properties are merged. Properties in a sub-type take precedence if they are not set to default values.

The record inheritance example
def onBeforeLoad():
    sponge.addType("Person", lambda: RecordType().withFields([
        StringType("firstName").withLabel("First name"),
        StringType("surname").withLabel("Surname")
    ]))
    sponge.addType("Citizen", lambda: RecordType().withBaseType(sponge.getType("Person")).withFields([
        StringType("country").withLabel("Country")
    ]))

7. Features

Features are represented by a map of names to values. They provide additional information to data types and processors. They are flexible in a sense that they are not a part of the static API. The predefined features are listed in the Features.java.

Features can be used in a client code to provide specialized behavior.

8. Events

Events are the basic element of processing in Sponge. They have properties such as id, name and send time. The name of an event is also the type of this event. All events than have the same name belong to the same type. Event names should follow Java naming conventions for variable names. Events may have any number of attributes. These attributes will be available, for example, in event processors.

Sponge supports only point-in-time events.

8.1. Properties and methods

Table 14. Event properties and methods
Property / Method Description

id

A property that is a global unique identifier of an event (String). This is a shortcut for getId()/setId(text) methods.

name

A read-only property that is the name (type) of an event. This is a shortcut for the getName() method. A name of an event shouldn’t be changed after the event has been created. A name must not be empty nor contain white spaces or reserved characters (:). You should also avoid using names that are regular expressions.

time

A property that is a send time of an event, i.e. a time of adding an event to the Input Event Queue. The time is represented as java.time.Instant. This is a shortcut for getTime()/setTime(instant) methods.

label

An optional event label.

description

An optional event description.

set(attributeName, value)

A method that allows setting an attribute of an event.

Object get(attributeName)

A method that returns a value of an attribute or throws IllegalArgumentException if it does’t exist.

Object get(attributeClass, attributeName)

A method that returns a value of an attribute (assuming it is an instance of attributeClass) or throws IllegalArgumentException if it does’t exist.

Object get(attributeName, defaultValue)

A method that returns a value of an attribute or defaultValue if it does’t exist.

boolean has(attributeName)

A method that checks if an event has an attribute.

all

A property that returns a map of all attributes. This is a shortcut for the getAll() method.

Event clone()

A method that clones an event.

Properties id and time are automatically set when adding an event to the Input Event Queue and there is no need for setting them manually.

8.2. Typical event processing

In order to process an event there must be an event processor listening to events of that type (the types of events are recognized by their names). So this steps should be taken:

  • Creating an event processor.

  • Enabling the event processor automatically or manually (by invoking a proper sponge.enable*() method).

  • Creating a new event instance and sending it to the system (e.g. sponge.event("alarm").set("location", "Building 1").send().

  • The event goes directly to the Input Event Queue or is scheduled to be inserted to the Input Event Queue later. Scheduling is performed by the Event Scheduler.

  • From this queue the events are taken by the Filter Processing Unit. The list of filters defined for this event type is taken and then each of them is invoked. If all filters accept the event, it will be put to the Main Event Queue in which it will await to be processed by other event processors.

  • Then the event is collected by the Main Processing Unit. The list of event processors listening to this type of events is selected and then each of them is given the event to process.

  • After processing by the Main Processing Unit the event goes to the Output Event Queue if and only if it hasn’t been processed (i.e. listened to) by any of event processors.

8.3. Event cloning

The event is cloned each time when the periodically generated events are sent to the Input Event Queue.

The standard implementation of events allows choosing the cloning policy shallow or deep. These policies differ in the way of cloning of events attributes. When using the former, the references to attributes are copied - each event processor works on the same attribute instances. The policy deep executes the procedure of deep cloning, so each next generated event will contain individual copies of the attributes.

8.4. Custom events

The default implementation of an event is the AttributeMapEvent class. However, Sponge allows to use custom event implementations of the Event interface.

8.5. System events

System events are sent automatically by the engine. An event sent in your code shouldn’t have the same name as any of the system events. Currently there is only one system event.

Table 15. System events
Event name Description

startup

The startup event will be sent as the first event when the engine is starting up.

8.5.1. Startup system event

The startup system event could be useful to define rules or correlators that detect lack of other events since the startup of the engine.

The following rule detects a situation when there is no heartbeat event for 5 seconds since the startup of Sponge.

Example of startup system event
class DetectLackOfHearbeat(Rule):
    def onConfigure(self):
        self.withEvents(["startup", "heartbeat :none"]).withDuration(Duration.ofSeconds(5))
    def onRun(self, event):
        print "No heartbeat!"

8.6. Control events

Control events are used by the engine internally. The names of control events have a prefix $. You shouldn’t give to your events a name that starts with this character.

8.7. Creating and sending events

Creating an event means creating an instance of an event class. Sending an event means that the created event will be put into the Input Event Queue, to be processed by filters and then by triggers, rules and correlators.

Event can be created and sent using the EventDefinition fluent API (e.g. sponge.event("helloEvent").set("say", "Hello World!").send()). The method sponge.event returns EventDefinition.

An event may be sent as:

  • A single instance – the event will be placed in the Input Event Queue only once.

  • Many instances periodically – new instances of an event will be placed in the Input Event Queue periodically, each of them with its own id and send time.

Table 16. EventDefinition methods
Method Description

EventDefinition set(String name, Object value)

Sets the event attribute.

EventDefinition modify(EventDefinitionModifier modifier)

Modifies the underlying event.

send()

Sends an event immediately.

sendAfter(delay)

Sends an event after a specified time (given in milliseconds or as a Duration). Note that the order of inserting events to the Input Event Queue may be different than the order of invocations of sendAfter, even with the same delay. It depends on the internal implementation of the Quartz library.

sendAfter(delay, interval)

Periodically sends events after a specified time (given in milliseconds or as a Duration) every interval (given in milliseconds or as a Duration).

sendAt(at)

Sends an event at the specified time (given in milliseconds as the number of milliseconds since 01/01/1970 or as an Instant).

sendAt(at, interval)

Periodically sends events starting at the specified time (given in milliseconds as the number of milliseconds since 01/01/1970 or as an Instant) every interval (given in milliseconds or as a Duration).

sendAt(crontabSpec)

Sends events at time specified by Cron compatible time entry.

sendEvery(interval)

Periodically sends events every interval (given in milliseconds or as a Duration).

Event make()

Only returns the newly created event without sending.

8.8. Examples of sending events

Sample sending of events from the level of a knowledge base:

sponge.event("e1").sendAfter(Duration.ofSeconds(1))

Sends the event named "e1" after 1 second from now.

sponge.event("e2").sendAfter(2000, 1000)

Sends the event named "e2" after 2 seconds from now. New events will be periodically generated and sent every second.

sponge.event("e2").set("color", "red").set("severity", 5).send()

Sends an event with attributes "color" and "severity" immediately.

sponge.event("alarm").sendAt("0-59 * * * * ?")

Sends an event at the time specified by Cron notation.

8.9. Registered event types

Event type may be registered in the engine. The registered event type is a RecordType that describes event attributes. The name of an event instance is also a registration name of a registered event type. Event types are are not verified by the engine but could be interpreted by a client code or Sponge plugins. For example they could be useful in a generic GUI that sends or subscribes to events.

Example of registering event types
def onBeforeLoad():
    sponge.addEventType("notification", RecordType().withFields([
        StringType("source").withLabel("Source"),
        IntegerType("severity").withLabel("Severity").withNullable()
    ]).withLabel("Notification"))

def onStartup():
    sponge.event("notification").set({"source":"Sponge", "severity":10}).label("The notification").description("The new event notification").send()

8.10. Event priorities

A priority may be assigned only to control events, that are used internally by the engine. For standard events the priority always equals to 0 and cannot be modified.

A priority defines a level of the importance of an event. Events are added to and taken from queues with respect to their priorities. Priority is a positive or negative integer and the higher the number is, the higher is the priority of an event and the event will be processed before the others.

9. Processors

Processors are the basic objects that you define in Sponge to implement your knowledge base behavior.

Types of processors:

  • Actions - processors that provide functionality similar to functions. They don’t listen to events.

  • Event processors - processors that perform specified operations using events they listen to.

    • Filters - event processors used for allowing only certain events to be later processed by other event processors.

    • Triggers - event processors that execute a specified code when an event happens.

    • Event set processors - event processors that process sets of events.

      • Rules - event set processors that detect sequences of events.

      • Correlators - event set processors that detect any set of events and could be also used for implementing any complex event processing that isn’t provided by filters, triggers or rules.

9.1. Creating processors

In order to define your processor in a script knowledge base, you have to create a class extending the base class pointed by a specific alias (e.g. Filter for filters). In order to define your processor in a Java knowledge base, you have to create a class extending a specific class (e.g. JFilter for filters).

A name of a processor is a name of a class defining this processor.

9.2. Enabling processors

The operation of registering a processor in the engine is called enabling. Registered processors are available to the engine to perform specific tasks. For example, after enabling an event processor starts listening to events it is interested in.

Processors could be enabled:

  • by auto-enable (this is the default setting for script-based processors),

  • manually.

9.2.1. Auto-enable

Sponge automatically enables all processors (i.e. actions, filters, triggers, rules and correlators) defined in a script knowledge base. This is done just before invoking the onLoad callback function in the knowledge base. Processor classes whose names start with the Abstract prefix are considered abstract and will not be automatically enabled.

As previously mentioned, the auto-enable feature scans only for processors defined in the scripting knowledge base. Enabling Java-based processors has to be done manually.

For non script knowledge bases (Java or Kotlin based) the auto-enable feature will scan only for processor classes nested in a corresponding knowledge base class. Other processors have to be enabled manually.

Example of processor inheritance and auto-enable
# This abstract action will not be automatically enabled.
class AbstractCalculateAction(Action):
    def calculateResult(self):
        return 1

# This action will be automatically enabled.
class CalculateAction(AbstractCalculateAction):
    def onCall(self):
        return self.calculateResult() * 2

You may turn off auto-enable by setting the autoEnable engine configuration parameter to false (for example in the Sponge XML configuration file). In that case you have to enable processors manually.

9.2.2. Manual enabling

In most cases enabling processors manually should be done in the onLoad callback function.

To manually enable any script-based processors in a script knowledge base you may use: sponge.enable() to enable one processor and sponge.enableAll() to enable many processors.

Example of enabling a script-based processor
def onLoad:
    sponge.enable(TriggerA)
Example of enabling script-based processors
def onLoad:
    sponge.enableAll(Trigger1, Trigger3)

To manually enable any Java-based processors in a script knowledge base you may use sponge.enableJava(), sponge.enableJavaAll() or sponge.enableJavaByScan(). The default name of a Java-based processor is its full Java class name.

The enableJavaByScan method enables Java-based processors by scanning a given packages in search of all non abstract processor classes. The scanning is performed by the Reflections library. The method parameters are compatible with the Reflections(Object…​) constructor.

Example of enabling one Java-based processor
def onLoad():
    sponge.enableJava(SameSourceJavaRule)
Example of enabling many Java-based processors
def onLoad():
    sponge.enableJavaAll(SameSourceJavaRule, SameSourceJavaRule2, SameSourceJavaRule3)
Example of enabling Java-based processors by scanning Java packages
def onLoad():
    sponge.enableJavaByScan("org.openksavi.sponge.integration.tests.core.scanning")

9.3. Disabling processors

Processors could be disabled only manually. To disable any script-based processors in a script knowledge base you may use sponge.disable() to disable one processor and sponge.disableAll() to disable many processors.

Example of disabling a script-based processor
def onLoad:
    sponge.disable(EchoAction)

To disable any Java-based processors in a script knowledge base you may use sponge.disableJava(), sponge.disableJavaAll() or sponge.disableJavaByScan().

Example of disabling a Java-based processor
def onLoad():
    sponge.disableJava(SameSourceJavaRule)

9.4. Properties and methods

Table 17. Processor properties and methods
Property / Method Description

onConfigure()

The configuration callback method that will be invoked when a processor is being enabled. This method is mandatory.

onInit()

The initialization callback method that will be invoked after onConfigure(), each time a new working instance of the processor is created.

withName(String name)

Sets a processor name. The name can be read using self.meta.name. Because names of processors are created automatically, this method shouldn’t be invoked directly.

withLabel(String label)

Sets a processor label. The label can be read using self.meta.label. A label is not used internally but could be useful in a client code.

withDescription(String description)

Sets a processor description. The description can be read using self.meta.description. A description is not used internally but could be useful in a client code.

withVersion(Integer version)

Sets a processor version. The version can be read using self.meta.version. A version is not used internally by the engine. However it is used to enforce version checking when calling actions via the REST API.

withFeatures(Map<String, Object> features)

Adds processor features. The features can be read using self.meta.features. The processor features is simply a map of String to Object associated with a processor definition (not instance). It could be used to provide a custom behavior in a client code.

withFeature(String name, Object value)

Adds a single processor feature.

withCategory(String category)

Sets a processor category. The category can be read using self.meta.category. A category is not used internally but could be useful in a client code.

meta

The read-only property that provides a processor metadata.

logger

The read-only property that provides a processor logger. This is a shortcut for getLogger() method. A processor logger name has the following format: sponge.kb.<language>.<knowledgeBaseName>.<processorName>, e.g. sponge.kb.python.kb1.EchoAction for a python-based processor, sponge.kb.python.kb1.org.openksavi.sponge.examples.PowerEchoAction for a Java-based processor enabled in a Python-based knowledge base.

adapter

The read-only property that provides a processor adapter. This is a shortcut for getAdapter() method. A processor adapter is an internal object, associated with the processor, that is used by the engine. There should be no need to use this property in the client code.

Processors provide builder-style, fluent methods to set metadata properties, e.g. self.withLabel("Label").withDescription("Description"). In many scripting languages properties can be accessed using a dot notation rather than a direct method call. For example a processor metadata may be read using self.meta or self.getMeta().

10. Actions

Actions provide functionality similar to synchronous functions. They may be used in many knowledge bases that are written in different languages.

The alias for the base class for script-based actions is Action. The base class for Java-based actions is JAction.

10.1. Properties and methods

In addition to the inherited processor properties and methods, actions provide the following ones.

Table 18. Action properties and methods
Property / Method Description

withArgs(List<DataType> argTypes)

Sets action arguments metadata as data types. The action argument types can be read using self.meta.args. Each argument data type must have a name. The same name should be used as a name of a corresponding argument in an onCall method. Arguments that have the optional flag set in their type are optional. Optional arguments can be specified only as last in the argument list and are not required to be passed when calling an action.

withArg(DataType argType)

Sets a single action argument metadata as a type.

withNoArgs()

Defines that an action has no arguments.

withResult(DataType resultType)

Sets an action result type. The action result type can be read using self.meta.result.

withNoResult()

Defines that an action has no result, i.e. it’s result is of VoidType.

withCallable(boolean callable)

Sets a callable flag for the action. A callable action must have an onCall method defined. Defaults to true. The reason an action would be not callable is if it is used with provided arguments.

Object onCall(dynamic arguments)

A dynamic callback method that should be defined in a callable action. It will be invoked when an action is called, e.g.: sponge.call(actionName, [argument list]). The behavior of this method is dynamic, i.e. custom actions define onCall methods with the arbitrary number of named arguments, for example def onCall(self, value, text). This is the reason that the Action interface doesn’t force any implementation of onCall. The result is an Object.

void onProvideArgs(ProvideArgsContext context)

A callback method that provides argument values along with argument value sets (i.e. possible values of an argument). The provided arguments are explained later in this document.

The onConfigure method in actions is not mandatory.

Example in a script language

The code presented below defines the action named EchoAction that simply returns all arguments.

Action example
class EchoAction(Action): (1)
    def onCall(self, text): (2)
        return text

def onStartup():
    result = sponge.call("EchoAction", ["test"]) (3)
    logger.debug("Action returned: {}", result)
1 The definition of the action EchoAction. The action is represented by the class of the same name.
2 The action onCall dynamic callback method that takes one argument text.
3 Calls the action named "EchoAction" passing one argument.
Console output
Action returned: test

Example in Java

The code presented below defines the Java-based action named JavaEchoAction.

Java action example
public class JavaEchoAction extends JAction { (1)

    @Override
    public Object onCall(String text) { (2)
        return text;
    }
}
1 The definition of the action JavaEchoAction. The action is represented by the Java class of the same name.
2 The action onCall callback method.
Java action manual registration in the Python knowledge base
sponge.enableJava(JavaEchoAction)

10.2. Arguments and result metadata

Actions may have metadata specified in the onConfigure method. Metadata may describe action arguments and a result. Metadata are not verified by the engine while performing an action call but could be interpreted by a client code or Sponge plugins. For example they could be useful in a generic GUI that calls Sponge actions. Metadata can be specified using the builder-style methods.

Metadata for arguments and a result are specified by types.

Action metadata example
class UpperCase(Action):
    def onConfigure(self):
        self.withLabel("Convert to upper case").withDescription("Converts a string to upper case.")
        self.withArg(
            StringType("text").withMaxLength(256).withLabel("Text to upper case").withDescription("The text that will be converted to upper case.")
        )
        self.withResult(StringType().withLabel("Upper case text"))
    def onCall(self, text):
        return text.upper()
Action metadata example with multiple arguments
class MultipleArgumentsAction(Action):
    def onConfigure(self):
        self.withLabel("Multiple arguments action").withDescription("Multiple arguments action.")
        self.withArgs([
            StringType("stringArg").withMaxLength(10).withFormat("ipAddress"),
            IntegerType("integerArg").withMinValue(1).withMaxValue(100).withDefaultValue(50),
            AnyType("anyArg").withNullable(),
            ListType("stringListArg", StringType()),
            ListType("decimalListArg", ObjectType().withClassName("java.math.BigDecimal")),
            ObjectType("stringArrayArg").withClassName("java.lang.String[]"),
            ObjectType("javaClassArg").withClassName("org.openksavi.sponge.examples.CustomObject"),
            ListType("javaClassListArg", ObjectType().withClassName("org.openksavi.sponge.examples.CustomObject")),
            BinaryType("binaryArg").withMimeType("image/png").withFeatures({"width":28, "height":28, "background":"black", "color":"white"}),
            TypeType("typeArg"),
            DynamicType("dynamicArg")
        ])
        self.withResult(BooleanType().withLabel("Boolean result"))
    def onCall(self, stringArg, integerArg, anyArg, stringListArg, decimalListArg, stringArrayArg, javaClassArg, javaClassListArg, binaryArg, typeArg, dynamicArg):
        return True
Action metadata using the fluent builder-style methods
class UpperEchoAction(Action):
    def onConfigure(self):
        self.withLabel("Echo Action").withDescription("Returns the upper case string").withArg(
            StringType("text").withLabel("Argument 1").withDescription("Argument 1 description")
        ).withResult(StringType().withLabel("Upper case string").withDescription("Result description"))
    def onCall(self, text):
        return self.meta.label + " returns: " + text.upper()

10.3. Provided arguments

An action argument can be provided, i.e. its value and possible value set may be computed and returned to a client code any time before calling an action. A provided argument gives more flexibility than the defaultValue in the argument data type. Nested values of action arguments can be provided as well. In that case both a type being provided and a dependency path have to be named and can’t contain collections (lists or maps) as intermediate path elements.

The onProvideArgs(ProvideArgsContext context) method is used to provide action argument values.

Table 19. ProvideArgsContext properties
Property Description

Set<String> names

A not null set of argument names (or name paths) that are to be provided. A name path is a dot-separated sequence of names of parent types, e.g. "book.author.surname".

Map<String, Object> current

The not null map of argument names (or name paths) and their current values passed from a client code. The map is required to contain values of those arguments that the arguments specified in the names depend on.

Map<String, ProvidedValue> provided

An initially empty map of argument names (or name paths) and their provided values (value sets) that is to be set up in an onProvideArgs callback method implementation.

This feature makes easier creating a generic UI for an action call that reads and presents the actual state of the entities that are to be changed or only viewed by the action and its arguments.

Example of an action with provided arguments
def onInit():
    sponge.setVariable("actuator1", "A")
    sponge.setVariable("actuator2", False)
    sponge.setVariable("actuator3", 1)
    sponge.setVariable("actuator4", 1)
    sponge.setVariable("actuator5", "X")

class SetActuator(Action):
    def onConfigure(self):
        self.withLabel("Set actuator").withDescription("Sets the actuator state.")
        self.withArgs([
            StringType("actuator1").withLabel("Actuator 1 state").withProvided(ProvidedMeta().withValue().withValueSet()),
            BooleanType("actuator2").withLabel("Actuator 2 state").withProvided(ProvidedMeta().withValue()),
            IntegerType("actuator3").withNullable().withLabel("Actuator 3 state").withProvided(ProvidedMeta().withValue().withReadOnly()),
            IntegerType("actuator4").withLabel("Actuator 4 state"),
            StringType("actuator5").withLabel("Actuator 5 state").withProvided(ProvidedMeta().withValue().withValueSet().withDependency("actuator1"))
        ]).withNoResult()
    def onCall(self, actuator1, actuator2, actuator3, actuator4, actuator5):
        sponge.setVariable("actuator1", actuator1)
        sponge.setVariable("actuator2", actuator2)
        # actuator3 is read only in this action.
        sponge.setVariable("actuator4", actuator4)
        sponge.setVariable("actuator5", actuator5)
    def onProvideArgs(self, context):
        if "actuator1" in context.names:
            context.provided["actuator1"] = ProvidedValue().withValue(sponge.getVariable("actuator1", None)).withAnnotatedValueSet(
                [AnnotatedValue("A").withLabel("Value A"), AnnotatedValue("B").withLabel("Value B"), AnnotatedValue("C").withLabel("Value C")])
        if "actuator2" in context.names:
            context.provided["actuator2"] = ProvidedValue().withValue(sponge.getVariable("actuator2", None))
        if "actuator3" in context.names:
            context.provided["actuator3"] = ProvidedValue().withValue(sponge.getVariable("actuator3", None))
        if "actuator5" in context.names:
            context.provided["actuator5"] = ProvidedValue().withValue(sponge.getVariable("actuator5", None)).withValueSet([
                "X", "Y", "Z", context.current["actuator1"]])

def onStartup():
    sponge.logger.debug("The provided value of actuator1 is: {}", sponge.provideActionArgs("SetActuator", ["actuator1"])["actuator1"].getValue())
Console output
The provided value of actuator1 is: A

A provided argument can be readOnly. In that case its value in the onCall method should be ignored. A read only argument type has to be nullable.

A provided argument can depend on other arguments but only those that are specified earlier. In the example argument actuator5 depends on actuator1. Its possible value set contains the value of actuator1.

Arguments configured as provided have to be calculated in the onProvideArgs callback method and set in the provided map. For each provided argument its value and possible value set can be produced as the instance of the ArgValue class. The optional withValue method sets the provided value. The optional withAnnotatedValueSet method sets the value set along with annotations (e.g. labels) where each element is an instance of the AnnotatedValue class. The optional withValueSet method sets the possible value set with no annotations.

The parameter names in the onProvideArgs is a set of argument names that are to be provided. The current parameter is a not null map of argument names and their current values passed from a client code. The current value means the value used in a client code, for example entered by a user into an UI before calling the action. This map is required to contain values of those arguments that the arguments specified in the names depend on.

10.4. Implementing interfaces

Actions may implement additional Java interfaces. It could be used to provide custom behavior of actions.

Action implementing a Java interface
from org.openksavi.sponge.integration.tests.core import TestActionVisibiliy

class EdvancedAction(Action, TestActionVisibiliy): (1)
    def onCall(self, text):
        return text.upper()
    def isVisible(self, context):
        return context == "day"
1 The Java interface TestActionVisibiliy declares only one method boolean isVisible(Object context).

11. Event processors

Event processors are processors that perform asynchronous operations using events they listen to.

Instances of event processors, depending on their type, may be created:

  • only once, while enabling, so they are treated as singletons,

  • many times.

Table 20. Event processors
Event processor Singleton

Filter

Yes

Trigger

Yes

Rule

No

Correlator

No

Filters and triggers are singletons, i.e. there is only one instance of one processor in the engine. However there can be many instances of one rule or one correlator in the engine.

When configuring an event processor, each event name can be specified as a regular expression thus creating a pattern matching more event names. The regular expression has to be compatible with java.util.regex.Pattern.

Event name pattern example
class TriggerA(Trigger):
    def onConfigure(self):
        self.withEvent("a.*") (1)
    def onRun(self, event):
        self.logger.debug("Received event: {}", event.name)
1 The trigger will listen to all events whose name starts with "a", as specified by the regular expression.

Event processors shouldn’t implement infinite loops in their callback methods because it would at least disrupt the shutdown procedure. If you must create such a loop, please use for example while sponge.engine.isRunning(): rather than while True:.

11.1. Filters

Filters allow only certain events to be processed by the engine. Filters are executed in the same order as the order of their registration (i.e. enabling).

You could modify event attributes in filters if necessary.

The alias for the base class for script-based filters is Filter. The base class for Java-based filters is JFilter.

11.1.1. Properties and methods

In addition to the inherited processor properties and methods, filters provide the following ones.

Table 21. Filter properties and methods
Property / Method Description

withEvents(List<String> eventNames) or withEvent(String eventName)

Sets a name (a name pattern) or names (name patterns) of filtered events. The event names can be read using self.meta.eventNames. Setting an event or events is mandatory. It should be set in the onConfigure callback method. You may use only one of these methods in a processor.

boolean onAccept(event)

This method checks if an incoming event should be further processed. If onAccept method returns false, then the event will be discarded. Otherwise it will be processed by the other event processors. This method is mandatory.

Every filter should implement abstract onConfigure and onAccept methods.

Example in a script language

The code presented below creates a filter which filters only events whose name is "e1". Other events are not processed by this filter. Events e1 successfully pass through the filter only if they have an attribute "color" set to the value "blue". The others are rejected.

Class methods defined in a Python class have an instance object (self) as the first parameter.
Filter example
class ColorFilter(Filter): (1)
    def onConfigure(self): (2)
        self.withEvent("e1") (3)
    def onAccept(self, event): (4)
        self.logger.debug("Received event {}", event) (5)
        color = event.get("color", None) (6)
        if (color is None or color != "blue"): (7)
            self.logger.debug("rejected")
            return False
        else: (8)
            self.logger.debug("accepted")
            return True
1 The definition of the filter ColorFilter. The filter is represented by the class of the same name.
2 The filter configuration callback method.
3 Sets up ColorFilter to listen to e1 events (i.e. events named "e1").
4 The filter onAccept method will be called when an event e1 happens. The event argument specifies that event instance.
5 Logs the event.
6 Assigns the value of the event attribute "color" to the local variable color.
7 If color is not set or is not "blue" then rejects that event by returning false.
8 Otherwise accepts the event by returning true.

The filter ColorFilter will be enabled automatically. The enabling creates one instance of ColorFilter class and invokes ColorFilter.onConfigure method to set it up. Since that moment the filter listens to the specified events.

Example in Java

The filter presented below checks if an event named "e1" or "e2" or "e3" has an attribute "shape" set. If not, an event is ignored and will not be processed further.

Java filter example
public class ShapeFilter extends JFilter { (1)

    @Override
    public void onConfigure() { (2)
        withEvents("e1", "e2", "e3"); (3)
    }

    @Override
    public boolean onAccept(Event event) { (4)
        String shape = event.get("shape", String.class); (5)
        if (shape == null) {
            getLogger().debug("No shape for event: {}; event rejected", event);
            return false; (6)
        }

        getLogger().debug("Shape is set in event {}; event accepted", event);

        return true; (7)
    }
}
1 The definition of the filter ShapeFilter. The filter is represented by the Java class of the same name.
2 The filter configuration callback method.
3 Sets up ShapeFilter to listen to e1, e2 and e3 events. Java-based filters have a convenience method that accepts varargs.
4 The filter onAccept method will be called when any of these events happen. The event argument specifies that event instance.
5 Assigns a value of an event attribute "shape" to the local variable shape.
6 If shape is not set then rejects that event by returning false.
7 Otherwise accepts the event by returning true.

This Java-based filter can be enabled only manually, for example in a script knowledge base e.g.:

Enabling a Java-based filter
sponge.enableJava(ShapeFilter)

11.2. Triggers

Triggers run a specified code when an event happens.

The alias for the base class for script-based triggers is Trigger. The base class for Java-based filters is JTrigger.

11.2.1. Properties and methods

In addition to the inherited processor properties and methods, triggers provide the following ones.

Table 22. Trigger properties and methods
Property / Method Description

withEvents(List<String> eventNames) or withEvent(String eventName)

Sets a name (a name pattern) or names (name patterns) of the events that cause this trigger to fire. The event names can be read using self.meta.eventNames. Setting an event or events is mandatory. It should be set in the onConfigure callback method. You may use only one of these methods in a processor.

onRun(event)

The callback method used for processing the event, called when the specified event (or one of the events) happens. This method is mandatory.

boolean onAccept(event)

This optional callback method checks if an incoming event should processed by this trigger. The default implementation returns true.

Every trigger should implement abstract onConfigure and onRun methods.

Example in a script language

The code presented below defines the trigger named TriggerA listening to events named "a".

Trigger example
class TriggerA(Trigger): (1)
    def onConfigure(self): (2)
        self.withEvent("a") (3)
    def onRun(self, event): (4)
        self.logger.debug("Received event: {}", event.name) (5)
1 The definition of the trigger TriggerA. The trigger is represented by a class of the same name.
2 The trigger configuration callback method.
3 Sets up TriggerA to listen to a events (i.e. events that have name "a").
4 The trigger onRun method will be called when an event a happens. The event argument specifies that event instance.
5 Logs the event.

The trigger TriggerA will be enabled automatically. The enabling creates an instance of TriggerA class and invokes TriggerA.onConfigure method to set it up. Since that moment the trigger listens to the specified events.

Example in Java

The code presented below defines the trigger named SampleJavaTrigger listening to events named "e1".

Java trigger example
public class SampleJavaTrigger extends JTrigger { (1)

    @Override
    public void onConfigure() { (2)
        withEvent("e1"); (3)
    }

    @Override
    public void onRun(Event event) { (4)
        getLogger().debug("Received event {}", event); (5)
    }
}
1 The definition of the trigger SampleJavaTrigger. The trigger is represented by a Java class of the same name.
2 The trigger configuration callback method.
3 Sets up SampleJavaTrigger to listen to e1 events (i.e. events that have name "e1").
4 The trigger onRun method will be called when an event e1 happen. The event argument specifies that event instance.
5 Logs the event.
Java trigger manual registration in the script knowledge base
sponge.enableJava(SampleJavaTrigger)

11.3. Rules

Sometimes there is a need to perform certain actions when a sequence of events has happened, additionally fulfilling some conditions. To handle such event relationships (both temporal and logical), Sponge provides rules. It is important for the behavior of the rules that events that happened first must be sent first into the engine.

The alias for the base class for script-based rules is Rule. The base class for Java-based rules is JRule.

A rule group is a set of rule instances, each created automatically for every event that could be accepted as the first event of this rule.

11.3.1. Properties and methods

In addition to the inherited processor properties and methods, rules provide the following ones.

Table 23. Rule properties and methods
Property / Method Description

onConfigure()

The callback method that is invoked only once, when a rule is being enabled. In this method it should be established for what type of events the rule listens. Optionally event conditions for incoming events or rule duration could be set. This method is mandatory.

onInit()

The initialization callback method that is invoked while creating every new rule instance but after onConfigure.

withEvents(List<String> eventStringSpecs) or withEvent(String eventStringSpec)

Sets String-based specifications of events whose sequence causes the rule to fire. The complete event specifications can be read using self.meta.eventSpecs. Setting an event or events is mandatory. It should be set in the onConfigure callback method. You may use only one of these methods in a processor.

withDuration(Duration duration)

Sets a duration that may be used to set the time how long a rule lasts (represented as a Duration). The instance of a rule will be active only for a given period of time since the arrival of the first event. Until that time the instance of the rule will fire for each suitable event sequence that happens.

withSynchronous(Boolean synchronous)

Sets a synchronous flag for a rule. If a rule is synchronous it means that an event will be processed sequentially (in one thread) for all instances of this rule. If a rule is asynchronous then an event will be processed by the instances of this rule concurrently (in many threads). If the synchronous flag is not set then the default value as specified by eventSetProcessorDefaultSynchronous configuration parameter will be used. In most cases there should be no need to change this flag.

withConditions(String eventAlias, List<_event condition_> conditions)

Adds conditions for an event specified by an alias (or event name if aliases are not used). A condition is a method of this class or a closure/lambda that is invoked to verify that a new incoming event corresponds to this rule. The name of the condition method is irrelevant.

withCondition(String eventAlias, event condition condition)

Adds a single condition for an event.

withAllConditions(List<_event condition_> conditions)

Adds conditions for all events. This method must be invoked after the event specifications.

withAllCondition(event condition condition)

Adds a single condition for all events. This method must be invoked after the event specifications.

withEventSpecs(List<RuleEventSpec> eventSpecs) or withEventSpec(RuleEventSpec eventSpec)

Sets complete specifications of events whose sequence causes the rule to fire.

withOrdered(boolean ordered)

Sets a flag indicating that the rule should listen to ordered (ordered rule) or unordered (unordered rule) sequences of events. Defaults to true, i.e. the rule would listen to ordered sequences of events.

RuleEventSpec makeEventSpec(args) methods

Each makeEventSpec method creates a new event specification. The preferred way is to use String-based specifications of events in the rule configuration.

onRun(event)

The callback method invoked when a sequence of events specified by this rule has happened and all the conditions have been fulfilled. The argument event is the reference to the final event that caused this rule to fire. There could be many sequences of events fitting the rule definition. In order to access the events which fulfilled the conditions and caused the rule fire, the getEvent(eventAlias) method should be used. The onRun method is mandatory.

Event getEvent(String eventAlias)

Returns the instance of the event that already happened and that has a specified alias. This method may be used inside onRun method. If an event hasn’t happened yet, this method throws an exception. This method may return null only when an event that supposed not to happen didn’t occur as specified.

firstEvent

This property is a reference to the first event that has been accepted by this rule. It is a shortcut for the Event getFirstEvent() method. It could be used for example in event condition methods (including the one for the first event itself).

eventSequence

Returns a sequence of events that happened, as a list of event instances. The sequence may contain null values when an event that supposed not to happen didn’t occur as specified. This method may be used inside onRun method.

Every rule should implement the abstract onConfigure and onRun methods.

Because of rules are not singletons the onConfigure() method is invoked only once, while enabling the rule. So it should contain only basic configuration as stated before. The onInit() method must not contain such configuration because it is invoked every time the new instance of the rule is created.
A duration is relative to an internal clock of the engine, that is related to the time of events. When a duration timeout occurs, the engine sends a control event (DurationControlEvent) to the Input Event Queue so that the control event, before finishing the rule, goes the same route as all events. This is to ensure that no events will be skipped by a rule if the system is highly loaded. Note that this may cause the rule to last longer in terms of an external clock.

11.3.2. Event specification

Event specification for the rule consists of:

Event name

A name (or name pattern) of the event (mandatory).

Event alias

An optional alias for the event. The alias is a unique (in the scope of the rule) name assigned to the event. Aliases are mandatory if there is more than one event of the same type (i.e. having the same name). When each of the events is of different type, there is no need to specify an alias. In such case aliases will be defined automatically and equal to the name of the corresponding event.

Event mode

Specifies which sequences of events suitable to this rule should be used for running the rule (i.e. invoking the onRun callback method). Event modes are defined in the EventMode Java enumeration.

Table 24. Rule event modes
Event mode Description

first

The first suitable event. This is the default event mode when none is specified for an event.

last

The last suitable event for the duration of the rule.

all

All suitable events for the duration of the rule.

none

An event that cannot happen in the sequence.

Event specification should be formatted as text "eventName [eventAlias [:eventMode"]] or "eventNamePattern [eventAlias [:eventMode"]]. White characters between all elements are allowed. For example the specifications "event1 e1 :first", "event1", "event1 e1" define the suitable first event named "event1". The specification "[Ee]vent.* e" define all events which name starts with "Event" or "event".

11.3.3. Ordered rules

For ordered rules:

  • The first event in the sequence, i.e. the event that would initiate the rule, must always have the mode first.

  • If the mode of the last (final) specified event is last or none, a duration must be set. Otherwise the rule would never fire.

The following examples of complete event specifications assume that the ordered rule has a duration that spans over all incoming events listed in the second column. The integer value in the brackets is the id of the event. An element null means that the event hasn’t happened. Incoming events: e1[1], e2[2], e2[3], e3[4], e2[5], e3[6], e3[7].

Table 25. Examples of ordered event specifications
Events specification Event sequences

["e1", "e2 :all", "e3 :all"]

[e1[1], e2[2], e3[4]], [e1[1], e2[3], e3[4]], [e1[1], e2[2], e3[6]], [e1[1], e2[3], e3[6]], [e1[1], e2[5], e3[6]], [e1[1], e2[2], e3[7]], [e1[1], e2[3], e3[7]], [e1[1], e2[5], e3[7]]

["e1", "e2 :all", "e3"]

[e1[1], e2[2], e3[4]], [e1[1], e2[3], e3[4]]

["e1", "e2 :all", "e3 :last"]

[e1[1], e2[2], e3[7]], [e1[1], e2[3], e3[7]], [e1[1], e2[5], e3[7]]

["e1", "e2 :all", "e4 :none"]

[e1[1], e2[2], null], [e1[1], e2[3], null], [e1[1], e2[5], null]

["e1", "e2", "e3 :all"]

[e1[1], e2[2], e3[4], [e1[1], e2[2], e3[6]], [e1[1], e2[2], e3[7]]

["e1", "e2", "e3"]

[e1[1], e2[2], e3[4]]

["e1", "e2", "e3 :last"]

[e1[1], e2[2], e3[7]]

["e1", "e2", "e4 :none"]

[e1[1], e2[2], null]

["e1", "e2 :last", "e3 :all"]

[e1[1], e2[3], e3[4]], [e1[1], e2[5], e3[6]], [e1[1], e2[5], e3[7]]

["e1", "e2 :last", "e3"]

[e1[1], e2[3], e3[4]]

["e1", "e2 :last", "e3 :last"]

[e1[1], e2[5], e3[7]]

["e1", "e2 :last", "e4 :none"]

[e1[1], e2[5], null]

["e1", "e4 :none", "e3 :all"]

[e1[1], null, e3[4]], [e1[1], null, e3[6]], [e1[1], null, e3[7]]

["e1", "e4 :none", "e3"]

[e1[1], null, e3[4]]

["e1", "e4 :none", "e3 :last"]

[e1[1], null, e3[7]]

["e1", "e2", "e3 :none"]

This rule hasn’t been fired because the event e3 wasn’t supposed to happen.

11.3.4. Unordered rules

Behavior:

  • The matching of unordered events is done starting from the left in the list of events the unordered rule listens to.

  • Every event that is relevant to the unordered rule causes a new instance of the rule to be created. This implicates that the event mode for an event that actually happens as the first is used by the engine only as a suggestion. So the actual order of events that happen has a significant impact on the behavior of unordered rules.

  • If at least one specified event has none mode, you probably should set a duration for such a rule to avoid superfluous instances of the rule.

Unordered rules should be treated as an experimental feature.

11.3.5. Event conditions

A rule may define conditions for events that have to be met to consider an incoming event as corresponding to the rule:

  • of the form of a any class method that takes one argument (Event) and returns boolean, e.g.:

    boolean conditionA(Event event);
    boolean check1(Event event);
  • as a closure or a lambda (depending on the language) that takes two arguments (Rule, Event) and returns boolean, e.g.:

    lambda rule, event: Duration.between(rule.getEvent("filesystemFailure").time, event.time).seconds > 2
  • as an instance of an implementation of the interface EventCondition (takes two arguments (Rule, Event) and returns boolean), e.g. as a Java lambda expression:

    (rule, event) -> {
        return true;
    };

An event condition in Java is represented by the interface EventCondition.

A condition in the form of a closure or a lambda specifies two arguments: a rule instance (determined at the runtime) and an event instance. Take care not to mix up the rule argument with this (in Java) or self (in Python) as they are references to different objects.

The condition methods tell if an incoming event (corresponding to the sequence of events specified by the rule) should be considered suitable.

Example in a script language

The code presented below defines a rule named SameSourceAllRule listening to an ordered sequence of events ("filesystemFailure", "diskFailure"). The two events have to have severity greater than 5 and the same source. Moreover the second event has to happen not later than after 4 seconds since the first one. The method onRun() will be invoked for every sequence of events that match this definition.

Rule example
class SameSourceAllRule(Rule): (1)
    def onConfigure(self): (2)
        # Events specified with aliases (e1 and e2)
        self.withEvents(["filesystemFailure e1", "diskFailure e2 :all"]) (3)
        self.withAllCondition(self.severityCondition) (4)
        self.withCondition("e2", self.diskFailureSourceCondition) (5)
        self.withDuration(Duration.ofSeconds(8)) (6)
    def onRun(self, event): (7)
        self.logger.info("Monitoring log [{}]: Critical failure in {}! Events: {}",
            event.time, event.get("source"), self.eventSequence) (8)
    def severityCondition(self, event): (9)
        return int(event.get("severity")) > 5 (10)
    def diskFailureSourceCondition(self, event): (11)
        event1 = self.getEvent("e1") (12)
        return event.get("source") == event1.get("source") and \
            Duration.between(event1.time, event.time).seconds <= 4 (13)
1 The definition of the rule SameSourceAllRule. The rule is represented by a class of the same name.
2 The rule configuration callback method.
3 Defines that the rule is supposed to wait for sequences of events "filesystemFailure" (alias "e1") and "diskFailure" (alias "e2") and take into consideration the first occurrence of "e1" event and all occurrences of "e2" event.
4 Sets the condition checking an event severity for all events.
5 Sets conditions checking "e2" event source.
6 Setting the duration of the rule. The duration must be set for this rule because the final event has all mode. The rule lasts for 8 seconds. So, for 8 seconds since the occurrence of the first matching e1 a tree of event instances will be constantly built with the root containing the instance of initial e1 event. Each matching e2 event will cause the rule to fire immediately for the current event sequence. After reaching the duration time this rule instance will be discarded.
7 The onRun method will be called when the proper sequence of events happens and all the conditions have been fulfilled. The event argument specifies the last event in that sequence.
8 Logs message and the sequence of events.
9 An event condition method severityCondition.
10 Accept only events that have severity greater than 5.
11 An event condition method diskFailureSourceCondition.
12 Assigns the first event (e1) to the local variable event1.
13 Accept e2 events that have the same source as the first event e1 and that happened not later than after 4 seconds since the corresponding e1 event.

The rule will be enabled automatically. Then, in case of occurrence of e1 event that has severity greater than 5, a new instance of a rule SameSourceAllRule will be created.

A condition could be expressed as a lambda function, for example:

self.withCondition("e1", lambda rule, event: int(event.get("severity")) > 5)

Example in Java

The code presented below defines a rule analogous to the one shown above but defined as a Java class.

Java rule example
public class SameSourceJavaRule extends JRule { (1)

    private static final Logger logger = LoggerFactory.getLogger(SameSourceJavaRule.class);

    @Override
    public void onConfigure() {
        withEventSpecs(makeEventSpec("filesystemFailure", "e1"), makeEventSpec("diskFailure", "e2", EventMode.ALL)); (2)
        withAllConditions("severityCondition"); (3)
        withConditions("e2", (rule, event) -> { (4)
            Event event1 = rule.getEvent("e1");
            return event.get("source").equals(event1.get("source")) &&
                    Duration.between(event1.getTime(), event.getTime()).getSeconds() <= 4;
        });
        withDuration(Duration.ofSeconds(8)));
    }

    @Override
    public void onRun(Event event) {
        logger.info("Monitoring log [{}]: Critical failure in {}! Events: {}", event.getTime(), event.get("source"),
                getEventAliasMap());
    }

    public boolean severityCondition(Event event) { (5)
        return event.get("severity", Number.class).intValue() > 5;
    }
}
1 The definition of the rule SameSourceAllRule. The rule is represented by a Java class of the same name.
2 The makeEventSpec method is used here to create event specifications instead of a formatted String. The same setting could be achieved by withEvents("filesystemFailure e1", "diskFailure e2 :all"). Java-based rules have convenience methods that accept varargs.
3 Sets the condition checking an event severity for all events.
4 Sets conditions checking "e2" event source (as a Java lambda expression).
5 An event condition method severityCondition.
Java rule manual registration in the Python knowledge base
sponge.enableJava(SameSourceJavaRule)

11.4. Correlators

Correlators could be viewed as a generalized form of rules. They detect correlations between events and could be used for implementing any complex event processing that isn’t provided by filters, triggers or rules.

Correlators listen to the specified events regardless of their order and provide manual processing of each such event. It means that they require more programming than the other processors, however provide more customized behavior. For example they need explicit stopping by calling the finish method. An instance of a correlator is created when the correlator accepts an incoming event as its first event.

A correlator instance, when started, may be finished:

  • manually by invoking the finish method from inside the onEvent method,

  • automatically when duration is set and the duration timeout takes place.

The alias for the base class for script-based correlators is Correlator. The base class for Java-based correlators is JCorrelator.

A correlator group is a set of instances of the correlator.

11.4.1. Properties and methods

In addition to the inherited processor properties and methods, correlators provide the following ones.

Table 26. Correlator properties and methods
Property / Method Description

onConfigure()

The configuration callback method that is invoked when the correlator is being enabled. In this method it should be established for what type of events this correlator listens. Optionally a correlator duration could be set. This method is mandatory.

withEvents(List<String> eventNames) or withEvent(String eventName)

Sets a name (a name pattern) or names (name patterns) of of events that this correlator listens to. The event names can be read using self.meta.eventNames. Setting an event or events is mandatory. It should be set in the onConfigure callback method. You may use only one of these methods in a processor.

withDuration(Duration duration)

Sets a time how long a correlator lasts (represented as a Duration). The instance of a correlator will be active only for a given period of time since the arrival of the first accepted as first event. After that time on the instance of this correlator the onDuration callback method will be invoked.

withSynchronous(Boolean synchronous)

Sets a synchronous flag for a correlator. For details see a description of this flag for rules.

withMaxInstances(int maxInstances)

Sets a maximum number of concurrent instances allowed for this correlator. If this value is not set, there will be no limit of concurrent instances. In that case you will probably need to implement onAcceptAsFirst() method.

withInstanceSynchronous(boolean instanceSynchronous)

Sets an instance synchronous flag. If true (the default value), one instance of the correlator will process only one event at a time. If false, one instance of the correlator will process many events concurrently. In that case the correlator has to be thread safe.

boolean onAcceptAsFirst(Event event)

Checks if the event should be accepted as the first event of a correlator, therefore starting a new working instance. The method onAcceptAsFirst is invoked after onConfigure. This method is optional. The default implementation returns true.

onInit()

The initialization callback method that is invoked while creating a new correlator instance but after onAcceptAsFirst if it returns true. This method is optional.

onEvent(Event event)

The callback method invoked when an event that a correlator listens to happens. This method is mandatory.

firstEvent

This property is a reference to the first event that has been accepted by this correlator. It is a shortcut for the Event getFirstEvent() method. It could be used for example in the onEvent callback method.

onDuration()

The callback method invoked when the duration timeout occurs. This method should be implemented if a duration timeout is set. After invoking this callback method, finish is invoked automatically.

finish()

The final method that should be invoked in onEvent(Event event) method when the correlator has done its work. Only by invoking finish this instance of the correlator is closed and its resources are released.

Every correlator may implement the onAcceptAsFirst method and should implement the abstract onEvent method. If a duration is set up, the onDuration callback method should be implemented as well.

Because of correlators are not singletons the onConfigure method is invoked only once while enabling the correlator. So it should contain only basic configuration as stated before. The onInit method must not contain such configuration because it is invoked later, every time a new instance of the correlator is created.

Example in a script language

The code presented below defines the correlator named SampleCorrelator that listens to events "filesystemFailure" and "diskFailure". The maximum number of concurrent instances allowed for this correlator is set to 1. A filesystemFailure event will be accepted as the first event only when there is no instance of this correlator already running. When the filesystemFailure event is accepted as the first, a new instance of this correlator will be created. Each instance of this correlator adds to its internal event log list eventLog any suitable event. When 4 fitting events are collected the correlator instance will finish.

Correlator example
class SampleCorrelator(Correlator): (1)
    def onConfigure(self): (2)
        self.withEvents(["filesystemFailure", "diskFailure"]) (3)
        self.withMaxInstances(1) (4)
    def onAcceptAsFirst(self, event): (5)
        return event.name == "filesystemFailure" (6)
    def onInit(self): (7)
        self.eventLog = [] (8)
    def onEvent(self, event): (9)
        self.eventLog.append(event) (10)
        self.logger.debug("{} - event: {}, log: {}", self.hashCode(), event.name, str(self.eventLog))
        if len(self.eventLog) == 4:
            self.finish() (11)
1 The definition of the correlator SampleCorrelator. The correlator is represented by a class of the same name.
2 The correlator configuration callback method.
3 Define that the correlator is supposed to listen to events "filesystemFailure" and "diskFailure" (in no particular order).
4 Sets the maximum number of concurrent instances.
5 The correlator onAcceptAsFirst callback method.
6 The correlator will accept as the first an event named filesystemFailure.
7 The correlator initialization callback method. It is invoked after onAcceptAsFirst.
8 Setting an initial value to the field eventLog.
9 The correlator onEvent callback method.
10 Adds a new event to eventLog.
11 This correlator instance will finish when 4 fitting events are collected into eventLog.

The correlator will be enabled automatically. Then, in case of acceptance of an event, a new instance of a correlator SampleCorrelator will be created.

Example in Java

The code presented below defines the correlator analogous to the one shown above but defined as a Java class.

Java correlator example
public class SampleJavaCorrelator extends JCorrelator { (1)

    private List<Event> eventLog;

    public void onConfigure() {
        withEvents("filesystemFailure", "diskFailure");
        withMaxInstances(1);
    }

    public boolean onAcceptAsFirst(Event event) {
        return event.getName().equals("filesystemFailure");
    }

    public void onInit() {
        eventLog = new ArrayList<>();
    }

    public void onEvent(Event event) {
        eventLog.add(event);
        getLogger().debug("{} - event: {}, log: {}", hashCode(), event.getName(), eventLog);
        if (eventLog.size() >= 4) {
            finish();
        }
    }
}
1 The definition of the correlator SampleJavaCorrelator. The correlator is represented by a Java class of the same name.
Java correlator manual registration in the Python knowledge base
sponge.enableJava(SampleJavaCorrelator)

12. Plugins

Plugins are used for expanding Sponge with new functionalities and use them in knowledge bases. Typically they provide access to and from external systems.

The alias for the base class for script-based plugins is Plugin. The base class for Java-based plugins is JPlugin.

Each of these base classes extends the BasePlugin class that provides empty implementations of callback methods. If the created plugin requires own configuration parameters (e.g. in the XML configuration file) the onConfigure method should be implemented.

Each plugin is also an engine module and that means that in inherits from the BaseEngineModule class.

Plugins could be written in Java or in a supported scripting language as a part of a scripting knowledge base. However plugins written in a scripting language must be used only in the same scripting knowledge base they were defined in. That is because there are limitations of scripting languages interoperation. Only plugins written in Java could be used in any scripting knowledge base.

12.1. Properties and methods

Table 27. Plugin properties and methods
Property / Method Description

name

The property that is a name of a plugin. This is a shortcut for getName()/setName(text) methods. Because of names of plugins are created automatically, the setter shouldn’t be used in a client code.

onConfigure(Configuration configuration)

The configuration callback method that will be invoked after a plugin has been loaded. This method allows reading an XML configuration for the plugin.

onInit()

The initialization callback method that will be invoked after a configuration of a plugin.

onStartup()

The callback method that will be invoked once after the startup of the engine.

onShutdown()

The callback method that will be invoked once before the shutdown of the engine.

onBeforeReload()

The callback method that will be invoked before every reloading of a knowledge base.

onAfterReload()

The callback method that will be invoked after every reloading of a knowledge base.

logger

The read-only property that provides a plugin logger. This is a shortcut for getLogger() method. A plugin logger name has the following format: sponge.kb.plugin.<pluginName>. Example logger name: sponge.kb.plugin.scriptPlugin.

Example in Java

Definition of the plugin in the XML configuration file
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">
    <knowledgeBases>
        <knowledgeBase name="sampleKnowledgeBase">
            <file>plugins_java.py</file>
        </knowledgeBase>
    </knowledgeBases>
    <plugins>
        <plugin name="echoPlugin" class="org.openksavi.sponge.examples.EchoPlugin">
            <configuration>
                <echo>Echo test!</echo>
                <count>2</count>
            </configuration>
        </plugin>
    </plugins>
</sponge>

This plugin definition section contains:

  • The unique name of the plugin. This name may be used in knowledge bases as a variable referencing this plugin instance.

  • The class name of the plugin. It could be a Java class name or a scripting language class name. If the plugin is defined in a scripting knowledge base than you must specify that knowledge base name as an XML tag <knowledgeBaseName>.

  • Custom configuration for a plugin. That section could be any XML that is understood by this plugin.

The above configuration defines a plugin implemented by org.openksavi.sponge.examples.EchoPlugin class. This plugin may be used in the knowledge base as a global variable named echoPlugin (according to the name attribute). There are additional configuration parameters defined for this plugin. These parameters could be read in the onConfigure() method of the plugin class, called before starting the plugin.

Java plugin example
public class EchoPlugin extends JPlugin { (1)

    private static final Logger logger = LoggerFactory.getLogger(EchoPlugin.class);

    private String echo = "noecho";

    private int count = 1;

    public EchoPlugin() {
    }

    @Override
    public void onConfigure(IConfiguration configuration) {  (2)
        echo = configuration.getString("echo", echo);
        count = configuration.getInteger("count", count);
    }

    @Override
    public void onInit() {  (3)
        logger.debug("Initializing {}", getName());
    }

    @Override
    public void onStartup() { (4)
        logger.debug("Starting up {}", getName());
    }

    public String getEcho() {
        return echo;
    }

    public void setEcho(String echo) {
        this.echo = echo;
    }

    public int getCount() {
        return count;
    }

    public String getEchoConfig() {
        return echo + " x " + count;
    }

    public void sendEchoEvent() {
        getSponge().event("echoEvent").set("echo", getEcho()).send();
    }
}
1 The definition of the plugin class.
2 The plugin configuration callback method.
3 The plugin initialization callback method.
4 The plugin startup callback method.

12.2. Using plugins

Using plugin in a script knowledge base
class PluginTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("e1")
    def onRun(self, event):
        self.logger.debug("Echo from the plugin: {}", echoPlugin.echo) (1)
1 Obtaining echo bean property from the plugin that is an instance of the class EchoPlugin.

An access to the plugin could be achieved in two ways:

  • directly using the name echoPlugin as any other scripting language variable (this is the preferred way),

  • by using the sponge API, e.g. plugin = sponge.getPlugin("echoPlugin").

Because echoPlugin implements the method getEcho(), you may invoke it in two ways:

  • sponge.getPlugin("echoPlugin").echo

  • echoPlugin.echo

Example in a script language

Definition of the plugin in the XML configuration file
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">
    <knowledgeBases>
        <knowledgeBase name="sampleKnowledgeBase">
            <file>plugins_kb.py</file>
        </knowledgeBase>
    </knowledgeBases>
    <plugins>
        <plugin name="scriptPlugin" class="ScriptPlugin" knowledgeBaseName="sampleKnowledgeBase">
            <configuration>
                <storedValue>Value A</storedValue>
            </configuration>
        </plugin>
    </plugins>
</sponge>
Scripting language plugin example
class ScriptPlugin(Plugin):
    def onConfigure(self, configuration):
        self.storedValue = configuration.getString("storedValue", "default")
    def onInit(self):
        self.logger.debug("Initializing {}", self.name)
    def onStartup(self):
        self.logger.debug("Starting up {}", self.name)
    def getStoredValue(self):
        return self.storedValue
    def setStoredValue(self, value):
        self.storedValue = value

12.3. Plugin life cycle

Sponge loads plugins when starting the system according to the steps:

  1. Creates the plugin class instance. The class must have a no-parameter constructor.

  2. Configures the plugin by invoking the method onConfigure().

  3. Initializes the plugin by invoking the method onInit().

  4. Invokes the callback method onStartup() when starting the engine.

  5. After starting all plugins the methods onStartup() defined in all knowledge bases are invoked.

  6. In case of reloading a knowledge base, the method onBeforeReload() of each plugin is invoked before the method onBeforeReload() of knowledge bases. Invoking the methods onAfterReload() goes in reverse (first the methods onAfterReload() of all knowledge bases and then the methods defined in plugins).

  7. Before Sponge shuts down, methods onShutdown() of all knowledge bases are invoked and then the method onShutdown() is invoked for each plugin.

13. Exception handling

Sponge introduces its own runtime exception defined as a Java class SpongeException. Exception handling in custom Java components (for example plugins) should follow standard Java conventions. Exception handling in scripting knowledge bases should follow standard conventions for the corresponding scripting language.

14. Embedding Sponge in custom applications

Sponge may be embedded in a custom Java application using a Maven dependency and the Engine Builder API.

14.1. Maven dependency

If you want to use Sponge with, for example, Python scripting knowledge bases, add this dependency to your pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-jython</artifactId>
    <version>1.12.0</version>
</dependency>

There is also a Bill Of Materials style maven artifact for Sponge. Example usage in your pom.xml:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.openksavi.sponge</groupId>
            <artifactId>sponge-bom</artifactId>
            <version>1.12.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

In that case you may omit the versions of the dependencies.

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-jython</artifactId>
</dependency>

15. Integration

15.1. Spring framework

Sponge engine can be configured as a Spring bean. That configuration provides standardized access to an embedded Sponge engine for example in J2EE environment.

To provide access to the Spring ApplicationContext in the knowledge base, the SpringPlugin instance should be created, configured as a Spring bean and added to the Sponge engine. The Spring plugin shouldn’t be defined in Sponge XML configuration file.

For more information see the SpringPlugin Javadoc.

Spring Java configuration example
@Configuration
public class TestConfig {

    @Bean
    public Engine spongeEngine() { (1)
        return SpringSpongeEngine.builder().plugin(springPlugin()).knowledgeBase("kb", "examples/spring/spring.py").build(); (2)
    }

    @Bean
    public SpringPlugin springPlugin() { (3)
        return new SpringPlugin();
    }

    @Bean
    public String testBean() {
        return BEAN_VALUE;
    }
}
1 The engine configured as the Spring bean. The SpringSpongeEngine implementation is used here in order to startup and shutdown the engine by Spring. DefaultSpongeEngine could also be used here but it wouldn’t provide automatic startup and shutdown.
2 Added SpringPlugin.
3 SpringPlugin configured as the Spring bean.
Python knowledge base
class SpringTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("springEvent")
    def onRun(self, event):
        beanValue = spring.context.getBean("testBean") (1)
        self.logger.debug("Bean value = {}", beanValue)
1 A Spring bean named "testBean" is acquired from the Spring ApplicationContext by using SpringPlugin instance referenced by the spring variable.

The SpringSpongeEngine starts up automatically (in the afterPropertiesSet Spring callback method) by default. However it can be configured not to start automatically by setting autoStartup to false.

SpringSpongeEngine not starting automatically example
@Bean
public SpongeEngine spongeEngine() {
    return SpringSpongeEngine.builder().autoStartup(false).plugin(springPlugin()).knowledgeBase("kb", "examples/spring/spring.py").build();
}

Maven configuration

Maven users will need to add the following dependency to their pom.xml for this component:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-spring</artifactId>
    <version>1.12.0</version>
</dependency>

15.2. Apache Camel

15.2.1. Sponge Camel component

The sponge component provides integration bridge between Apache Camel and the Sponge engine. It allows:

  • to route a body of a Camel message to the Sponge engine by converting it to a Sponge event (producer endpoint),

  • to route a message from a Sponge knowledge base to a Camel route (consumer endpoint).

Maven configuration

Maven users will need to add the following dependency to their pom.xml for this component:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-camel</artifactId>
    <version>1.12.0</version>
</dependency>

15.2.2. URI format

sponge:engineRef[?options]

Where engineRef represents the name of the SpongeEngine implementation instance located in the Camel registry.

15.2.3. Options

Name Default value Description

action

"CamelProducerAction"

Could be used only on the producer side of the route. It will synchronously call the Sponge action that has a name specified by the value of this option. However if there is the header named CamelSpongeAction in the Camel In message, it would override the value of this option.

managed

true

If set to true the Sponge engine will be started automatically when the endpoint starts and will be shut down when the endpoint stops.

15.2.4. Sponge support for Camel

15.2.4.1. CamelPlugin

CamelPlugin provides an interface to the Camel context so it may be used in a knowledge base.

CamelPlugin may be configured in three different ways.

  • Explicitly as a Spring bean and assigned to the engine using the Engine Builder API. This is the preferred way.

    Example
    @Configuration
    public class SpringConfiguration extends SpongeCamelConfiguration {
    
        @Bean
        public SpongeEngine spongeEngine() {
            return SpringSpongeEngine.builder()
                    .config("config.xml")
                    .plugin(camelPlugin())
                    .build();
        }
    }
  • Implicitly when creating a Sponge Camel endpoint.

  • Explicitly in the Sponge XML configuration file.

    Example
    <?xml version="1.0" encoding="UTF-8"?>
    <sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">
    
        <plugins>
            <!-- Note: don't change the plugin name. -->
            <plugin name="camel" class="org.openksavi.sponge.camel.CamelPlugin" />
        </plugins>
    </sponge>
If you use an implicit configuration and you get an error stating that camel variable is not defined, it signifies that a Camel context is not configured yet or Sponge engine is not used in any Camel route.

Only one CamelContext may be used with one instance of Sponge engine, bound by a single CamelPlugin.

Table 28. Important CamelPlugin properties and methods
Property / Method Description

emit(body)

Emits (sends) the body to all current consumers.

producerTemplate

The Camel ProducerTemplate for working with Camel and sending Camel messages.

sendBody(uri, body)

Sends the body to an endpoint. The shortcut for producerTemplate.sendBody(uri, body).

requestBody(uri, body)

Sends the body to an endpoint returning any result output body. The shortcut for producerTemplate.requestBody(uri, body).

getContext()

Returns a Camel context.

getConsumers()

Returns the current list of consumers.

For more information see the CamelPlugin Javadoc.

15.2.4.2. Spring-based support

SpongeCamelConfiguration provides base Camel and Sponge configuration using Spring Java configuration. Your Spring configuration could inherit from this class.

Spring bean named "spongeProducerTemplate" allows you to configure a Camel producer template used by CamelPlugin to send Camel messages. If none is present in a Spring configuration, then a default will be used.

Spring bean named springPlugin is the instance of SpringPlugin that could be registered in the engine and used in knowledge bases as the spring variable.

Spring bean named camelPlugin is the instance of CamelPlugin that could be registered in the engine and used in knowledge bases as the camel variable.

15.2.5. Producer

Using sponge component on the producer side of the route will forward a body of a Camel message to the specified Sponge engine.

Sponge in a producer mode could be placed in many routes in one Camel context.

Producer example - Spring configuration
@Configuration
public class ExampleConfiguration extends SpongeCamelConfiguration {

    @Bean
    public SpongeEngine spongeEngine() {
        // Use EngineBuilder API to create an engine. Also bind Spring and Camel plugins as beans manually.
        return SpringSpongeEngine.builder()
                .knowledgeBase("camelkb", "examples/camel/camel_producer.py")
                .plugins(springPlugin(), camelPlugin())
                .build();
    }

    @Bean
    public RouteBuilder exampleRoute() {
        return new RouteBuilder() {
            @Override
            public void configure() {
                from("direct:start").routeId("spongeProducer")
                    .to("sponge:spongeEngine");
            }
        };
    }
}
Python knowledge base camel_producer.py
class CamelTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("spongeProducer")
    def onRun(self, event):
        print event.body
Producer example - Sample code that sends a Camel message
// Starting a Spring context.
GenericApplicationContext context = new AnnotationConfigApplicationContext(ExampleConfiguration.class);
context.start();

// Sending a Camel message.
CamelContext camelContext = context.getBean(CamelContext.class);
ProducerTemplate producerTemplate = camelContext.createProducerTemplate();
producerTemplate.sendBody("direct:start", "Send me to the Sponge");
Output console
Send me to the Sponge
15.2.5.1. Camel producer action

Camel producer action will be invoked by Sponge synchronously when a Camel exchange comes to the Sponge engine. The result returned by this action is placed as the body of the Camel IN message. So it can be used by the next endpoint in the route if there is any.

To avoid any misconception please note that events in the Output Event Queue are not sent to the Camel route.
15.2.5.2. Default Camel producer action

The default Camel producer action is provided by a Java action CamelProducerAction. If the body of the Camel message is a Sponge event or event definition, than the event is sent to the Sponge immediately. Otherwise this action creates and sends a new event that encapsulates the body. The event is then returned, so it is placed as the body of the Camel In message. The default name of the new event is the name of the corresponding Camel route.

15.2.5.3. Custom Camel producer action

You could provide a custom implementation of a Camel producer action in two ways:

  • define your own implementation of CamelProducerAction in a knowledge base,

  • define an action in a knowledge base that takes an instance of Exchange as an argument and specify it in the producer endpoint URI or in the message header, e.g.:

    Python knowledge base
    class CustomAction(Action):
        def onCall(self, exchange):
            return "OK"
    Camel route that sets the action in the endpoint URI
    from("direct:start").routeId("spongeProducer")
            .to("sponge:spongeEngine?action=CustomAction")
            .log("Action result as a body: ${body}");
    Camel route that sets the action in the header
    from("direct:start").routeId("spongeProducer")
            .setHeader("CamelSpongeAction", constant("CustomAction"))
            .to("sponge:spongeEngine)
            .log("Action result as a body: ${body}");

15.2.6. Consumer

Using sponge component on the consumer side of the route will forward messages sent from the specified Sponge engine to a Camel route.

Sponge in a consumer mode could be placed in many routes in one Camel context.

Consumer example - Spring configuration
@Configuration
public class ExampleConfiguration extends SpongeCamelConfiguration {

    @Bean
    public SpongeEngine spongeEngine() {
        // Use EngineBuilder API to create an engine. Also bind Spring and Camel plugins as beans manually.
        return SpringSpongeEngine.builder()
                .knowledgeBase("camelkb", "examples/camel/camel_consumer.py")
                .plugins(springPlugin(), camelPlugin())
                .build();
    }

    @Bean
    public RouteBuilder exampleRoute() {
        return new RouteBuilder() {

            @Override
            public void configure() {
                from("sponge:spongeEngine").routeId("spongeConsumer")
                    .log("${body}")
                    .to("stream:out");
            }
        };
    }
}
Python knowledge base camel_simple_consumer.py
class CamelTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("spongeEvent")
    def onRun(self, event):
        camel.emit(event.get("message"))

    sponge.event("spongeEvent").set("message", "Send me to Camel")

The variable camel is a reference to the instance of CamelPlugin that is associated with the Camel context.

Output console
Send me to Camel

You may also send a message to the Camel endpoint directly, e.g.:

camel.sendBody("direct:log", event.get("message"))

This allows you, for example, to create a flexible message flow using Camel routes and Sponge as a dispatcher.

15.2.7. Routes in scripting languages

ScriptRouteBuilder class introduces fromS methods (meaning from Script) that delegate to the corresponding from methods in order to avoid using from since it could be a reserved keyword in scripting languages (e.g. in Python). So when defining Camel routes in Python you should use this class instead of standard RouteBuilder, e.g.:

from org.openksavi.sponge.camel import ScriptRouteBuilder

class PythonRoute(ScriptRouteBuilder):
    def configure(self):
        self.fromS("sponge:spongeEngine").routeId("spongeConsumerCamelPython") \
                .transform().simple("${body}") \
                .process(lambda exchange: sponge.getVariable("receivedRssCount").incrementAndGet()) \
                .to("stream:out")

def onStartup():
    camel.context.addRoutes(PythonRoute())

15.3. Sponge Remote API

The Sponge Remote API is a common name for the Sponge REST API and the Sponge gRPC API. The main remote API for Sponge is the REST API.

15.4. Sponge REST API server

The Sponge REST API provides users a remote access to the key Sponge functionalities. The REST API server plugin (RestApiServerPlugin) uses Apache Camel REST DSL in order to configure the JSON/REST service.

The default name of the REST API plugin (which can be used in knowledge bases) is restApiServer.

Table 29. Key REST API plugin configuration parameters
Name Type Description

autoStart

boolean

If true then the REST service will start when the plugin starts up. Defaults to true.

restComponentId

String

The Camel REST component id. Defaults to "jetty".

host

String

The REST API host.

port

int

The REST API port. Defaults to 1836.

prettyPrint

boolean

The pretty print option. Defaults to false.

publicActions

List<ProcessorQualifiedName>

Public actions.

publicEvents

List<String>

Public event names.

sslConfiguration

SslConfiguration

The SSL configuration.

publishReload

boolean

If true then the reload operation will be published. Defaults to false.

routeBuilderClass

String

The name of the class extending RestApiRouteBuilder (which is the default route builder).

apiServiceClass

String

The RestApiService implementation class name. Defaults to DefaultRestApiService.

securityServiceClass

String

The RestApiSecurityService implementation class name. Defaults to NoSecuritySecurityService.

authTokenServiceClass

String

The RestApiAuthTokenService implementation class name. The default implementation is JwtRestApiAuthTokenService that uses JSON Web Token (JWT) for Java JJWT library. Note that tokens used by the default implementation are signed but not encrypted.

authTokenExpirationDurationSeconds

Long

The duration (in seconds) after which an authentication token will expire. The value null or less than or equal to 0 means infinity. Defaults to 30 minutes.

The REST API server plugin XML configuration example
<sponge>
    <plugins>
        <plugin name="restApiServer" class="org.openksavi.sponge.restapi.server.RestApiServerPlugin">
            <configuration>
                <port>1836</port>
                <autoStart>false</autoStart>
            </configuration>
        </plugin>
    </plugins>
</sponge>
The REST API server plugin Java configuration example
@Configuration
public static class Config extends SpongeCamelConfiguration {

    @Bean
    public SpongeEngine spongeEngine() {
        return SpringSpongeEngine.builder().plugins(camelPlugin(), restApiPlugin())
                .config("sponge_config.xml").build();
    }

    @Bean
    public RestApiServerPlugin restApiPlugin() {
        RestApiServerPlugin plugin = new RestApiServerPlugin();
        plugin.setSecurityService(restApiSecurityService());

        return plugin;
    }

    @Bean
    public RestApiSecurityService restApiSecurityService() {
        return new SimpleInMemorySecurityService();
    }
}

For more information see the RestApiServerPlugin Javadoc.

The REST API plugin provides a simple predefined knowledge base libraries: engine_admin_library.py and engine_public_library.py.

Example use of the REST API predefined knowledge base library
<knowledgeBase name="security" label="Security">
    <file>classpath:org/openksavi/sponge/restapi/server/engine_admin_library.py</file>
</knowledgeBase>

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-rest-api-server</artifactId>
    <version>1.12.0</version>
</dependency>

Depending on the REST Camel component, you should add a corresponding dependency, e.g. camel-jetty for Jetty, camel-servlet for a generic servlet. For more information see the Camel documentation.

15.4.1. Operations summary

The following table contains a summary of the REST API operations. For a complete list of operations see the specification generated using Swagger and Swagger2Markup.

Table 30. The REST API operations summary
Name URI Description

Get the Sponge version

version

Returns the Sponge version.

Get the API features

features

Returns the API features.

Login

login

User login. Used in a token-based authentication scenario.

Logout

logout

User logout. Used in a token-based authentication scenario.

Get knowledge bases

knowledgeBases

Returns the knowledge bases which the user may use (i.e. may call actions registered in these knowledge basses).

Get actions

actions

Returns the metadata of actions that are available to the user. If you want to get metadata for specified actions, set the request property name to an action name or a Java-compatible regular expression. If you want to get only actions that have argument and result metadata specified in their configuration, set the request property metadataRequired to true (defaults to false). Actions will be sorted by a category sequence number, a knowledge base sequence number and an action label or name. The sequence number reflects the order in which categories or knowledge bases have been added to the engine. The optional request property registeredTypes is a flag for requesting registered types used in the actions in the result (defaults to false).

Call an action

call

Calls an action.

Provide action arguments

actionArgs

Returns provided arguments, i.e. values along with value sets of action arguments. The request accepts the following properties: name - the action name, argNames - the optional list of argument names that are to be provided (if null, all provided arguments will be produced), current - the optional map of argument names and their current values passed from a client code.

Send a new event

send

Sends a new event.

Get event types

eventTypes

Returns the registered event types.

Reload knowledge bases

reload

Reloads all knowledge bases. Depending on the configuration, this operation may not be published. It should be available only to administrators.

The OpenAPI specification of the REST API is included in the Appendix A of the Sponge Reference Documentation.

You can define a custom REST API operation (using the ActionDelegateRestApiOperation class in the route builder) that delegates a REST API request to an action call (e.g. to allow implementing an operation body in a scripting language but keeping a static REST interface).

15.4.2. OpenAPI specification

After starting the plugin, the online API specification in the OpenAPI 2.0 (Swagger) JSON format will be available, depending on the configuration, at URL http://localhost:1836/sponge.json/v1/doc.

The generated OpenAPI specification is currently limited. For example it doesn’t support inheritance e.g. for Sponge data types. Therefore it is most useful for simple customized operations.

15.4.3. JSON/Java mapping

The REST API uses the Jackson library to process JSON. A transformation of action arguments and result values is determined by types specified in the corresponding action arguments and result metadata.

The default Jackson configuration for the REST API sets the ISO8601 format for dates.

A BinaryType value is marshalled to a base64 encoded string. This encoding adds significant overhead and should be used only for relatively small binary data.

15.4.4. Request

Each request may contain base properties.

Table 31. Base request properties
Name Required Description

header

No

A request header.

Table 32. Request header properties
Name Required Description

id

No

A request identifier. If it is present, the response will contain a header with same id property with the same value. This feature provides some compatibility with the JSON-RPC protocol.

username

No

A user name that may be used in a user/password authentication mode. In that case, if there is no user name present, the anonumous user is assumed.

password

No

A user password that may be used in a user/password authentication mode.

authToken

No

An authentication token that may be used in a token-based authentication mode.

The Sponge REST API supports both POST and GET methods.

Examples of REST API requests
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/version
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/knowledgeBases
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/actions
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/actions -d '{"header":{"username":"john","password":"password"}}'
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/actions -d '{"name":".*Case"}'
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/call -d '{"name":"UpperCase","args":["test1"]}'
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/send -d '{"name":"alarm","attributes":{"a1":"test1","a2":"test2", "a3":4}}'
curl -i -k -X POST -H "Content-type:application/json" http://localhost:1836/sponge.json/v1/reload

curl http://localhost:1836/sponge.json/v1/version
curl http://localhost:1836/sponge.json/v1/knowledgeBases
curl -G "http://localhost:1836/sponge.json/v1/call" --data-urlencode 'request={"name":"OutputStreamResultAction","args":[]}'

15.4.5. Response

Each response may contain base properties.

Table 33. Base response properties
Name Required Description

header

No

A response header.

Table 34. Response header properties
Name Required Description

id

No

A corresponding request id.

errorCode

No

An optional error code in case of server side error.

errorMessage

No

An optional error message in case of server side error.

detailedErrorMessage

No

An optional detailed error message in case of server side error.

15.4.6. Session

For each request the REST API service creates a thread local session. The session provides access to a logged user and a Camel exchange for a thread handling the request. The session can be accessed in an action via the REST API server plugin.

Accessing the REST API session
class LowerCaseHello(Action):
    def onConfigure(self):
        self.withLabel("Hello with lower case")
        self.withArg(StringType("text").withLabel("Text to lower case")).withResult(StringType().withLabel("Lower case text"))
    def onCall(self, text):
        return "Hello " + restApiServer.session.user.name + ": " + text.lower()

15.4.7. API features

Table 35. Remote API features
Name Type Description

grpcEnabled

Boolean

Set to true if the optional Sponge gRPC API service is enabled.

15.4.8. Security

The REST API provides only simple security out of the box and only if turned on. All requests allow passing a user name and a password. If the user name is not set, the anonymous user is assumed. A user may have roles.

You may set a security strategy by providing an implementation of the RestApiSecurityService interface. You may find a few examples of such implementations in the source code. In production mode we suggest using Spring Security and configure Camel security. An advanced security configuration has to be set up in Java rather than in a Sponge XML configuration file. You may implement various authorization scenarios, for example using HTTP headers that are available in a Camel exchange.

15.4.8.1. Authentication mode

Only a username/password authentication mode is currently supported by the default REST API service implementation.

Table 36. Authentication modes
Name Description

Username/password

Every request has to contain a username and a password. Invoking the login operation switches to the authentication token mode.

Authentication token

Every request has to contain an authentication token, returned by the login operation. It may not contain neither username nor password.

15.4.8.2. Simple security strategy

The simple security strategy uses in-memory user data. User privileges and access to knowledge bases, actions and events are verified by calling Sponge actions (RemoteApiIsActionPublic, RemoteApiIsEventPublic, RemoteApiCanUseKnowledgeBase, RemoteApiCanSendEvent, RemoteApiCanSubscribeEvent). Passwords are stored as SHA-512 hashes.

Example of the Remote API simple security
from org.openksavi.sponge.restapi.server.security import User

# Simple access configuration: role -> knowledge base names regexps.
ROLES_TO_KB = { "admin":[".*"], "anonymous":["demo", "digits", "demoForms.*"]}
# Simple access configuration: role -> event names regexps.
ROLES_TO_SEND_EVENT = { "admin":[".*"], "anonymous":[]}
ROLES_TO_SUBSCRIBE_EVENT = { "admin":[".*"], "anonymous":["notification.*"]}

class RemoteApiCanUseKnowledgeBase(Action):
    def onCall(self, userContext, kbName):
        return restApiServer.canAccessResource(ROLES_TO_KB, userContext, kbName)

class RemoteApiCanSendEvent(Action):
    def onCall(self, userContext, eventName):
        return restApiServer.canAccessResource(ROLES_TO_SEND_EVENT, userContext, eventName)

class RemoteApiCanSubscribeEvent(Action):
    def onCall(self, userContext, eventName):
        return restApiServer.canAccessResource(ROLES_TO_SUBSCRIBE_EVENT, userContext, eventName)

def onStartup():
    # Setup users. To hash a password use (on Mac): echo -n username-password | shasum -a 512 | awk '{ print $1 }'
    # Note that the user name must be lower case.
    securityService = restApiServer.service.securityService
    securityService.addUser(User("john", "f4f28d85c27f6038bbdd2c8c73c4c2d2ca21350b368431b641999d3f6e1a38a474bae4b6856556532b30fc36a72272be4922ebe3d3b720ee3224b6bb7ced79b4", ["admin"]))

For more information see examples in the source code.

15.4.9. HTTPS

In production mode you should configure HTTPS, preferably using a signed certificate. Otherwise your passwords could be sent in plain text over the network as a part of the REST API JSON requests.

15.4.10. Environment

15.4.10.1. Standalone

This is the default configuration that uses the embedded Jetty server.

15.4.10.2. Servlet container

The Sponge REST API service may also be deployed into a servlet container (e.g. Tomcat) as a web application. See the REST API Demo Service example.

15.5. Sponge REST API client for Java

The Sponge REST API client for Java simplifies connecting to a remote Sponge REST API server from applications written in Java. The default implementation uses the OkHttp library. The REST API client uses POST methods.

The fully featured Sponge REST API client is the client for Dart. Clients for other languages may have less features. For more information check the client API.
REST API client for an anonymous user
try (SpongeRestClient client = new DefaultSpongeRestClient(SpongeRestClientConfiguration.builder()
        .url("http://localhost:8080/sponge.json/v1")
        .build())) { (1)
    String upperCaseText = client.call(String.class, "UpperCase",  Arrays.asList("text")); (2)
}
1 Create a new REST API client.
2 Call the remote action.
REST API client for a named user
SpongeRestClient client = new DefaultSpongeRestClient(SpongeRestClientConfiguration.builder()
        .url(String.format("http://localhost:%d/%s", PORT, RestApiConstants.DEFAULT_PATH))
        .username(username)
        .password(password)
        .build());

DefaultSpongeRestClient performs best when you create a single instance and reuse it for all of your REST API calls.

For more information see the DefaultSpongeRestClient Javadoc and examples in the source code.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-rest-api-client</artifactId>
    <version>1.12.0</version>
</dependency>

15.6. Sponge REST API client for Dart

The Sponge REST API client for Dart simplifies connecting to a remote Sponge REST API service from applications written in Dart. It could be used in a Flutter mobile application or an AngularDart web application to connect to a Sponge based back-end. The REST API client uses POST methods.

The fully featured Sponge REST API client is the client for Dart. Clients for other languages may have less features.
REST API client example
import 'package:sponge_client_dart/sponge_client_dart.dart';

void main() async {
  // Create a new client for an anonymous user.
  var client = SpongeRestClient(
      SpongeRestClientConfiguration('http://localhost:8888/sponge.json/v1'));

  // Get the Sponge server version.
  var version = await client.getVersion();
  print('Sponge version: $version.');

  // Get actions metadata.
  List<ActionMeta> actionsMeta = await client.getActions();
  print('Available action count: ${actionsMeta.length}.');

  // Call the action with arguments.
  String upperCaseText = await client.call('UpperCase', ['Text to upper case']);
  print('Upper case text: $upperCaseText.');

  // Send a new event to the Sponge engine.
  var eventId = await client.send('alarm',
      attributes: {'source': 'Dart client', 'message': 'Something happened'});
  print('Sent event id: $eventId.');

  // Create a new client for a named user.
  client = SpongeRestClient(
    SpongeRestClientConfiguration('http://localhost:8888/sponge.json/v1')
      ..username = 'john'
      ..password = 'password',
  );
}

Unless noted otherwise in the release notes, versions of the REST API client for Dart that have the same major.minor numbers as the Sponge service are compatible.

The REST API client for Dart is published as sponge_client_dart at pub.dartlang.org.

For more information see the SpongeRestClient Dartdoc and the project source code.

The example of using the REST API client for Dart in the AngularDart web application is hosted at https://github.com/softelnet/sponge_client_angular_dart_example.

15.7. Sponge gRPC API server

The Sponge gRPC API allows users to remotely subscribe to Sponge events. The gRPC API server plugin (GrpcApiServerPlugin) starts the gRPC[https://grpc.io] server. The gRPC API server plugin requires the REST API plugin because it reuses some parts of the configuration.

The gRPC has been chosen to provide event subscriptions because the main remote Sponge API, that is REST, has limited options for push notifications. The Sponge gRPC API can be seen as an addition to the Sponge REST API.

The default name of the gRPC API plugin (which can be used in knowledge bases) is grpcApiServer.

The plugin registeres the correlator (GrpcApiSubscribeCorrelator) in the Sponge engine that listens to all Sponge events and pushes them to subscribed clients. The client code can request that only events that have its data type registered will be pushed.

Events are pushed online, i.e. if a client subscribes to an event type, only events that come after that time will be delivered.
Table 37. Key gRPC API plugin configuration parameters
Name Type Description

autoStart

boolean

If true then the gRPC service will start when the plugin starts up. Defaults to true.

port

Integer

The gRPC API port. Defaults to null i.e. the default port convention will be used.

The gRPC API server plugin XML configuration example
<sponge>
    <plugins>
        <plugin name="grpcApiServer" class="org.openksavi.sponge.grpcapi.server.GrpcApiServerPlugin">
            <configuration>
                <autoStart>false</autoStart>
            </configuration>
        </plugin>
    </plugins>
</sponge>
The gRPC API server plugin Java configuration example
@Configuration
public static class Config extends SpongeCamelConfiguration {

    @Bean
    public SpongeEngine spongeEngine() {
        return SpringSpongeEngine.builder().plugins(camelPlugin(), restApiPlugin(), grpcApiPlugin())
                .config("sponge_config.xml").build();
    }

    @Bean
    public RestApiServerPlugin restApiPlugin() {
        return new RestApiServerPlugin();
    }

    @Bean
    public GrpcApiServerPlugin grpcApiPlugin() {
        return new GrpcApiServerPlugin();
    }
}

For more information see the GrpcApiServerPlugin Javadoc.

The gRPC API plugin provides support actions GrpcApiManageSubscription and GrpcApiSendEvent that can be called in the client code.

Example use of the gRPC API support actions
def onStartup():
    # Enable support actions in this knowledge base.
    grpcApiServer.enableSupport(sponge)

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-grpc-api-server</artifactId>
    <version>1.12.0</version>
</dependency>

15.7.1. Operations summary

The following table contains a summary of the gRPC API operations.

Table 38. The gRPC API operations summary
Name URI Description

Get the Sponge version

GetVersion

Returns the Sponge version.

Subscribe events

Subscribe

Subscribes to Sponge events and returns a stream of events.

15.7.2. Interface specification

The protobuf file for the gRPC API
syntax = "proto3";

import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";

option java_multiple_files = true;
option java_package = "org.openksavi.sponge.grpcapi.proto";
option java_outer_classname = "SpongeGrpcApiProto";
option objc_class_prefix = "SPG";

package org.openksavi.sponge.grpcapi;

// The Sponge gRPC API service definition.
service SpongeGrpcApi {
  rpc GetVersion (VersionRequest) returns (VersionResponse) {}

  rpc Subscribe (stream SubscribeRequest) returns (stream SubscribeResponse) {}
}

message ObjectValue {
  oneof value_oneof {
    // An empty json_value indicates a null.
    string value_json = 1;
    google.protobuf.Any value_any = 2;
  }
}

message Event {
  string id = 1;
  string name = 2;
  google.protobuf.Timestamp time = 3;
  int32 priority = 4;
  string label = 5;
  string description = 6;
  // Event attributes as a JSON string containing event aributes map corresponding a registered event record type.
  ObjectValue attributes = 7;
}

message RequestHeader {
   string id = 1;
   string username = 2;
   string password = 3;
   string auth_token = 4;
}

message ResponseHeader {
   string id = 1;
   string error_code = 2;
   string error_message = 3;
   string detailed_error_message = 4;
}

message VersionRequest {
  RequestHeader header = 1;
}

message VersionResponse {
  ResponseHeader header = 1;
  string version = 2;
}

message SubscribeRequest {
  RequestHeader header = 1;
  repeated string event_names = 2;
  bool registered_type_required = 3;
}

message SubscribeResponse {
  ResponseHeader header = 1;
  int64 subscription_id = 2;
  Event event = 3;
}

15.7.3. Configuration

The gRPC server port can be set by a Java system property sponge.grpc.port or by setting the port property of the plugin or by a convention. The convention is that the gRPC server port is the REST API port plus 1, e.g. if the REST API port is 8080 then the gRPC API port will be 8081.

15.7.4. Error handling

An application error is returned to a client in a response message, just like in the REST API. An internal error is returned as a gRPC exception with a status.

15.7.5. Security

If the REST API server is published as HTTPS, the gRPC server will be published as secure (TLS) using the same SSL/TLS configuration as the REST API.

Example of generating a PEM file from the JKS keystore used by the REST API
keytool -exportcert -rfc -file remote_api_selfsigned.pem -keystore remote_api_selfsigned.jks -alias rest_api -keypass sponge -storepass sponge

15.8. Sponge gRPC API client for Java

The Sponge gRPC API client for Java simplifies connecting to a remote Sponge gRPC API service from applications written in Java.

gRPC API client
// Create a new Sponge REST API client.
try (SpongeRestClient restClient = new DefaultSpongeRestClient(SpongeRestClientConfiguration.builder()
        .url("http://localhost:8080/sponge.json/v1")
        .build())) {
    // Create a new Sponge gRPC API client associated with the REST API client.
    // It is assumed that this gRPC service is insecure because the REST API is published as HTTP.
    try (SpongeGrpcClient grpcClient = new DefaultSpongeGrpcClient(restClient)) {
        // Get the Sponge version.
        String version = grpcClient.getVersion();
    }
}

The client follows the convention that a gRPC service port is a REST API port plus 1. If the gRPC service uses a different port, set this port in the client configuration.

Setting port in a gRPC API client
new DefaultSpongeGrpcClient(restClient, SpongeGrpcClientConfiguration.builder().port(9000).build())

For more information see the DefaultSpongeGrpcClient Javadoc and examples in the source code.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-grpc-api-client</artifactId>
    <version>1.12.0</version>
</dependency>

15.9. Sponge gRPC API client for Dart

The Sponge gRPC API client for Dart simplifies connecting to a remote Sponge gRPC API service from applications written in Dart.

gRPC API client example
import 'package:grpc/grpc.dart';
import 'package:sponge_client_dart/sponge_client_dart.dart';
import 'package:sponge_grpc_client_dart/sponge_grpc_client_dart.dart';

void main() async {
  // Create a new Sponge REST API client.
  var restClient = SpongeRestClient(
      SpongeRestClientConfiguration('http://localhost:8888/sponge.json/v1'));

  // Create a new Sponge gRPC API client associated with the REST API client.
  // Don't use insecure channel in production.
  var grpcClient = SpongeGrpcClient(restClient,
      channelOptions:
          ChannelOptions(credentials: const ChannelCredentials.insecure()));

  // Get the Sponge version.
  var version = await grpcClient.getVersion();

  print('Version: $version');

  // Close the client connection.
  await grpcClient.close();
}

The client follows the convention that a gRPC service port is a REST API port plus 1. If the gRPC service uses a different port, set this port in the client configuration.

Setting port in a gRPC API client
SpongeGrpcClient(restClient,
  configuration: SpongeGrpcClientConfiguration(port: 9000),
  channelOptions: ChannelOptions(credentials: const ChannelCredentials.insecure()));

This project uses the Dart implementation of gRPC and doesn’t support the web platform.

Unless noted otherwise in the release notes, versions of the gRPC API client for Dart that have the same major.minor numbers as the Sponge service are compatible.

The gRPC API client for Dart is published as sponge_grpc_client_dart at pub.dartlang.org.

15.10. Running external processes

Sponge provides the ProcessInstance API to run an external executable as a subprocess of the Sponge Java process. This feature is used by some of the plugins, for example by the Py4J integration plugin to execute an external Python script.

In general, an external process can be executed using:

  • Sponge ProcessInstance API (covered in this chapter),

  • scripting language API,

  • Apache Camel exec component,

  • Java API (ProcessBuilder).

Table 39. Subprocess configuration parameters
Name Type Description

name

String

The process name.

executable

String

The process executable.

argument

String

Zero or more process arguments.

workingDir

String

The process working directory. If null (the default value) then the current directory will be used.

env

name, value

Zero or more additional environment variables for the subprocess.

waitSeconds

Long

The maximum number of seconds to wait after the start of the process. The thread that started the process will be blocked until the time elapses or the subprocess exits. If null (the default value), the thread will not wait.

inputRedirect

InputRedirect

The standard input redirect type (see the following tables). There are convenience methods inputAs…​ available.

outputRedirect

OutputRedirect

The standard output redirect type (see the following tables). There are convenience methods outputAs…​ available.

errorRedirect

ErrorRedirect

The standard error redirect type (see the following tables). There are convenience methods errorAs…​ available.

charset

String

The the charset of the subprocess streams used if the redirect type is STRING.

waitForPositiveLineRegexp

String

The Java regular expression of a line from the process output text stream. The thread that started the process will wait (blocking) for such line. If set to null, the thread will not wait for a specific line (or waitForNegativeLineRegexp if set).

waitForNegativeLineRegexp

String

Sets the Java regular expression of a line from the process output text stream that signals an error and should cause throwing an exception.

waitForLineTimeout

Long

The timeout for waiting for a specific line from the process output stream (in seconds). If null, the thread could wait indefinitely. If the timeout is exceeded, the exception will be thrown.

inputString

String

The input string that will be set as the process standard input. Applicable only if the input redirect type is STRING.

inputBinary

byte[]

he input bytes that will be set as the process standard input. Applicable only if the input redirect type is BINARY.

Table 40. Standard input redirect type
Value Description

PIPE

Indicates that subprocess standard input will be connected to the current Java process over a pipe. This is the default handling of subprocess standard input.

INHERIT

Sets the destination for subprocess standard input to be the same as those of the current Java process.

STRING

Sets the subprocess input as the ProcessConfiguration.inputString string.

BINARY

Sets the subprocess input as the ProcessConfiguration.inputBinary bytes.

FILE

Sets the subprocess input as the ProcessConfiguration.inputFile file specified as the filename.

STREAM

Sets the subprocess input as a stream. This is a special case of PIPE that makes easier writing to and closing the subprocess standard input ProcessInstance.getInput() after start. Then you should invoke manually ProcessInstance.waitForReady().

Table 41. Standard output redirect type
Value Description

PIPE

Indicates that subprocess standard output will be connected to the current Java process over a pipe. This is the default handling of subprocess standard output.

INHERIT

Sets the destination for subprocess standard output to be the same as those of the current Java process.

STRING

Writes all subprocess standard output to the ProcessInstance.outputString string. The thread that started the subprocess will wait for the subprocess to exit.

BINARY

Writes all subprocess standard output to the ProcessInstance.outputBinary byte array. The thread that started the subprocess will wait for the subprocess to exit.

FILE

Writes all subprocess standard output to the ProcessInstance.outputFile file. The thread that started the subprocess will wait for the subprocess to exit.

CONSUMER

Sends a subprocess standard output as text lines to a line consumer (if set). It also logs the subprocess standard output to the logger (as INFO).

Table 42. Standard error redirect type
Value Description

PIPE

Indicates that subprocess error output will be connected to the current Java process over a pipe. This is the default handling of subprocess error output.

INHERIT

Sets the destination for subprocess error output to be the same as those of the current Java process.

STRING

Writes all subprocess error output to the ProcessInstance.errorString string. The thread that started the subprocess will wait for the subprocess to exit.

FILE

Writes all subprocess error output to the ProcessInstance.getErrorFile file. The thread that started the subprocess will wait for the subprocess to exit.

EXCEPTION

Throw an exception if the error output is not empty. The thread that started the subprocess will wait for the subprocess to exit.

CONSUMER

Sends a subprocess standard error as text lines to a line consumer (if set). It also logs the subprocess error output to the logger (as WARN).

The preferred way to configure redirects is to use inputAs…​, outputAs…​ and errorAs…​ methods.

Example of running an external executable with arguments in Java
ProcessInstance process = engine.getOperations().process(ProcessConfiguration.builder("echo", "TEST").outputAsString()).run();
String output = process.getOutputString();
Example of running an external executable with arguments in Python
from org.openksavi.sponge.util.process import ProcessConfiguration

process = sponge.process(ProcessConfiguration.builder("echo", "TEST").outputAsString()).run()
print process.outputString
Example of running an external executable with an additional environment in Java
ProcessInstance process = engine.getOperations().process(ProcessConfiguration.builder("printenv")
        .arguments("TEST_VARIABLE").env("TEST_VARIABLE", "TEST").outputAsString()).run();
Example of running an external executable with an additional environment in Python
from org.openksavi.sponge.util.process import ProcessConfiguration

process = sponge.process(ProcessConfiguration.builder("printenv").arguments("TEST_VARIABLE")
                .env("TEST_VARIABLE", "TEST").outputAsString()).run()

For more examples see ProcessInstanceTest.java.

15.11. Python (CPython) / Py4J

Sponge may communicate with external programs written in the reference implementation of the Python programming language - CPython using Py4J, and vice versa. A Python program and a Sponge Java process communicate through network sockets.

Py4J by default uses the TCP port 25333 to communicate from Python to Java and TCP port 25334 to communicate from Java to Python.

There is no support for writing knowledge bases in CPython.

In the following examples Python 3 will be used.

The CPython environment must have Py4J installed, e.g.:

pip3 install py4j

For more information on Py4J see https://www.py4j.org/advanced_topics.html.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-py4j</artifactId>
    <version>1.12.0</version>
</dependency>

15.11.1. Py4J plugins

Sponge provides two plugins for integration with CPython.

Local network sockets used by Py4j should be secured, for example using TLS. Please be aware that all Sponge operations are accessible in other processes that communicate with the Sponge with Py4J enabled by a plugin. See https://github.com/softelnet/sponge/tree/master/sponge-py4j/examples/py4j//java_server_tls for an example of TLS security, based on Py4J examples. Note that in a production environment you should customize this simple configuration, possibly by providing your own configured instance of GatewayServer or ClientServer to the plugin.
Table 43. Py4J plugin common configuration parameters
Name Type Description

facadeInterface

String

A Java interface that is a facade to the Py4J entry point object configured on the CPython side.

javaPort

int

Java side server port.

pythonPort

int

CPython side server port.

security

XML element/SslConfiguration

The simple SSL security configuration.

security/keyStore

String

Simple security keystore file location on the classpath.

security/keyStorePassword

String

Simple security keystore password.

security/keyPassword

String

Simple security key password.

security/algorithm

String

Simple security algorithm. The default value is SunX509.

pythonScript

XML element/ProcessConfiguration

The configuration of the CPython script that can be run as a subprocess of the Sponge Java process when the plugin is starting up. Typically such script would init the Py4J connection on the CPython side. The plugin automatically adds to the environment variables for the subprocess: PY4J_JAVA_PORT, PY4J_PYTHON_PORT and optionally PY4J_AUTH_TOKEN.

pythonScriptBeforeStartup

boolean

If true, the CPython script will be started before this plugin startup (the default value), otherwise it will be started after this plugin startup.

generateAuthToken

boolean

If true, the plugin will generate the Py4J auth token (for both sides). The default value is false. This option is useful when combined with the pythonScript.

authToken

String

The manual or generated Py4J auth token (for both sides).

randomPorts

boolean

If true, the plugin will use random ports (for both sides). The default value is false. This option is useful when combined with the pythonScript.

15.11.1.1. GatewayServerPy4JPlugin

GatewayServerPy4JPlugin provides integration with CPython using Py4J GatewayServer.

For more information see the GatewayServerPy4JPlugin Javadoc.

Sponge side example
GatewayServerPy4JPlugin XML configuration example
<sponge>
    <plugins>
        <plugin name="py4j" class="org.openksavi.sponge.py4j.GatewayServerPy4JPlugin" />
    </plugins>
</sponge>
CPython side example
Sending Sponge event in CPython
from py4j.java_gateway import JavaGateway

gateway = JavaGateway()

# The Sponge in other process accessed via Py4J
sponge = gateway.entry_point

print "Connected to {}".format(sponge.getInfo())
sponge.event("helloEvent").set("say", "Hello from Python's Py4J").send()

Note that a simplified bean property access is not supported here. So instead of sponge.info you have to invoke sponge.getInfo().

15.11.1.2. ClientServerPy4JPlugin

ClientServerPy4JPlugin provides integration with CPython using Py4J ClientServer.

Table 44. ClientServerPy4JPlugin plugin specific configuration parameters
Name Type Description

autoStartJavaServer

Boolean

Auto start of Py4J JavaServer.

For more information see the ClientServerPy4JPlugin Javadoc.

Sponge side example
ClientServerPy4JPlugin XML configuration example
<sponge>
    <plugins>
        <plugin name="py4j" class="org.openksavi.sponge.py4j.ClientServerPy4JPlugin">
            <configuration>
                <facadeInterface>org.openksavi.sponge.py4j.PythonService</facadeInterface>
            </configuration>
        </plugin>
    </plugins>
</sponge>
Python facade interface
public interface PythonService {
    String toUpperCase(String text);
}
ClientServerPy4JPlugin knowledge base example written in Jython
# Note that this code is interpreted by Jython in Sponge, not CPython
class PythonUpperCase(Action):
    def onCall(self, text):
        result = py4j.facade.toUpperCase(text)
        self.logger.debug("CPython result for {} is {}", text, result)
        return result
CPython side example
Implementation of the facade interface in CPython
from py4j.clientserver import ClientServer

class PythonService(object):
    def toUpperCase(self, text):
        return text.upper()
    class Java:
        implements = ["org.openksavi.sponge.py4j.PythonService"]

pythonService = PythonService()
gateway = ClientServer(python_server_entry_point=pythonService)

15.11.2. Executing an external Python script

The plugin may run a CPython script as a subprocess.

Example of an XML configuration for executing an external Python script
    <plugins>
        <plugin name="py4j" class="org.openksavi.sponge.py4j.GatewayServerPy4JPlugin">
            <configuration>
                <pythonScript>
                    <executable>python3</executable>
                    <argument>${sponge.configDir}/cpython_script.py</argument>
                    <waitSeconds>60</waitSeconds>
                    <waitForOutputLineRegexp>The CPython service has started.</waitForOutputLineRegexp>
                    <outputRedirect>CONSUMER</outputRedirect>
                </pythonScript>
                <pythonScriptBeforeStartup>false</pythonScriptBeforeStartup>
            </configuration>
        </plugin>
    </plugins>

15.12. ReactiveX

The ReactiveX plugin (ReactiveXPlugin) provides support for using ReactiveX in knowledge bases, e.g. for processing stream of Sponge events using reactive programming. The plugin uses RxJava library. The current version of the plugin is very simple. For example it hasn’t got any configuration parameters.

The default name of the ReactiveX plugin (which can be used in knowledge bases) is rx.

The main object provided by this plugin is an instance of a hot observable (rx.observable) that emits all non system Sponge events. The plugin registers a Java-based correlator that listens to Sponge events and sends them to the observable.

For more information see the ReactiveXPlugin Javadoc.

The following example shows how to use reactive programming in a Sponge knowledge base.

Example Python knowledge base - Reactive programming
import time
from io.reactivex.schedulers import Schedulers

def onStartup():
    sponge.event("e1").set("payload", 1).send()
    sponge.event("e2").set("payload", 2).sendAfter(500)
    sponge.event("e3").set("payload", 3).sendAfter(1000)

    rx.observable.subscribe(lambda event: sponge.logger.info("{}", event.name))

    def observer(event):
        time.sleep(1)
        sponge.logger.info("After sleep: {}", event.name)
    rx.observable.observeOn(Schedulers.io()).subscribe(observer)
Example XML configuration
<sponge>
    <knowledgeBases>
        <knowledgeBase name="kb">
            <file>reactivex.py</file>
        </knowledgeBase>
    </knowledgeBases>
    <plugins>
        <plugin name="rx" class="org.openksavi.sponge.reactivex.ReactiveXPlugin" />
    </plugins>
</sponge>

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-reactivex</artifactId>
    <version>1.12.0</version>
</dependency>

15.13. MIDI

The MIDI plugin (MidiPlugin) allows processing MIDI messages by the Sponge and provides communication with MIDI devices. It wraps MIDI messages in Sponge events. The plugin supports ShortMessage, MetaMessage and SysexMessage MIDI messages wrapping them respectively in MidiShortMessageEvent, MidiMetaMessageEvent and MidiSysexMessageEvent Sponge events. Although the MIDI support in the Sponge provides a set of methods that use the javax.sound.midi API, the goal of this plugin is not to be a complete interface to the MIDI system but a bridge between MIDI messages and Sponge events.

The default name of the MIDI plugin (which can be used in knowledge bases) is midi.

Table 45. MIDI plugin configuration parameters
Name Type Description

sequencerConnectedToSynthesizer

Boolean

If true then the default MIDI sequencer will be connected to the default synthesizer (e.g. to generate sound while playing MIDI files). The default value is false.

loadAllInstruments

Boolean

If true then all instruments in the default soundbank will be loaded at startup. The default value is true.

midiShortMessageEventName

String

A name of a MIDI ShortMessage Sponge event sent by this plugin to the engine. The default value is "midiShort".

midiMetaMessageEventName

String

A name of a MIDI MetaMessage Sponge event sent by this plugin to the engine. The default value is "midiMeta".

midiSysexMessageEventName

String

A name of a MIDI SysexMessage Sponge event sent by this plugin to the engine. The default value is "midiSysex".

For more information see the MidiPlugin Javadoc.

Example Python knowledge base that shows how to process MIDI messages created by an external MIDI input device
from javax.sound.midi import ShortMessage
from org.openksavi.sponge.midi import MidiUtils

class SameSound(Trigger):
    def onConfigure(self):
        self.withEvent("midiShort") (1)
    def onRun(self, event):
        midi.sound(event.message) (2)

class Log(Trigger):
    def onConfigure(self):
        self.withEvent("midiShort")
    def onRun(self, event):
        self.logger.info("{}Input message: {}", "[" + MidiUtils.getKeyNote(event.data1) + "] " if event.command == ShortMessage.NOTE_ON else "",
                         event.messageString) (3)

def onStartup():
    sponge.logger.info("This example program enables a user to play an input MIDI device (e.g. a MIDI keyboard) using the Sponge MIDI plugin.")
    midi.connectDefaultInputDevice() (4)
    sponge.logger.info("Input MIDI device: {}", midi.inputDevice.deviceInfo.name)
    sponge.logger.info("Instruments: {}", ",".join(list(map(lambda i: i.name + " (" + str(i.patch.bank) + "/" + str(i.patch.program) + ")", midi.instruments))))
    midi.setInstrument(0, "Electric Piano 1") (5)
1 The trigger SameSound listens to all MIDI short messages.
2 The trigger SameSound sends all MIDI short messages received from the input MIDI device to the MIDI synthesizer to generate sounds. It is achieved through the use of the sound method in the midi plugin.
3 The trigger Log only logs a MIDI message info and a note for note on MIDI messages.
4 Connects a default input MIDI device in the system (e.g. a MIDI keyboard) to the MIDI plugin in order to receive all MIDI messages generated by this device and send them to the Sponge engine as Sponge events.
5 Sets the instrument (by name) in the MIDI synthesizer for the MIDI channel 0. Note that this example assumes that the input MIDI device will generate MIDI messages for the same channel.
An event flow in the Sponge engine introduces an additional performance overhead that in some situations may be not acceptable when dealing with real-time physical MIDI instruments.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-midi</artifactId>
    <version>1.12.0</version>
</dependency>

15.14. Raspberry Pi - Pi4J

The Pi4J plugin (Pi4JPlugin) allows using the Pi4J library in Sponge knowledge bases. The Pi4J library provides a friendly object-oriented I/O API and implementation libraries to access the full I/O capabilities of the Raspberry Pi platform. The current version of the plugin is very simple. For example it hasn’t got any configuration parameters.

The default name of the Pi4J plugin (which can be used in knowledge bases) is pi4j.

For more information see the Pi4JPlugin Javadoc.

The Pi4J documentation states that You must now have WiringPi installed on your target Raspberry Pi system separately from Pi4J. WiringPi is now included be default in the latest Raspbian builds.

The following example shows how to turn on/off a Grove LED connected to the Raspberry Pi GPIO. The hardware setup for this example includes Raspberry Pi 3, a ribbon cable, a ribbon cable socket, a breadboard, a 4-pin male jumper to Grove 4 pin conversion cable and a Grove LED. Before setting up the hardware make sure that your Raspberry Pi is not powered! The Grove LED should be connected to GPIO via a 4-pin connector: the black wire goes on PIN#14 (Ground), the red wire goes on PIN#02 (DC Power 5V), the yellow wire goes on PIN#12 (GPIO18/GPIO_GEN1), the white wire goes on PIN#06 (Ground).

Example Python knowledge base - Blinking LED
from com.pi4j.io.gpio import RaspiPin, PinState

state = False

class LedBlink(Trigger):
    def onConfigure(self):
        self.withEvent("blink")
    def onRun(self, event):
        global led, state
        state = not state
        led.setState(state)

def onStartup():
    global led
    led = pi.gpio.provisionDigitalOutputPin(RaspiPin.GPIO_01, "led", PinState.LOW)
    sponge.event("blink").sendAfter(0, 1000)

def onShutdown():
    off()

on = lambda: led.setState(True)
off = lambda: led.setState(False)
Example XML configuration
<sponge>
    <properties>
        <!-- Due to the problem https://github.com/Pi4J/pi4j/issues/319, the dynamic linking option is turned on, where Pi4J is dynamically linked
            to WiringPi rather than the default static linking. -->
        <property name="pi4j.linking" system="true">dynamic</property>
    </properties>
    <knowledgeBases>
        <knowledgeBase name="kb">
            <file>pi4j_led_blink.py</file>
        </knowledgeBase>
    </knowledgeBases>
    <plugins>
        <plugin name="pi" class="org.openksavi.sponge.rpi.pi4j.Pi4JPlugin" />
    </plugins>
</sponge>

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-rpi-pi4j</artifactId>
    <version>1.12.0</version>
</dependency>

15.15. Raspberry Pi - GrovePi

The GrovePi plugin (GrovePiPlugin) allows accessing the GrovePi hardware in Sponge knowledge bases. GrovePi is an electronics board for Raspberry Pi that may have a variety of sensors and actuators connected to. The plugin uses Java 8 GrovePi library. The current version of the plugin is very simple. For example it hasn’t got any configuration parameters.

The default name of the GrovePi plugin (which can be used in knowledge bases) is grovepi.

If using this plugin in an embedded Sponge, you have to manually install the Java 8 GrovePi library in you local Maven repository because it isn’t available in the Central Maven Repository.

For more information see the GrovePiPlugin Javadoc.

The following example shows how to turn on/off a LED connected to the GrovePi board that in turn is connected to the Raspberry Pi.

Example Python knowledge base - Blinking LED
# GrovePi board: Connect LED to D4

state = False

class LedBlink(Trigger):
    def onConfigure(self):
        self.withEvent("blink")
    def onRun(self, event):
        global led, state
        state = not state
        led.set(state)

def onStartup():
    global led
    led = grovepi.device.getDigitalOut(4)
    sponge.event("blink").sendAfter(0, 1000)
Example XML configuration
<sponge>
    <knowledgeBases>
        <knowledgeBase name="kb">
            <file>led_blink.py</file>
        </knowledgeBase>
    </knowledgeBases>
    <plugins>
        <plugin name="grovepi" class="org.openksavi.sponge.rpi.grovepi.GrovePiPlugin" />
    </plugins>
</sponge>

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-rpi-grovepi</artifactId>
    <version>1.12.0</version>
</dependency>

15.16. TensorFlow

Sponge provides integration with TensorFlow. TensorFlow could be used for machine learning applications such as neural networks. The machine learning is a subset of Artificial Intelligence.

Although there could be many ways of using TensorFlow from Java, this integration uses the Py4J library wrapped in the Py4J plugin to communicate between a Sponge Java process and a Python program running TensorFlow. The TensorFlow Python API has been chosen over the Java API, because, at the time of writing, the TensorFlow APIs in languages other than Python were not covered by the API stability promises. For use cases that require low latency times, the usage of Py4J may be insufficient. An alternative approach is to use TensorFlow serving, designed for production environments.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-tensorflow</artifactId>
    <version>1.12.0</version>
</dependency>

15.16.1. The Digits recognition REST API service example

This example shows how to expose the TensorFlow machine learning model trained for the MNIST database as a REST API service to recognize handwritten digits. For the complete source code see https://github.com/softelnet/sponge/tree/master/sponge-tensorflow. Please note that the Python language is used both in the Sponge knowledge base (Jython version 2.7) and in the script running TensorFlow (CPython version 3).

CPython prerequisites
# Install TensorFlow by following the guide https://www.tensorflow.org/install/, for example with Virtualenv and Python 3.
# Only the most important steps are presented hereunder.

virtualenv --system-site-packages -p python3 ~/tensorflow
cd ~/tensorflow
source ./bin/activate

# Change directory to the Sponge source code main directory and install Python dependencies.
(tensorflow)$ pip3 install -r sponge-tensorflow/examples/tensorflow/digits/requirements.txt
(tensorflow)$ deactivate
Table 46. The main components
Filename Description

ImageClassifierService.java

The Java interface of the image classifier Python service. This interface is used by Py4J to expose Python functionality to a Java process.

actions/digits_predict.py

The Sponge knowledge base that contains definitions of actions that will be exposed in the Sponge REST API service. The DigitsPredict action takes a binary representation of a PNG file and passes it to the running Python script file by invoking ImageClassifierService.predict(byte[] image) method. This method will be invoked on the remote object running in the Python process.

digits_rest_server.xml

The Sponge configuration file that instructs Sponge to create the Py4J plugin, execute the Python script file that will load a TensorFlow model and start the REST API server.

digits_rest_server.py

The main Sponge knowledge base file (compatible with Jython) for that example.

python/digits_model.py

The Python script file (compatible with CPython) that defines the ConvNet model trained on the MNIST database to recognize handwritten digits. This example uses Keras neural networks API that runs on top of TensorFlow.

python/image_classifier_service.py

The Python script file (compatible with CPython) that loads the model. If the model file data/digits_model.h5 exists, it will be loaded. Otherwise a new model will be trained and saved. This model is then used by the Python-based ImageClassifierService implementation that is exposed by the Python-side Py4J gateway.

python/digits_model_create.py

The auxiliary Python script file (compatible with CPython) that manually creates, trains and saves the model. It overrides the model file. Additionally the script plots the training and validation loss side by side, as well as the training and validation accuracy.

The Sponge REST API configuration used for this example is not secure. In the production environment you should use HTTPS as well as user authentication.

15.17. GSM modem

Sponge provides access to a GSM modem device. The GammuGsmModemPlugin uses Gammu. The requirement is that Gammu utility has to be installed. The current implementation of the GammuGsmModemPlugin is limited. It only sends SMSes. However you may invoke gammu in a knowledge base using the Sponge ProcessInstance API.

The default name of the plugin (which can be used in knowledge bases) is gsm.

Example of sending SMS using the GammuGsmModemPlugin
class SendSms(Action):
    def onConfigure(self):
        self.withLabel("Send an SMS").withDescription("Sends a new SMS.")
        self.withArgs([
            StringType("recipient").withFormat("phone").withLabel("Recipient").withDescription("The SMS recipient."),
            StringType("message").withMaxLength(160).withFeatures({"maxLines":5}).withLabel("Message").withDescription("The SMS message.")
        ]).withNoResult()
    def onCall(self, recipient, message):
        gsm.sendSms(recipient, message)
Example XML configuration
<plugin name="gsm" class="org.openksavi.sponge.gsmmodem.GammuGsmModemPlugin" />
Example of sending SMS using the ProcessInstance API
from org.openksavi.sponge.util.process import ProcessConfiguration

def sendSms(recipient, message):
    process = sponge.runProcess(ProcessConfiguration.builder("gammu").arguments("sendsms", "TEXT", recipient,
            None if gsm.canEncodeGsm(message) else "-unicode")
            .inputAsString(message).outputAsString().build())
    if process.waitFor() != 0:
        raise Exception("Exit code {}: {}".format(process.exitCode, process.outputString))

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-gsm-modem</artifactId>
    <version>1.12.0</version>
</dependency>

16. Best practices

When developing an application using Sponge you have to be aware of the fact that knowledge bases could be created in two categories of programming languages:

  • Java,

  • supported scripting languages (e.g. Python, Ruby, Groovy, JavaScript).

Each of these two categories has its pros and cons which makes it better for a certain use. For example scripting languages work well when flexible modification of source code is required.

Libraries written in Java or supported scripting languages may be used, however make sure that they are compatible with the implementations of these languages.

The following chapters describe the best practices for typical use cases.

16.1. Events

Sponge is used for developing applications based on event processing. That is why you should start with defining event types. Events should contain enough information (in the form of attributes) so that event processors could provide demanded logic. Moreover, if necessary, you should consider using event chaining, i.e. sending events of a more abstract level based on correlations of low level events.

16.2. Plugins

If there is a need for creating an interface to an external system, the best way is to use existing or create a new plugin. Once written plugin could be used in other Sponge based applications.

In most cases a CamelPlugin, by providing access to all Camel components, should be sufficient when integrating with various systems.

If there is a need for creating a custom plugin, in most use cases we suggest creating it in Java rather than in a scripting language.

16.3. Processors

When defining a processor that is not a singleton, its class implementation should provide lightweight creating of new instances.

16.4. Filters

Filters are especially important when an application cooperates with an external system. If such system sends events, it is a good practice to check if an event contains all expected information and if event attribute values are valid. This type of selection could be done in filters. Filters may also prevent from idly processing events that should be ignored by the application logic at an early phase as they can have an impact on the whole performance.

16.5. Triggers

Triggers should be implemented in a way to support concurrent processing of many events by different threads. You should avoid class level (static) variables and restrict, if possible, to data transfered in events.

16.6. Rules

Rules should be used when triggers functionality is not sufficient.

16.7. Correlators

Correlators should be used when filters, triggers and rules functionality is not sufficient for the problem you try to solve.

16.8. Actions

Actions should be created only when there is a need to provide some functionality that is to be used in many knowledge bases that are written in different scripting languages and only when you don’t want to write it in Java.

17. Scripting languages

17.1. Supported scripting languages

Table 47. Supported knowledge base scripting languages
Language Implementation

Python

Jython

Ruby

JRuby

Groovy

Groovy

JavaScript

Nashorn

17.2. Python

17.2.1. Limitations

Note that Jython 2.7.1 has a few bugs that could in some cases impact the stability of the system, e.g.: http://bugs.jython.org/issue2487.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-jython</artifactId>
    <version>1.12.0</version>
</dependency>

The dependency for Jython used in Sponge is Jython shaded.

17.3. Ruby

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-jruby</artifactId>
    <version>1.12.0</version>
</dependency>

17.4. Groovy

17.4.1. Limitations

In Groovy you cannot define a class or a function twice in the same file. If you want to prepare a processor to reload, you have to put it in a separate file and use sponge.reloadClass() method. That separate file could be modified and reloaded.

Example
void onLoad() {
    sponge.reloadClass(TriggerA)
    sponge.enable(TriggerA)

    sponge.reloadClass(ActionA)
    sponge.enable(ActionA)
    sponge.call("ActionA")
}

For every knowledge base file there is a new Groovy Script instance created. For example when reloading, a new Groovy Script instance is created for each knowledge base file and they are placed in a list (in a reverse order) to be used by the Sponge Groovy interpreter internally.

It seems that a Groovy-based knowledge base must have at east one function (may be empty). Otherwise you may get a Groovy exception.

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-groovy</artifactId>
    <version>1.12.0</version>
</dependency>

17.5. JavaScript

JavaScript interpreter supports shell scripting extensions in Nashorn to provide simpler shell integration.

Support for JavaScript in Sponge is deprecated because Nashorn engine is planned to be removed from future JDK releases.

17.5.1. Limitations

17.5.1.1. Custom class attributes and methods

There is a limitation for using custom class attributes and methods in processors written in JavaScript implementation Nashorn. In that case you should set a class field target in the onInit() method as in the following example. All class fields and methods that are new (i.e. not inherited from the base classes) must be defined in target.

JavaScript target
var HeartbeatFilter = Java.extend(Filter, {
    onConfigure: function(self) {
        self.withEvent("heartbeat");
    },
    onInit: function(self) {
        self.target = new function() { (1)
            this.heartbeatCounter = 0;
        }
    },
    onAccept: function(self, event) {
        self.target.heartbeatCounter++; (2)
        if (self.target.heartbeatCounter > 2) {
            sponge.removeEvent(hearbeatEventEntry);
            return false;
        } else {
            return true;
        }
    }
});
1 Setting target that defines an attribute heartbeatCounter.
2 Using target for accessing attribute heartbeatCounter.
17.5.1.2. Abstract processors

The support for abstract processors is not implemented for processors written in JavaScript.

17.5.1.3. Dynamic onCall callback methods in actions

Dynamic onCall callback methods are not supported. Every JavaScript action has to implement the abstract Object onCall(Object self, Object[] args) method. Arguments are passed to an action only as an array.

JavaScript onCall method
var EmphasizeAction = Java.extend(Action, {
    onCall: function(self, args) {
        self.logger.debug("Action {} called", self.meta.name);
        if (args.length > 0) {
            return "*** " + args[0] + " ***";
        } else {
            return args;
        }
    }
});

Maven configuration

Maven users will need to add the following dependency to their pom.xml:

<dependency>
    <groupId>org.openksavi.sponge</groupId>
    <artifactId>sponge-nashorn</artifactId>
    <version>1.12.0</version>
</dependency>

18. Logging

Sponge uses SLF4J facade for logging.

Examples and Sponge standalone command-line application use Logback as a logging implementation.

Java-based processors and plugins may use:

  • Sponge logging, by using the getLogger() method, e.g. getLogger().info("logging"), or

  • own loggers defined in their classes, according to the standard conventions, e.g.

    private static final Logger logger = LoggerFactory.getLogger(ConnectionPlugin.class);
    ...
        logger.info("logging");
    }
You may see ignored events (i.e. events that go to the Output Event Queue) in the logs by setting the sponge.event.ignored logger to INFO.

19. Examples

19.1. Complete example projects of embedding Sponge

The complete projects could be used as a point of reference to embed Sponge in your Java application. They are placed in sponge-examples-projects.

19.1.1. News project

This project shows how to process news as events. It is placed in sponge-examples-project-news (see sources).

Event flow:

  • News are manually generated and sent as Sponge events named news in onStartup function of the knowledge base named newsGenerator. Each event has custom attributes: source and title.

  • Every event named news is filtered to discard news that have empty or short (according to newsFilterWordThreshold configuration property) titles. This is done by NewsFilter filter.

  • Events named news are logged by LogNewsTrigger trigger.

  • When there are no new news events (that passed filters) for a specified time, then alarm event is sent. This is done by NoNewNewsAlarmRule rule.

  • LatestNewsCorrelator correlator listens to news events and stores the latest news in storagePlugin plugin in a Python deque. The number of latest news is configured as latestNewsMaxSize property.

  • When alarm event happens, this fact is logged by AlarmTrigger trigger using echoPlugin plugin and EmphasizeAction action.

Main class - NewsExampleMain
package org.openksavi.sponge.examples.project.news;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.openksavi.sponge.core.engine.DefaultSpongeEngine;
import org.openksavi.sponge.engine.SpongeEngine;

/**
 * Example class containing main method.
 */
public class NewsExampleMain {

    private static final Logger logger = LoggerFactory.getLogger(NewsExampleMain.class);

    /** XML configuration file. */
    public static final String CONFIG_FILE = "config/config.xml";

    /** The engine. */
    private SpongeEngine engine;

    /**
     * Starts up an engine.
     */
    public void startup() {
        if (engine != null) {
            return;
        }

        // Use EngineBuilder API to create an engine.
        engine = DefaultSpongeEngine.builder().config(CONFIG_FILE).build();

        // Start the engine.
        engine.startup();

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                shutdown();
            } catch (Throwable e) {
                logger.error("Shutdown hook error", e);
            }
        }));
    }

    /**
     * Shutdown the engine.
     */
    public void shutdown() {
        if (engine != null) {
            engine.shutdown();
            engine = null;
        }
    }

    public SpongeEngine getEngine() {
        return engine;
    }

    /**
     * Main method. Arguments are ignored.
     */
    public static void main(String... args) {
        new NewsExampleMain().startup();
    }
}
Sponge XML configuration file - config.xml
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">

    <properties>
        <!-- News that have less words in the title than specified by this parameter will be rejected by filters. -->
        <property name="newsFilterWordThreshold" variable="true">3</property>
        <!-- Max size of a buffer that stores latest news. -->
        <property name="latestNewsMaxSize" variable="true">5</property>
    </properties>

    <knowledgeBases>
        <!-- Main knowledge base (implemented in Python) that uses 3 files. These files will be loaded by the same interpreter. -->
        <knowledgeBase name="main">
            <!-- Plugin implemented in Python. -->
            <file>kb/main_plugins.py</file>
            <!-- Main event processors. For the sake of clarity registration of event processors is placed in the next file. -->
            <file>kb/main_event_processors.py</file>
            <!-- Knowledge base callback functions: onInit, onLoad, etc. -->
            <file>kb/main_functions.py</file>
        </knowledgeBase>
        <!-- Actions knowledge base (implemented in JavaScript). -->
        <knowledgeBase name="actions">
            <file>kb/actions.js</file>
        </knowledgeBase>
        <!-- News generator knowledge base. -->
        <knowledgeBase name="newsGenerator">
            <file>kb/news_generator.py</file>
        </knowledgeBase>
    </knowledgeBases>

    <plugins>
        <!-- Plugin defined in Java. -->
        <plugin name="echoPlugin" class="org.openksavi.sponge.examples.project.news.MultiEchoPlugin">
            <configuration>
                <count>2</count>
            </configuration>
        </plugin>

        <!-- Plugin defined in Python. Stores the last news entry". -->
        <plugin name="storagePlugin" class="StoragePlugin" knowledgeBaseName="main">
            <configuration>
                <storedValue>no news yet</storedValue>
            </configuration>
        </plugin>
    </plugins>
</sponge>
Python-based knowledge base 'main' file - main_event_processors.py
from java.util.concurrent.atomic import AtomicBoolean
import re, collections

# Reject news with empty or short titles.
class NewsFilter(Filter):
    def onConfigure(self):
        self.withEvent("news")
    def onAccept(self, event):
        title = event.get("title")
        words = len(re.findall("\w+", title))
        return title is not None and words >= int(sponge.getVariable("newsFilterWordThreshold"))

# Log every news.
class LogNewsTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("news")
    def onRun(self, event):
        self.logger.info("News from " + event.get("source") + " - " + event.get("title"))

# Send 'alarm' event when news stop arriving for 3 seconds.
class NoNewNewsAlarmRule(Rule):
    def onConfigure(self):
        self.withEvents(["news n1", "news n2 :none"]).withDuration(Duration.ofSeconds(3))
    def onRun(self, event):
        sponge.event("alarm").set("severity", 1).set("message", "No new news for " + str(self.meta.duration.seconds) + "s.").send()

# Handles 'alarm' events.
class AlarmTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("alarm")
    def onRun(self, event):
        self.logger.info("Sound the alarm! {}", event.get("message"))
        self.logger.info("Last news was (repeat {} time(s)):\n{}", echoPlugin.count,
                         sponge.call("EmphasizeAction", [echoPlugin.echo(storagePlugin.storedValue[-1])]))
        sponge.getVariable("alarmSounded").set(True)

# Start only one instance of this correlator for the system. Note that in this example data is stored in a plugin,
# not in this correlator.
class LatestNewsCorrelator(Correlator):
    def onConfigure(self):
        self.withEvent("news").withMaxInstances(1)
    def onInit(self):
        storagePlugin.storedValue = collections.deque(maxlen=int(sponge.getVariable("latestNewsMaxSize", 2)))
    def onEvent(self, event):
        storagePlugin.storedValue.append(event.get("title"))
        self.logger.debug("{} - latest news: {}", self.hashCode(), str(storagePlugin.storedValue))
Python-based knowledge base 'main' file - main_functions.py
from java.util.concurrent.atomic import AtomicBoolean

# Set initial values for variables.
def onInit():
    sponge.setVariable("alarmSounded", AtomicBoolean(False))
Python-based knowledge base 'main' file - main_plugins.py
# Plugin written in Python. Stores any value.
class StoragePlugin(Plugin):
    def onConfigure(self, configuration):
        self.storedValue = configuration.getString("storedValue", "default")
    def onInit(self):
        self.logger.debug("Initializing {}", self.name)
    def onStartup(self):
        self.logger.debug("Starting up {}", self.name)
    def getStoredValue(self):
        return self.storedValue
    def setStoredValue(self, value):
        self.storedValue = value
JavaScript-based knowledge base 'actions' file - actions.js
/**
 * Sponge Knowledge base
 */

var EmphasizeAction = Java.extend(Action, {
    onCall: function(self, args) {
        self.logger.debug("Action {} called", self.meta.name);
        if (args != null && args.length > 0) {
            return "*** " + args[0] + " ***";
        } else {
            return null;
        }
    }
});
Python-based knowledge base that generates sample news - news_generator.py
# Utility function.
def sendNewsEvent(source, title, delay):
    sponge.event("news").set("source", source).set("title", title).sendAfter(delay)

# Send sample events carrying news on startup.
def onStartup():
    allNews = ["First people landed on Mars!", "Ups", "Martians are happy to meet their neighbors"]
    for i in range(len(allNews)):
        sendNewsEvent("newsSourceA", allNews[i], i * 1000)
Java-based plugin class - MultiEchoPlugin
package org.openksavi.sponge.examples.project.news;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.openksavi.sponge.config.Configuration;
import org.openksavi.sponge.java.JPlugin;

/**
 * Java-based plugin.
 */
public class MultiEchoPlugin extends JPlugin {

    private static final Logger logger = LoggerFactory.getLogger(MultiEchoPlugin.class);

    private int count = 1;

    public MultiEchoPlugin() {
        //
    }

    public MultiEchoPlugin(String name) {
        super(name);
    }

    @Override
    public void onConfigure(Configuration configuration) {
        count = configuration.getInteger("count", count);
    }

    @Override
    public void onInit() {
        logger.debug("Initializing {}", getName());
    }

    @Override
    public void onStartup() {
        logger.debug("Starting up {}", getName());
    }

    public String echo(String text) {
        return StringUtils.repeat(text, ", repeat: ", count).toUpperCase();
    }

    public int getCount() {
        return count;
    }
}

19.1.2. Camel RSS News project

This example is an enhancement over the News project example. It is placed in sponge-examples-project-camel-rss-news (see sources).

The main change here is that news are acquired as RSS feeds from news services: BBC and CNN. Reading RSS feeds and transformation to Sponge events is performed in a Camel route. Sponge acts as a producer in this Camel route. This example shows Sponge as a consumer in other Camel routes as well.

This example also presents integration with Spring framework. A service provided as a Spring bean is accessed from the script knowledge base.

Knowledge bases main and actions that existed in the News project example are not changed. This is because the main processing is independent of the input and output interfaces, protocols or data structures. Internal events (in this case news events) are normalized.

Event flow:

  • RSS feeds are read from external sources, transformed to Sponge events and sent to the Sponge engine. This is done in Camel routes.

  • The main knowledge base related event flow is the same as in the previous example.

  • After the time configured as a property durationOfReadingRss Camel routes that read RSS feeds from external sources are stopped. It simulates lack of new news. This is done in the simulator knowledge base.

  • When alarm event happens, not only AlarmTrigger (as described in the previous example) handles that event, but here also ForwardAlarmTrigger trigger, defined in the consumer knowledge base. It sends an alarm message to:

    • all Camel endpoints that use the Sponge engine as a consumer in their routes,

    • to a specific endpoint given as URI.

Main class - CamelRssNewsExampleMain
package org.openksavi.sponge.examples.project.camelrssnews;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

import org.openksavi.sponge.engine.SpongeEngine;

/**
 * Example class containing main method.
 */
public class CamelRssNewsExampleMain {

    /** Spring context. */
    private GenericApplicationContext context;

    /**
     * Starts up Spring context (with the engine) manually.
     */
    public void startup() {
        if (context != null) {
            return;
        }

        // Starting Spring context.
        context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        context.registerShutdownHook();
        context.start();
    }

    public SpongeEngine getEngine() {
        return context.getBean(SpongeEngine.class);
    }

    /**
     * Shutdown Spring context.
     */
    public void shutdown() {
        if (context != null) {
            context.stop();
            context.close();
            context = null;
        }
    }

    /**
     * Main method. Arguments are ignored.
     */
    public static void main(String... args) {
        new CamelRssNewsExampleMain().startup();
    }
}
Spring Java configuration - SpringConfiguration
package org.openksavi.sponge.examples.project.camelrssnews;

import java.util.Map;

import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.processor.idempotent.MemoryIdempotentRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import org.openksavi.sponge.EngineOperations;
import org.openksavi.sponge.camel.CamelUtils;
import org.openksavi.sponge.camel.SpongeCamelConfiguration;
import org.openksavi.sponge.engine.SpongeEngine;
import org.openksavi.sponge.spring.SpringSpongeEngine;

/**
 * Spring configuration that creates the engine and Camel context.
 */
@Configuration
@ComponentScan
public class SpringConfiguration extends SpongeCamelConfiguration {

    /** RSS source header name. */
    public static final String HEADER_SOURCE = "source";

    /**
     * The engine is started by Spring at once, in order to load configuration variables (e.g. rssSources) before creating Camel routes.
     *
     * @return the engine.
     */
    @Bean
    public SpongeEngine camelRssEngine() {
        // Use EngineBuilder API to create an engine with the configuration file. Also bind Spring and Camel plugins as beans manually.
        return SpringSpongeEngine.builder().config(CamelRssConstants.CONFIG_FILE).plugins(springPlugin(), camelPlugin()).build();
    }

    /**
     * Camel routes for reading RSS feeds. Routes could be also defined in XML, Groovy or scripting knowledge bases.
     *
     * @return route builder.
     */
    @Bean
    public RouteBuilder rssInputRoute() {
        return new RouteBuilder() {

            // @formatter:off
            @SuppressWarnings("unchecked")
            @Override
            public void configure() throws Exception {
                EngineOperations operations = camelRssEngine().getOperations();
                Map<String, String> rssSources = operations.getVariable(Map.class, CamelRssConstants.VAR_RSS_SOURCES);

                // Read RSS feeds from all configured sources.
                rssSources.forEach((source, url) ->
                        from("rss:" + url + operations.getVariable(CamelRssConstants.VAR_RSS_ENDPOINT_PARAMETERS, "")).routeId(source)
                            .setHeader(HEADER_SOURCE).constant(source)
                            .to("direct:rss"));

                // Gathers RSS from different sources and sends to Sponge engine as a normalized event.
                from("direct:rss").routeId("rss")
                        .marshal().rss()
                        // Deduplicate by title.
                        .idempotentConsumer(xpath("/rss/channel/item/title/text()"),
                                MemoryIdempotentRepository.memoryIdempotentRepository())
                        // Conversion from RSS XML to Sponge event with attributes.
                        .process((exchange) -> exchange.getIn().setBody(operations.event("news")
                                .set("source", exchange.getIn().getHeader(HEADER_SOURCE))
                                .set("channel", CamelUtils.xpath(exchange, "/rss/channel/title/text()"))
                                .set("title", CamelUtils.xpath(exchange, "/rss/channel/item/title/text()"))
                                .set("link", CamelUtils.xpath(exchange, "/rss/channel/item/link/text()"))
                                .set("description", CamelUtils.xpath(exchange, "/rss/channel/item/description/text()"))
                                .make()))
                        .to("sponge:camelRssEngine");
            }
            // @formatter:on
        };
    }

    /**
     * Camel routes that use the engine as a consumer (directly or indirectly).
     *
     * @return route builder.
     */
    @Bean
    public RouteBuilder consumerRoute() {
        return new RouteBuilder() {

            @Override
            public void configure() throws Exception {
                // @formatter:off
                from("sponge:camelRssEngine").routeId("spongeConsumer")
                        .log("Received Camel message: ${body}");

                from("direct:log").routeId("directLog")
                        .log("${body}");
                // @formatter:on
            }
        };
    }

    /**
     * Camel producer template used by Sponge Camel component.
     *
     * @return producer template.
     * @throws Exception Camel context specific exception.
     */
    @Bean
    public ProducerTemplate spongeProducerTemplate() throws Exception {
        return camelContext().createProducerTemplate();
    }
}
Sponge XML configuration file - config.xml
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">

    <properties>
        <!-- News that have less words in the title than specified by this parameter will be rejected by filters. -->
        <property name="newsFilterWordThreshold" variable="true">3</property>
        <!-- Max size of a buffer that stores latest news. -->
        <property name="latestNewsMaxSize" variable="true">5</property>
        <!-- RSS endpoint URI parameters. -->
        <property name="rssEndpointParameters" variable="true">?sortEntries=false&amp;consumer.delay=1000</property>
        <!-- Duration of reading RSS feeds from sources (in seconds). -->
        <property name="durationOfReadingRss" variable="true">5</property>
    </properties>

    <knowledgeBases>
        <knowledgeBase name="config">
            <!-- Extended configuration (more complex data structures than in properties section). -->
            <file>kb/config.py</file>
        </knowledgeBase>
        <!-- Main knowledge base (implemented in Python) that uses 3 files. These files will be loaded by the same interpreter. -->
        <knowledgeBase name="main">
            <!-- Plugin implemented in Python. -->
            <file>kb/main_plugins.py</file>
            <!-- Main event processors. For the sake of clarity registration of event processors is placed in the next file. -->
            <file>kb/main_event_processors.py</file>
            <!-- Knowledge base callback functions: onInit, onLoad, onStartup, etc. -->
            <file>kb/main_functions.py</file>
        </knowledgeBase>
        <!-- Actions knowledge base (implemented in JavaScript). -->
        <knowledgeBase name="actions">
            <file>kb/actions.js</file>
        </knowledgeBase>
        <!-- A knowledge base that simulates lack of new news after a specified time by stopping corresponding Camel routes. -->
        <knowledgeBase name="simulator">
            <file>kb/simulator.py</file>
        </knowledgeBase>
        <!-- As a consumer in Camel routes. -->
        <knowledgeBase name="consumer">
            <file>kb/consumer.py</file>
        </knowledgeBase>
    </knowledgeBases>

    <plugins>
        <!-- Plugin defined in Java. -->
        <plugin name="echoPlugin" class="org.openksavi.sponge.examples.project.camelrssnews.MultiEchoPlugin">
            <configuration>
                <count>2</count>
            </configuration>
        </plugin>

        <!-- Plugin defined in Python. Stores the last news entry. -->
        <plugin name="storagePlugin" class="StoragePlugin" knowledgeBaseName="main">
            <configuration>
                <storedValue>no news yet</storedValue>
            </configuration>
        </plugin>
    </plugins>
</sponge>
Python-based extended configuration - config.py
# Set configuration variables.
# For the sake of clarity setting of configuration variables is done in the main level of the script. This code typically would be
# in onInit() callback function. However, because these are constants, a potential reload (causing this code to be executed once more)
# wouldn't cause any problems.
sponge.setVariable("rssSources", {"BBC":"http://rss.cnn.com/rss/edition.rss", "CNN":"http://feeds.bbci.co.uk/news/world/rss.xml"})
Python-based knowledge base that sends messages to Camel as a consumer - consumer.py
# Sends alarm messages to Camel endpoints in two ways.
class ForwardAlarmTrigger(Trigger):
    def onConfigure(self):
        self.withEvent("alarm")
    def onRun(self, event):
        # Emit the alarm message to all Camel endpoints that use the engine as a consumer.
        camel.emit(event.get("message"))

        # Send the alarm message to a specific endpoint.
        camel.sendBody("direct:log", event.get("message"))

        sponge.getVariable("alarmForwarded").set(True)

19.1.3. REST API Demo Service

The Demo Service use case shows how to deploy the REST API as a servlet. It uses the simple security strategy. There are a few options to run the demo.

Using the hosted Demo Service

The Demo Service hosted at https://spongedemoapi.openksavi.org provides an anonymous access to the Sponge demo. It is used by the Sponge mobile client application as a predefined connection. You can also connect to the service using command line tools.

curl -H "Content-type:application/json" https://spongedemoapi.openksavi.org/version
Running in Docker

The Docker image openksavi/sponge-demo contains a predefined REST API Demo Service running in Tomcat. I can be used as a Sponge mobile client application playground in your local environment.

The predefined file password.txt contains the hashed, insecure password password for the user admin. You should change it or use this demo only for tests in a secure network.
Initialization and configuration
# Run the demo in Docker using the predefined Sponge configuration.
docker run --name sponge-demo -it --rm -p 8080:8080 -p 8081:8081 openksavi/sponge-demo

# Copy the predefined Sponge configuration to the host in order to modify knowledge base files.
docker cp sponge-demo:/opt/sponge/ .

# Stop the running container.
docker stop sponge-demo

# Start a new container using a new Sponge configuration located in the host filesystem.
# The configurtion directory is expected to have the sponge.xml file.
docker run --name sponge-demo -it --rm -p 8080:8080 -p 8081:8081 --mount type=bind,source="$(pwd)/sponge",target=/opt/sponge openksavi/sponge-demo

The port 8080 is used by the REST API and the port 8081 is used by the gRPC API.

Verification
# Test the service.
curl http://localhost:8080/sponge.json/v1/version

# Invoke shell in the container.
docker exec -it sponge-demo /bin/bash
Development process
# Modify the Sponge configuration in the host.
vi sponge_demo.py
# For example add a new action.
class HelloWorld(Action):
    def onConfigure(self):
        self.withLabel("Hello World").withDescription("The action created in a running Docker container.")
        self.withNoArgs().withResult(StringType().withLabel("Greeting"))
        self.withFeature("icon", "human-greeting")
    def onCall(self):
        return "Hello World!"

# Reload the knowledge bases as admin via a commandline tool or the Sponge mobile client application.
curl -X POST -H "Content-type:application/json" http://localhost:8080/sponge.json/v1/reload -d '{"header":{"username":"admin","password":"password"}}'

# Refresh actions in the Sponge mobile client application.
# The new action will be visible in the action list.
Setting up manually and deploying in Tomcat

First, you have to create the web application and Sponge scripts.

cd sponge-examples-projects/sponge-examples-project-demo-service
mvn clean install -Pall

The resulting archive target/sponge-demo-api.war is the web application providing the Demo REST API service. The archive target/sponge-scripts.zip contains Sponge script files and the Digits recognition example files (see the TensorFlow integration chapter) that will be accessed by the web application.

Assuming that Tomcat is installed in /opt/tomcat and the Sponge script files and the Digits recognition example files are extracted into the /opt/tomcat/sponge directory, you should add the following properties to the catalina.properties file:

sponge.home=/opt/tomcat/sponge
digits.home=/opt/tomcat/sponge/digits
password.file=/opt/tomcat/sponge/password.txt
sponge.grpc.port=8081

The sample file password.txt contains the hashed, insecure password password for the user admin. The user admin has access to more actions that the anonymous user. This simple password can be used only for development and tests. In the case of publishing the service, this file should contain the hashed, secret and strong password.

# Create the password file.
sudo echo -n username-password | shasum -a 512 | awk '{ print $1 }' > /opt/tomcat/sponge/password.txt

# Setup privileges.
cd /opt/tomcat
sudo chown -R tomcat:tomcat sponge

# Restart Tomcat.
sudo systemctl restart tomcat.service

Deploy the web application as sponge-demo-api using the Tomcat Web Application Manager. Then test the service.

curl -i -k -X POST -H "Content-type:application/json" http://localhost:8080/sponge.json/v1/version
Running in Jetty

You may also run this example using the Jetty server started by the maven command:

Example of the REST API servlet
cd sponge-examples-projects/sponge-examples-project-demo-service
mvn jetty:run

19.1.4. IoT on Raspberry Pi

The IoT on Raspberry Pi project shows how to use Sponge to read sensors, set actuators, take pictures, send SMS messages, send emails and execute OS commands.

The Sponge standalone command line application is installed on a Raspberry Pi with a GrovePi extension board. Sponge provides a synchronous REST API to remotely call actions (that for example change state of actuators). It also sends sensor data (temperature, humidity and light) to an MQTT broker using Apache Camel. The project allows processing sensor data on two levels: locally on the Raspberry Pi edge device by Sponge (to avoid sending too much data to a management system) or by an external system that connects to the MQTT broker.

The hardware
Table 48. The sensors and actuators connected to the GrovePi
Sensor / actuator Description

DHT sensor

Connected to the port D2.

Light sensor

Connected to the port A1.

Rotary angle sensor

Connected to the port A0.

Sound sensor

Connected to the port A2.

Red LED

Connected to the port D4.

Blue LED

Connected to the port D5.

Buzzer

Connected to the port D7.

LCD RGB Backlight

Connected to the port I2C-1.

Table 49. Other hardware connected to the Raspberry Pi
Name Description

HD Night Vision IR camera

Huawei E3131h-2 modem

Connected via a powered USB hub.

Prerequisites

The Linux distribution used for this example is Raspbian. All command are invoked by the user pi. For SMS sending the gammu utility should be installed.

$ sudo apt-get install gammu
Installation

First you should download and unpack the Sponge standalone command line application into the /home/pi/local/app/ directory. The directory /home/pi/local/app/examples/sponge-iot-rpi (containing the example knowledge base files) should be copied to /home/pi/local/ in order to modify the configuration files in a fresh copy.

The preferred installation is as a systemd service.

$ sudo vim /lib/systemd/system/sponge_iot.service
[Unit]
Description=Sponge IoT Service
After=multi-user.target

[Service]
Type=simple
ExecStart=/bin/bash /home/pi/local/app/sponge-1.12.0/bin/sponge -c /home/pi/local/sponge-iot-rpi/kb/sponge_iot.xml -Dsponge.home=/home/pi/local/sponge-iot-rpi
WorkingDirectory=/home/pi/local/sponge-iot-rpi/
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
$ sudo chmod 644 /lib/systemd/system/sponge_iot.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable sponge_iot.service
Configuration

The sponge_iot.properties file allows the configuration of the service name, the phone number that will receive SMS notifications, the email address for notifications, the temperature threshold to trigger sending an SMS notification, the email client settings and the MQTT broker settings.

Note that the provided password file password.txt stores sample passwords. For each user the password is: password.

REST API Actions

The subset of Sponge actions is published via the Sponge REST API. The published actions have their metadata configured. These actions could be used by the Sponge mobile client application to manage the IoT device using a GUI.

Table 50. The published actions
Name Description

SetGrovePiMode

Sets the GrovePi mode (auto or manual). In the auto mode the device behavior is automated according to the following rules. The LCD display shows the current temperature and humidity. The red LED is turned on if there is dark in the room. The blue LED light depends on the position of the rotary angle sensor. The auto mode is implemented by triggers and correlators. In the manual mode the actuators (LCD, LEDs, etc.) can be managed manually via the published actions.

ManageLcd

Provides management of the LCD properties, i.e. the display text and color.

ManageSensorActuatorValues

Provides management of the sensor and actuator values. Reads the temperature and humidity sensor, the light sensor, the rotary angle sensor and the sound sensor. Sets the values of the LEDs and the buzzer.

TakePicture

Takes a picture using the RPI camera.

SendNotificationEmail

Sends a notification email to the configured recipient.

SendNotificationSms

Sends a notification SMS to the configured recipient.

OsGetDiskSpaceInfo

Executes df -h and returns the disk space info as a markdown string.

OsDmesg

Executes dmesg and returns the output as a markdown string.

The REST API uses the simple security strategy.

MQTT

Sponge publishes the values of temperature, humidity and light sensors to the MQTT topics sponge/temperature, sponge/humidity and sponge/light. The topic prefix can be changed in the configuration.

Modifications in the knowledge bases

After installation, configuration and an initial run you could add your modifications to the knowledge bases. The preferred way to do this is:

  • Temporarily stop and disable the Sponge system service.

  • Run Sponge in an interactive mode in the current console.

$ cd ~/local/sponge-iot-rpi
$ sudo ~/local/app/sponge-1.12.0/bin/sponge -c ~/local/sponge-iot-rpi/kb/sponge_iot.xml -Dsponge.home=. -i iot
  • Open a new shell console to view logs.

$ tail -f ~/local/sponge-iot-rpi/logs/sponge-<current_date>.log
  • Open a new shell console to modify and save the knowledge base files.

  • After saving the knowledge base files, reload the knowledge bases in the interactive mode.

 > sponge.reload()
  • If the changes require restarting Sponge, exit the interactive mode (it stops the Sponge engine) and start Sponge again.

 > exit
 $ sudo ~/local/app/sponge-1.12.0/bin/sponge -c ~/local/sponge-iot-rpi/kb/sponge_iot.xml -Dsponge.home=. -i iot
  • If you modify actions and use the Sponge mobile client application to test the knowledge bases, please remember to refresh the action metadata in the GUI.

  • Repeat these steps until your knowledge bases are finished.

  • Start and enable the Sponge system service.

19.2. Scripting examples

The scripting examples show how to use certain Sponge functionalities in script knowledge bases. See the sources in Python examples, Ruby examples, Groovy examples and JavaScript examples.

Each of these examples is also used in the corresponding JUnit class as a test case with assertions. Note that not all of these examples will work in the standalone application because some of them require additional setup.

Table 51. Scripting examples
Name Description

actions (py, rb, groovy, js)

Shows how to use actions.

correlators (py, rb, groovy, js)

Shows how to use correlators. The correlator creates an event log - a list of events that it listens to.

correlators_duration (py, rb, groovy, js)

Shows how to use correlators with duration.

events_clone_policy (py, rb, groovy, js, XML configuration)

Shows event clone policies.

events_cron (py, rb, groovy, js)

Shows sending events using Cron.

events_removing (py, rb, groovy, js)

Shows how to remove scheduled events.

filters_deduplication (py, rb, groovy, js)

Shows how to use a deduplication filter to prevent from processing many events that carry the same information.

filters_java (py, rb, groovy, js)

Shows how to use a Java-based filter.

filters (py, rb, groovy, js)

Shows how to use script-based filters.

actions_hello_world (py, rb, <groovy, js, XML configuration)

Hello world action complete example.

triggers_hello_world (py, rb, <groovy, js, XML configuration)

Hello world trigger complete example.

knowledge_base_callbacks (py, rb, groovy, js)

Shows how to use knowledge base callback functions.

knowledge_base_load (py, rb, groovy, js)

Shows how to load an additional knowledge base file.

knowledge_base_manager (py, rb, groovy, js)

Shows knowledge base operations.

library (py, rb, groovy, js, XML configuration)

Shows how to use a scripting language specific library (e.g. httplib for Python) to check HTTPS host status.

plugins_java (py, rb, groovy, js, XML configuration)

Shows how to define and use a Java-based plugin.

plugins_kb (py, rb, groovy, js, XML configuration)

Shows how to define and use a script-based plugin.

rules (py, rb, groovy, js)

Shows how to define and use ordered rules, i.e. rules listening to ordered sequences of events. Event conditions are specified using lambda expressions as well as class methods.

rules_events (py, rb, groovy, js, XML configuration)

Shows how to define and use rules that have different event modes, durations etc.

rules_heartbeat (py, rb, groovy, js)

Heartbeat complete example.

rules_none_mode_events_conditions (py, rb, groovy, js)

Shows how to define and use rules that have none event mode and event conditions.

rules_none_mode_events (py, rb, groovy, js)

Shows how to define and use rules that have none event mode.

unordered_rules (py, rb, groovy, js)

Shows how to define and use unordered rules, i.e. rules listening to unordered sequences of events. Event conditions are specified using lambda expressions as well as class methods.

triggers (py, rb, groovy, js, XML configuration)

Shows how to define and use triggers.

triggers_event_pattern (py, rb, groovy, js)

Shows how to define and use triggers that specify events they listen to as a pattern based on a regular expression.

19.3. Features examples

The features examples show how to use some of Sponge features. They are not implemented in all supported scripting languages.

Table 52. Features examples
Name Description

fibonacci (py)

Shows how to send a chain of events, each carrying a Fibonacci number as an attribute.

engine_parameters (xml)

Shows how to set engine parameters in the XML configuration file.

event_pattern (py)

Shows how to use event name patterns and how to enable/disable processors manually.

spring (py, java)

Shows how to integrate with Spring framework.

camel_producer (py, java)

Shows how to handle messages coming from Apache Camel route by a Sponge trigger.

camel_consumer (py, java)

Shows how to handle messages coming from Sponge by an Apache Camel route.

camel_rss (py, java)

Shows how to integrate with Apache Camel to send and handle Sponge events based on RSS feeds. This example uses a Spring configuration.

camel_producer_overridden_action (py, java)

Shows how to handle messages coming from Apache Camel route by a Sponge trigger using an overridden Camel producer action.

camel_producer_custom_action (py, java)

Shows how to handle messages coming from Apache Camel route by a Sponge trigger using a custom Camel producer action.

camel_multiple_consumer (py, java)

Shows sending Camel messages to many endpoints in a single Sponge trigger.

py4j_java_server (cpython, xml, jython)

Shows how to integrate with CPython program using Py4J - Java server.

py4j_python_server (cpython, xml, jython, java)

Shows how to integrate with CPython program using Py4J - Python server.

py4j_java_server_tls (cpython, xml, jython)

Shows how to integrate with CPython program using Py4J - Java server with TLS security.

midi_generate_sound (py)

Shows how to generate MIDI sounds in a Sponge knowledge base.

midi_input (py)

Shows how to process MIDI messages created by an external MIDI input device.

midi_play_file (py)

Shows how MIDI messages created by a MIDI sequencer playing a MIDI file could be processed in a Sponge knowledge base.

19.4. Standalone examples

The standalone examples show how to use some of Sponge features in the standalone command-line application.

Table 53. Standalone examples
Name Description

standalone_news (sources)

This example is based on complete example project of embedding Sponge - News, but adjusted to a standalone version.

standalone_camel_rss_news (sources)

This example is based on complete example project of embedding Sponge - Camel RSS News, but adjusted to a standalone version.

camel_route_groovy (sources)

Camel routes in Groovy Spring configuration.

camel_route_xml (sources)

Camel context and routes in XML Spring configuration.

20. Maven artifacts

The groupId of Sponge Maven artifacts is org.openksavi.sponge.

Table 54. Sponge Maven artifacts
ArtifactId Central Maven Repository Description

sponge-parent

Yes

The parent project.

sponge-bom

Yes

The Bill Of Materials style pom.xml.

sponge-api

Yes

The Sponge API.

sponge-core

Yes

The Sponge core implementation. This artifact includes a shaded Guava library.

sponge-jython

Yes

The support for Python-based scripting knowledge bases using Jython.

sponge-jruby

Yes

The support for Ruby-based scripting knowledge bases using JRuby.

sponge-groovy

Yes

The support for Groovy-based scripting knowledge bases.

sponge-nashorn

Yes

The support for JavaScript-based scripting knowledge bases using Nashorn.

sponge-kotlin

Yes

The support for Kotlin-based non scripting knowledge bases.

sponge-signal

Yes

The wrappers for Operating System signals.

sponge-camel

Yes

The Apache Camel integration.

sponge-spring

Yes

The Spring framework integration.

sponge-py4j

Yes

The CPython integration that uses Py4J.

sponge-midi

Yes

The MIDI integration.

sponge-rpi-pi4j

Yes

The Pi4J (for Raspberry Pi) library integration.

sponge-reactivex

Yes

The ReactiveX integration.

sponge-features

Yes

The predefined action and type features.

sponge-rest-api-client

Yes

The Sponge REST API client.

sponge-rest-api-server

Yes

The Sponge REST API server.

sponge-grpc-api-client

Yes

The Sponge gRPC API client.

sponge-grpc-api-server

Yes

The Sponge gRPC API server.

sponge-remote-api-server

Yes

The Sponge Remote API server (has dependencies on the REST and the gRPC API servers).

sponge-tensorflow

Yes

The TensorFlow integration.

sponge-standalone

Yes

The standalone version of Sponge.

sponge-standalone-extensions

Yes

Dependencies for external libraries used by the standalone command-line application.

sponge-logging

Yes

The Sponge logging used by the standalone application.

sponge-test

Yes

The Sponge test support.

sponge-rpi-grovepi

No

The GrovePi (for Raspberry Pi) library integration.

sponge-examples-projects

No

Complete example projects.

sponge-distribution

No

Contains documentation, release configuration, project pages etc.

sponge-integration-tests

No

Sponge integration tests.

21. Standalone command-line application

For a brief introduction to the Sponge standalone command-line application see Quickstart.

If you need additional libraries (e.g. Camel components) you should place JAR files into the lib directory. You should use only compatible versions of these libraries.

Standalone command-line application doesn’t support history of entered commands/expressions (i.e. upwards arrow doesn’t work).

21.1. Command-line options

Option Description

-c <arg>

Use given Sponge XML configuration file. Only one configuration file may be provided.

-k [name=]files

Use given knowledge base by setting its name (optional) and files (comma-separated). When no name is provided, a default name 'kb' will be used. This option may be used more than once to provide many knowledge bases. Each of them could use many files.

-s <file>

Use given Spring configuration file. This option may be used more than once to provide many Spring configuration files.

-m

Create an Apache Camel context.

-i [name]

Run in an interactive mode by connecting to a knowledge base interpreter. You may provide the name of one of the loaded knowledge bases, otherwise the first loaded knowledge base will be chosen.

-e

Applicable only in an interactive mode. Print all exceptions (e.g. also thrown in event processors running in other threads). Helpful for development purposes.

-h

Print help message and exit.

-v

Print the version information and exit.

-D property=value or -Dproperty=value

Set the Java system property.

21.2. Default parameters

Standalone command-line application sets its own default values for the following engine configuration parameters. You may change them in an XML configuration file.

Parameter Value

mainProcessingUnitThreadCount

10

asyncEventSetProcessorExecutorThreadCount

Same as mainProcessingUnitThreadCount

eventQueueCapacity

100000

Examples
# Change directory to Sponge bin/.

# Run with the specified Sponge XML configuration file.
./sponge -c ../examples/script/py/triggers_hello_world.xml

# Run with the knowledge base named 'helloWorldKb' using the specified knowledge base file.
./sponge -k helloWorldKb=../examples/script/py/triggers_hello_world.py

# Run with the knowledge base named 'kb' using the specified knowledge base file.
./sponge -k ../examples/script/py/triggers_hello_world.py

# Run with two knowledge bases.
./sponge -k filtersKb=../examples/script/py/filters.py -k heartbeatKb=../examples/script/js/rules_heartbeat.js

# Run in an interactive mode.
./sponge -k filtersKb=../examples/script/py/filters.py -i

# Run in an interactive mode and setup printing all exceptions to the console.
./sponge -k filtersKb=../examples/script/py/filters.py -i -e

# Run one knowledge base that use two files. Take caution not to use the same names for functions or classes in the files belonging to the same knowledge base.
./sponge -k ../examples/standalone/multiple_kb_files/event_processors.py,../examples/standalone/multiple_kb_files/example2.py

21.3. Environment variables

Optionally you may set the environment variable SPONGE_HOME.

Linux/MacOS/Unix
cd root/sponge-standalone
export SPONGE_HOME=`pwd`
Windows
cd root/sponge-standalone
set SPONGE_HOME=%cd%

21.4. Standalone plugin configuration parameters

Table 55. Standalone plugin configuration parameters
Name Type Description

spring

XML element

Spring configuration. A Spring context is created only when there is a spring configuration element present.

engineBeanName

String

The optional engineBeanName attribute of the spring element defines a Spring bean name that will reference the engine instance in the Spring context. The default value is spongeEngine.

camel

Boolean

The optional camel attribute of the spring element may be used to create a Camel context.

spring/file

String

Spring configuration files. The Spring context implementation used here is GenericGroovyApplicationContext, that allows to load XML and Groovy configuration files.

21.5. Spring

You may provide Spring configuration files using a command-line option or defining StandalonePlugin plugin in Sponge XML configuration file. This plugin allows to specify Spring configuration files that will be loaded. The name of this plugin must be "standalonePlugin".

Example of Spring configuration in StandalonePlugin
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">

    <plugins>
        <plugin name="standalonePlugin" class="org.openksavi.sponge.standalone.StandalonePlugin">
            <configuration>
                <spring engineBeanName="someEngine">
                    <file>spring-context-example-file-1.xml</file>
                    <file>spring-context-example-file-2.xml</file>
                    <file>SpringContextExample3.groovy</file>
                </spring>
            <configuration>
        </plugin>
    </plugins>
</sponge>

This standlonePlugin sets up the Spring configuration XML file and Spring bean name that will reference the engine instance.

21.6. Camel

If you want to use Camel, you could setup a predefined Camel context configuration, so that a Camel context will be created automatically.

Available options are:

  • Setting <spring camel="true"> will create a Camel context using a predefined Spring Java configuration.

  • Using <spring> without setting camel attribute will not create any Camel context automatically. In that case you may setup a Camel context in a custom way (for example using Spring).

You may use only one Camel context in the Sponge standalone command-line application.

You could use Camel routes to send events to Sponge from an external systems, for example by configuring Camel Rest DSL.

21.6.1. Spring XML configuration

Example of Spring configuration in StandalonePlugin
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">

    <plugins>
        <plugin name="standalonePlugin" class="org.openksavi.sponge.standalone.StandalonePlugin">
            <configuration>
                <spring camel="true">
                    <file>examples/standalone/camel_route_xml/spring-camel-xml-config-example.xml</file>
                </spring>
            </configuration>
        </plugin>
    </plugins>
</sponge>
Camel configuration in Spring XML (spring-camel-xml-config-example.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://camel.apache.org/schema/spring
                           http://camel.apache.org/schema/spring/camel-spring.xsd">

    <camelContext xmlns="http://camel.apache.org/schema/spring">
        <route id="spongeConsumerXmlSpringRoute">
            <from uri="sponge:spongeEngine" />
            <log message="XML/Spring route - Received message: ${body}" />
        </route>
    </camelContext>
</beans>

21.6.2. Spring Groovy configuration

Spring container plugin in Sponge configuration file example
<?xml version="1.0" encoding="UTF-8"?>
<sponge xmlns="https://sponge.openksavi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://sponge.openksavi.org https://sponge.openksavi.org/schema/config.xsd">

    <plugins>
        <plugin name="standalonePlugin" class="org.openksavi.sponge.standalone.StandalonePlugin">
            <configuration>
                <spring camel="true">
                    <file>examples/standalone/camel_route_groovy/SpringCamelGroovyConfigExample.groovy</file>
                </spring>
            </configuration>
        </plugin>
    </plugins>
</sponge>
Camel configuration in Spring Groovy (SpringCamelGroovyConfigExample.groovy)
import org.apache.camel.builder.RouteBuilder;

class GroovyRoute extends RouteBuilder {
    void configure() {
        from("sponge:spongeEngine").routeId("spongeConsumerCamelGroovySpring")
                .log("Groovy/Spring route - Received message: \${body}");
    }
}

beans {
    route(GroovyRoute)
}

21.6.3. Management of Camel routes in an interactive mode

Console - print camel status and routes
> print(camel.context.status)
> print(camel.context.routes)
Console - stop and remove a Camel route
> camel.context.stopRoute("rss")
> print(camel.context.removeRoute("rss"))
> print(camel.context.routes)

21.7. Logging and exception reporting

21.7.1. Non interactive mode

If you experience too many logs in the console while running a non-interactive standalone command-line application, you may want to change a logging configuration in config/logback.xml. For example to change a console threshold filter level from INFO to ERROR:

Example logging configuration
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>ERROR</level>
    </filter>

To provide a custom logging configuration you may use the -D option according to the Logback documentation.

Custom logging configuration
./sponge -c ../examples/script/py/triggers_hello_world.xml -Dlogback.configurationFile=custom_logback.xml

21.7.2. Interactive mode

In an interactive mode a predefined console logger appender (configured in config/logback.xml) is turned off programmatically.

Exceptions thrown from other threads of the Sponge engine are not printed into the console. You may change that behavior by specifying -e command-line option.

21.8. REST API

You may enable the Sponge REST API in the standalone command line application but such configuration will provide no user management and a very limited security. Thus it can be used only in a secure network or for test purposes.

Manual start of the REST API (autoStart must be turned off) is required because the REST API server must start after the Camel context has started.

For more information see examples in the source code.

21.9. Running examples

News example
# Change directory to Sponge bin/.

# Run with the specified Sponge XML configuration file.
./sponge -c ../examples/standalone/news/config/config.xml
Camel RSS News example
# Change directory to Sponge bin/.

# Run with the specified Sponge XML configuration file.
./sponge -c ../examples/standalone/camel_rss_news/config/config.xml

21.10. Directory structure

Table 56. Directory structure
Directory Description

bin

Shell scripts.

config

Configuration files.

docs

Documentation.

examples

Example configurations and knowledge base files.

lib

Libraries used by Sponge.

logs

Log files.

21.11. Extension components

The extension components are included in the Sponge standalone command-line application distribution and could be used out of the box in Sponge knowledge bases.

21.11.1. Camel components and data formats

Besides Camel core components and data formats, Sponge standalone command-line application provides also a selected set of other Camel components and data formats ready to use.

Table 57. Camel components
Component Description

camel-exec

Executing system commands

camel-grape

Grape

camel-http4

HTTP

camel-mail

Mail

camel-jdbc

JDBC

camel-jms

JMS

camel-jmx

JMX

camel-mqtt

MQTT

camel-mustache

Mustache

camel-netty4

Netty

camel-netty4-http

Netty HTTP

camel-paho

Paho/MQTT

camel-quartz2

Quartz

camel-rss

RSS

camel-snmp

SNMP

camel-sql

SQL

camel-ssh

SSH

camel-stream

Input/output/error/file stream

camel-velocity

Velocity

camel-xmpp

XMPP/Jabber

Table 58. Camel data formats
Data format Description

camel-jackson

JSON

camel-csv

CSV

camel-tarfile

Tar format

camel-syslog

Syslog

21.11.2. Other components

Table 59. Other components

Component

Description

Commons Email

Provides an API for sending emails.

22. Third party software

Sponge uses or supports third party software released under various open-source licenses.

Table 60. Key third party software
Package License Description

Apache Commons Configuration

Apache 2.0

Used for reading configuration files.

Apache Commons Email

Apache 2.0

Used for sending emails.

Jython

Jython license

Supports writing scripting knowledge bases in Python.

JRuby

EPL 1.0, GPL 2 or LGPL 2.1

Supports writing scripting knowledge bases in Ruby.

Groovy

Apache 2.0

Supports writing scripting knowledge bases in Groovy.

Nashorn

GPL with a linking exception

Supports writing scripting knowledge bases in JavaScript.

Kotlin

Apache 2.0

Supports writing knowledge bases in Kotlin.

Quartz

Apache 2.0

Used for scheduling events (e.g. provides cron functionality).

Apache Camel

Apache 2.0

Used as an integration facade to external systems.

Spring framework

Apache 2.0

Used for integration with Spring framework.

Guava

Apache 2.0

Used for services and as a utilities library.

JLine

BSD

Used for handling console input in an interactive mode.

Py4J

BSD

Used for integration with CPython programs.

Pi4J

LGPL 3.0

Used for integration with Raspberry Pi hardware.

GrovePi

Apache 2.0

Used for integration with GrovePi (for Raspberry Pi) hardware.

TensorFlow

Apache 2.0

An open source machine learning framework.

RxJava

Apache 2.0

RxJava is a Java VM implementation of Reactive Extensions.

Reflections

WTFPL

Reflections is a Java runtime metadata analysis library.

JJWT

Apache 2.0

JSON Web Token for Java.

The complete list of these libraries may be found in the THIRD-PARTY.txt and licenses.xml files of the standalone distribution.