User Guide
- 1. Introduction
- 2. Architecture
- 3. Configuration
- 4. Engine
- 5. Knowledge bases
- 5.1. Knowledge base file structure
- 5.2. Callback functions
- 5.3. Global variables
- 5.4. User variables
- 5.5. Engine facade
- 5.6. Loading knowledge base files
- 5.7. Loading knowledge base from an additional file
- 5.8. Reloading
- 5.9. Use of many knowledge base files
- 5.10. Synchronization of processes in a knowledge base
- 5.11. Non script knowledge bases
- 5.12. Scripting knowledge bases interoperability
- 5.13. Useful knowledge base functions
- 5.14. Predefined knowledge base libraries
- 5.15. Knowledge base artifacts
- 5.16. Knowledge base versioning
- 5.17. Naming conventions
- 5.18. Categories
- 5.19. Extending Java-based processors and plugins in non-Java knowledge bases
- 6. Data types
- 7. Features
- 8. Events
- 9. Processors
- 10. Actions
- 11. Event processors
- 12. Filters
- 13. Triggers
- 14. Rules
- 15. Correlators
- 16. Plugins
- 17. Exception handling
- 18. Embedding Sponge in custom applications
- 19. Integration
- 19.1. Spring framework
- 19.2. Spring Boot
- 19.3. Apache Camel
- 19.4. Sponge Remote API
- 19.5. Sponge Remote API server
- 19.6. Sponge Remote API client for Java
- 19.7. Sponge Remote API client for Dart
- 19.8. Sponge gRPC API
- 19.9. Sponge gRPC API server
- 19.10. Sponge gRPC API client for Java
- 19.11. Sponge gRPC API client for Dart
- 19.12. Running external processes
- 19.13. Python (CPython) / Py4J
- 19.14. ReactiveX
- 19.15. MIDI
- 19.16. Raspberry Pi - Pi4J
- 19.17. Raspberry Pi - GrovePi
- 19.18. TensorFlow
- 19.19. GSM modem
- 20. Best practices
- 21. Scripting languages
- 22. Logging
- 23. Standard plugins
- 24. Predefined knowledge base artifacts
- 25. Applications
- 26. Examples
- 27. Maven artifacts
- 28. Standalone command-line application
- 29. Third party software
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.
The Sponge engine consists of the following 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:
<?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
.
Property | Description |
---|---|
|
The Sponge home directory. The Sponge home directory may also be provided as a Java system property |
|
The configuration file directory. This property is read only. It may be |
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
. If not found in the configuration file directory, the properties file is looked up using the same strategy as the configuration file. 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 and a label of the Sponge engine. You can set an engine description and a license as well.
The most important parameters are mainProcessingUnitThreadCount
, asyncEventSetProcessorExecutorThreadCount
and eventQueueCapacity
.
Parameter | Description | Default value |
---|---|---|
|
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. |
|
|
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 |
|
|
The capacity (i.e. maximum size) of the Input Event Queue. Value |
|
|
Event clone policy ( |
|
|
Auto-enable script-based processors. |
|
|
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. |
|
|
The event set processor default synchronous flag. If this parameter is set to |
|
|
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 |
|
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:
Tag | Type | Description |
---|---|---|
|
Attribute |
The name of the knowledge base. |
|
Attribute |
The knowledge base label. |
|
Attribute |
The type of the script knowledge base corresponding to the scripting language. Allowed values: |
|
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. |
|
Attribute |
The flag indicating if the knowledge base should be cleared on reload. Defaults to |
|
Element |
The description of the knowledge base. |
|
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 tofalse
, the non existing files are ignored. The default value istrue
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:
Tag | Type | Description |
---|---|---|
|
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. |
|
Attribute |
The plugin label. |
|
Attribute |
The name of the plugin class (Java class or a class defined in the scripting language in the script knowledge base (mandatory). |
|
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. |
|
Element |
The plugin description. |
|
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.
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.
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.
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.
engine.shutdown();
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:
-
Import of modules and packages (from the scripting language or Java).
-
Definitions of knowledge base processors (actions, filters, triggers, rules and correlators).
-
Definitions of callback functions that will be invoked in particular situations.
5.2. Callback functions
Function | Description |
---|---|
|
Called once on the initialization of a knowledge base after the knowledge base files have been read. |
|
Called just before |
|
Called on the loading of a knowledge base and also every time a knowledge base is reloaded. Before invoking an |
|
Called just after |
|
Called once after the startup of the engine (after |
|
Called just after an |
|
Called once before the shutdown of the engine. |
|
Called before every reloading of the knowledge base. |
|
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:
-
executing all knowledge base files as scripts, i.e. executing the main body of the script files,
-
onInit()
, -
onBeforeLoad()
, -
onLoad()
, -
onAfterLoad()
, -
onStartup()
, -
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:
-
onBeforeReload()
executed in the previous version of the reloaded knowledge base, -
executing all knowledge base files as scripts, i.e. executing the main body of the script files,
-
onBeforeLoad()
executed in the new version of the reloaded knowledge base, -
onLoad()
executed in the new version of the reloaded knowledge base, -
onAfterLoad()
executed in the new version of the reloaded knowledge base, -
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.
Global variable | Description |
---|---|
|
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 |
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.
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.
hearbeatEventEntry = None
def onStartup():
global hearbeatEventEntry
hearbeatEventEntry = sponge.event("heartbeat").sendAfter(100, 1000)
5.5. Engine facade
Property / Method | Description |
---|---|
|
The knowledge base to which belongs the script using this variable. This value represents an object of a Java class implementing |
|
The knowledge base interpreter that has read the script using this variable. Generally it is an implementation of |
|
The engine. This is the reference to the actual implementation of the |
|
The logger instance associated with the knowledge base. The name of this logger has the following format: |
|
Enables the processor. |
|
Enables processors. |
|
Disables the processor. |
|
Disables processors. |
|
Enables the Java-based processor. |
|
Enables Java-based processors. |
|
Disables the Java-based processor. |
|
Disables Java-based processors. |
|
Calls registered action with arguments. |
|
Calls the action if it exists. Returns the action result wrapped in a value holder or |
|
Provides action arguments. Submits arguments and/or returns provided values along with value sets. |
|
Shuts down the engine using the current thread. |
|
Shuts down the engine using another thread. |
|
Reloads script-based knowledge bases. |
|
Reloads script-based knowledge bases using another thread. |
|
Removes the scheduled event. |
|
Returns the plugin that has the specified name. Throws exception if not found. |
|
Returns the plugin that has the specified name and type. Throws exception if not found. |
|
Returns the plugin that has the specified type. Throws exception if not found. |
|
Creates a new event definition. |
|
Creates a new event definition. |
|
Creates a new event definition. |
|
A set of methods returning |
|
Sets the engine scope variable. |
|
Returns the value of the engine scope variable. Throws exception if not found. |
|
Returns the value of the engine scope variable. Throws exception if not found. |
|
Returns the value of the engine scope variable or |
|
Returns the value of the engine scope variable or |
|
Removes the engine scope variable. |
|
Returns |
|
Sets the engine scope variable if not set already. |
|
The read-only property whose value is the engine version. |
|
The read-only property whose value is the engine description. |
|
The read-only property whose value is the engine statistics summary as a text. |
|
|
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 SpringPathMatchingResourcePatternResolver
, -
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 SPonge ARchive. The spar
protocol accepts wildcards both in an archive content path and in an archive file path itself.
spar
protocolSpongeEngine 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.8.1. The clearOnReload
flag
A scripting knowledge base can have the clearOnReload
flag set. This flag indicates if the knowledge base should be cleared on reload. Defaults to false
. If the knowledge base is to be cleared on reload, then all existing processors registered in this knowledge base will be disabled and a new interpreter will be created and attached to the knowledge base.
Setting the clearOnReload to true can be potentially dangerous for the consistency of the knowledge base, especially if there are event processors. So make sure that your knowledge base will not be affected by this flag.
|
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
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.18.0</version>
</dependency>
5.11.2. Kotlin knowledge bases
Kotlin-based knowledge bases are currently supported only as non script knowledge bases.
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.18.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 functions
sponge.event("alarm").set("severity", 10).send()
print sponge.engine.triggers
print sponge.engine.ruleGroups
print sponge.engine.ruleGroups[0].rules
print sponge.engine.correlatorGroups
sponge.requestShutdown()
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 artifacts
Knowledge bases artifacts provide selected functionalities that can be used in your projects ad hoc. They are intended for more advanced cases than the predefined knowledge base libraries.
A knowledge base artifact can be written in only one scripting language. It can be used as a part of a bigger knowledge base, but other script files must be written in the same language.
5.15.1. Usage
Embedded
<dependency>
<groupId>org.openksavi.sponge</groupId>
<artifactId>[ArtifactId]</artifactId>
</dependency>
<knowledgeBase ...>
<file>classpath*:sponge/[Name]/**/*.py</file>
</knowledgeBase>
For example <file>classpath*:sponge/mpd-mpc/*/.py</file>
.
Standalone commadline application
If a knowledge base artifact is included in the standalone commadline application, add the following entry to your Sponge configuration.
<knowledgeBase ...>
<file>classpath*:sponge/[Name]/**/*.py</file>
</knowledgeBase>
Otherwise add the following entry.
<knowledgeBase ...>
<file>spar:[ArtifactId]-*.jar!/**/*.py</file>
</knowledgeBase>
For example <file>spar:sponge-kb-mpd-mpc-*.jar!/\**/\*.py</file>
, where the sponge-kb-mpd-mpc-*.jar
is a file in the filesystem.
5.16. 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 Remote 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.
def onLoad():
sponge.kb.version = 1
5.17. Naming conventions
The builder-style methods in the metadata classes follow the naming convention with<Property>
, e.g. BinaryType().withMimeType("image/png")
.
5.18. 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 will be automatically assigned to a corresponding processor when it is being enabled by using the category predicate. However if a processor has its category already set in the processor configuration callback method onConfigure
it will not be changed.
The automatic assigning of categories to processors is supported only for actions, filters and triggers. It is not supported for event set processors. |
Categories can be registered by the addCategories
or addCategory
methods. They should be invoked before enabling of processors, i.e. in most cases in the onInit
callback function.
def onInit():
sponge.addCategories(
CategoryMeta("basic").withLabel("Basic").withPredicate(lambda processor: processor.kb.name in ("demo", "engine")),
CategoryMeta("extra").withLabel("Extra").withPredicate(lambda processor: processor.kb.name in ("demoExtra"))
class MyAction1(Action):
def onConfigure(self):
self.withLabel("MyAction 1").withCategory("myActions")
def onCall(self, text):
return None
5.19. 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.
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.
Property | Description |
---|---|
|
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. |
|
An optional type location label. For example an action argument label or a record field label. |
|
An optional type location description. |
|
A flag that tells if a value of this type is annotated. Defaults to |
|
An optional format. |
|
An optional default value. A default value for an annotated type must be wrapped in an |
|
Tells if a value of this type can be |
|
A flag specifying if this type is read only. Defaults to |
|
Optional features as a map of names to values (as the |
|
A flag specifying if a value in a location corresponding to this type is optional. Defaults to |
|
The provided type specification as an instance of the |
A type properties should be set using the builder-style methods, e.g. StringType("id").withLabel("Identifier")
.
Property | Description |
---|---|
|
A flag that specifies if this type value is provided. An action should implement the |
|
Metadata that specifies if a value set is provided. Defaults to |
|
A flag that specifies if the list element value set is provided. Applicable only for list types. Defaults to |
|
A list of type names of name paths that this type depends on. |
|
A flag that specifies if the provided value of this type should overwrite a value modified in a client code (in most cases by a user). Defaults to |
|
A metadata that specifies if a value can be submitted (i.e. written ad hoc), for example in an action, irrespectively of an action call. |
|
A flag that specifies if a provided value should be updated lazily in a client code, i.e. its previous value should be retained until the new value is obtained. This flag is experimental. |
|
A flag that specifies if a current value in a client code should be passed to a server when its new value is to be provided. |
|
A provided read mode: |
Property | Description |
---|---|
|
A list of object names that a submitted object influences (i.e. can change their values when submitted). |
See the provided action arguments as a use case of provided types.
Type | Description |
---|---|
|
An any type. It may be used in situations when type is not important. |
|
A binary (byte array) type. Provides an optional property |
|
A boolean type. |
|
A date/time type. This type requires a |
|
An dynamic type representing dynamically typed values. A value of this type has to be an instance of |
|
An integer type (commonly used integer type or long). Provides optional properties |
|
A list type. This type requires a |
|
A map type. This type requires two |
|
A number type, that include both integer and floating-point numbers. Provides optional properties |
|
An object. This type requires a class name (typically a Java class name). For example: |
|
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.: |
|
An output stream type. It can be used only as a result of an action. A value of this type has to be an instance of |
|
An input stream type. It can be used only as an argument of an action. A value of this type has to be an instance of |
|
A string type. Provides optional properties |
|
A type representing a data type. A value of this type has to be an instance of |
|
A void type that may be used to specify that an action returns no result. |
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.
Method | Description |
---|---|
|
Registers a data type by providing a type supplier that will create a new instance of the registered type each time, e.g. |
|
Returns a new instance of the registered data type. |
|
Returns a new instance of the registered data type setting the returned type location name as well. |
|
Returns the unmodifiable map of registered data types. |
Data types should be registered in the onBeforeLoad
, because it is invoked before scanning processors.
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.
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")
]))
6.4. Annotated values
An annotated value is wrapped by an instance of the AnnotatedValue
class. An annotated value allows passing a value label, a value description, features, type label and type description along with the value, e.g. AnnotatedValue(imageBytes).withValueLabel("Image1").withFeatures({"filename":imageFilename})
, where the first property is the annotated value, the second is the value label and the third is the features map that may be used in a client code.
Annotated values can be used to alter static, metadata-drived behavior of a client application. For example they are especially useful as provided action argument values that carry additional, dynamic data as features.
Property | Description |
---|---|
|
A value that is annotated. |
|
An optional value label. It can be used as a string representation of the value in a GUI. |
|
An optional value description. It can be used as a description of the value in a GUI. |
|
Features as a map of names to values. |
|
An optional type label. Overwrites the type label in a client code. |
|
An optional type description. Overwrites the type description in a client code. |
If a data type is annotated and has a default value, the default value should be wrapped in an AnnotatedValue
.
6.5. Object type companion type
Limitations:
-
Conversion between a companion type value and an object type value is supported only in the Sponge Remote API.
-
Loops in nested data types are not supported.
-
Object inheritance is not supported (if a companion type is a record type).
-
A companion type is not validated against the object type class.
7. Features
Features are represented by a map of names to values. They provide additional information to data types, processors and other entities. 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 customized behavior.
A feature can have a feature converter registered for its name. The converter is used by the Remote API. It allows to use complex feature values in knowledge bases (e.g. "icon":IconInfo().withName("home").withColor("FF0000").withSize(50)
) that will be marshalled and unmarshalled to and from JSON. If a feature has a converter, all usages of this feature will have to conform to a feature type supported by the converter (e.g. the "icon"
feature converter supports an instance of IconInfo
or String
as a value).
If a feature is used in events, its values should be Serializable
.
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
Property / Method | Description |
---|---|
|
A property that is a global unique identifier of an event (String). This is a shortcut for |
|
A read-only property that is the name (type) of an event. This is a shortcut for the |
|
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 |
|
An optional event label. |
|
An optional event description. |
|
A method that allows setting an attribute of an event. |
|
A method that returns a value of an attribute or throws |
|
A method that returns a value of an attribute (assuming it is an instance of |
|
A method that returns a value of an attribute or |
|
A method that checks if an event has an attribute. |
|
A property that returns a map of all attributes. This is a shortcut for the |
|
Event features. |
|
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.
Event name | Description |
---|---|
|
The |
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.
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.
Method | Description |
---|---|
|
Sets the event attribute. |
|
Add an event feature. |
|
Add event features. |
|
Modifies the underlying event. |
|
Sends an event immediately. |
|
Sends an event after a specified time (given in milliseconds or as a |
|
Periodically sends events after a specified time (given in milliseconds or as a |
|
Sends an event at the specified time (given in milliseconds as the number of milliseconds since 01/01/1970 or as an |
|
Periodically sends events starting at the specified time (given in milliseconds as the number of milliseconds since 01/01/1970 or as an |
|
Sends events at a time specified by a Cron compatible time entry (supports Quartz format). |
|
Periodically sends events every |
|
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.
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.
-
-
A processors can be defined by a class or by a processor builder.
9.1. Processors defined by classes
9.1.1. Definition
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.1.2. Enabling
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.
Auto-enable
Sponge automatically enables all processors (i.e. actions, filters, triggers, rules and correlators) defined as classes 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.
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.
# 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.
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.
def onLoad:
sponge.enable(TriggerA)
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.
def onLoad():
sponge.enableJava(SameSourceJavaRule)
def onLoad():
sponge.enableJavaAll(SameSourceJavaRule, SameSourceJavaRule2, SameSourceJavaRule3)
def onLoad():
sponge.enableJavaByScan("org.openksavi.sponge.integration.tests.core.scanning")
9.1.3. Disabling
Processors can 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.
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()
.
def onLoad():
sponge.disableJava(SameSourceJavaRule)
Any processor can be disabled by its name.
sponge.disable("EchoAction")
9.2. Processors defined by processor builders
In order to define your processor by a processor builder you have to configure it using a builder fluter API and manually enable in the engine. A name of a processor is a text passed to a builder constructor.
def onLoad:
sponge.enable(ActionBuilder("HelloWorldAction").withOnCall(lambda action, name: "Hello World! Hello {}!".format(name)))
A processor defined by a processor builder can be disabled by its name.
9.3. Properties and methods
Property / Method | Description |
---|---|
|
The configuration callback method that will be invoked when a processor is being enabled. This method is mandatory for processors defined as classes. |
|
The initialization callback method that will be invoked after |
|
Sets a processor name. The name can be read using |
|
Sets a processor label. The label can be read using |
|
Sets a processor description. The description can be read using |
|
Sets a processor version. The version can be read using |
|
Adds processor features. The features can be read using |
|
Adds a single processor feature. |
|
Sets a processor category. The category can be read using |
|
The read-only property that provides a processor metadata. |
|
The read-only property that provides a processor logger. This is a shortcut for |
|
The read-only property that provides a processor adapter. This is a shortcut for |
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()
.
Property / Method | Description |
---|---|
|
Configures the |
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.
Property / Method | Description |
---|---|
|
Sets action arguments metadata as data types. The action argument types can be read using |
|
Sets a single action argument metadata as a type. |
|
Defines that an action has no arguments. |
|
Sets an action result type. The action result type can be read using |
|
Defines that an action has no result, i.e. it’s result is of |
|
Sets a callable flag for the action. A callable action must have an |
|
Sets the action to be non callable. |
|
Sets an activatable flag for the action. An activatable action should have an |
|
A dynamic callback method that should be defined in a callable action. It will be invoked when an action is called, e.g.: |
|
A callback method that informs if the action in a given context is active. If this method is implemented in an action, an activatable flag should to be set as well. |
|
A callback method that provides argument values along with argument value sets (i.e. possible values of an argument) and/or submits arguments. The provided arguments are explained later in this document. |
The onConfigure
method in actions is not mandatory.
Property / Method | Description |
---|---|
|
Configures the |
|
Configures the |
|
Configures the |
|
Configures the |
Example in a script language
The code presented below defines an action named EchoAction
that simply returns all arguments.
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. |
Action returned: test
Example in Java
The code presented below defines a Java-based action named JavaEchoAction
.
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. |
sponge.enableJava(JavaEchoAction)
Example of an action builder
The code presented below defines and enables an action named EchoAction
.
def onLoad():
sponge.enable(ActionBuilder("EchoAction").withOnCall(lambda action, text: text))
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.
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()
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
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. Active/Inactive actions
An action can be active or inactive in a given context. The status has to be fetched manually in a client code if necessary.
The boolean onIsActive(IsActionActiveContext context)
method is used to provide this information.
Property | Description |
---|---|
|
A context value. Can be |
|
A context value type. Can be |
|
Action arguments in the context. Can be |
|
A context features. It is guaranteed to be non null in the |
10.4. 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.
An action argument can also be submitted by a client code, irrespectively of an action call.
The onProvideArgs(ProvideArgsContext context)
method is used to provide action argument values.
Property | Description |
---|---|
|
A not null set of argument names (or name paths) that are to be provided (i.e. read). A name path is a dot-separated sequence of names of parent types, e.g. |
|
A not null set of argument names (or name paths) that are to be submitted (i.e. written). A name path is a dot-separated sequence of names of parent types, e.g. |
|
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 |
|
The types of dynamic values used in |
|
An initially empty map of argument names (or name paths) and their provided values (value sets) that is to be set up in an |
|
A not null map of features for arguments in a context, set by a client code. Each argument specified in |
|
The flag indicating if this is the initial provide action arguments request. This flag can be set by a client code and used in the |
Provided arguments make 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.
A provided argument can depend on other arguments but only those that are specified earlier.
Arguments configured as provided with a value, a value set or a element value set 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.
Arguments configured as provided with submit should be handled in the onProvideArgs
callback method.
Provided arguments can handle a pagination of list elements.
10.5. Implementing interfaces
Actions may implement additional Java interfaces. It could be used to provide custom behavior of actions.
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.
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
.
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:
.
12. 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
.
12.1. Properties and methods
In addition to the inherited processor properties and methods, filters provide the following ones.
Property / Method | Description |
---|---|
|
Sets a name (a name pattern) or names (name patterns) of filtered events. The event names can be read using |
|
This method checks if an incoming event should be further processed. If |
Every filter defined by a class should implement the abstract onConfigure
method. Every filter should implement the onAccept
method.
Property / Method | Description |
---|---|
|
Configures the |
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.
|
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.
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.:
sponge.enableJava(ShapeFilter)
Example of a filter builder
The code presented below defines and enables a filter named ColorFilter
.
def onLoad():
def onAccept(filter, event):
color = event.get("color", None)
if color is None or color != "blue":
return False
else:
return True
sponge.enable(FilterBuilder("ColorFilter").withEvent("e1").withOnAccept(onAccept))
13. 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
.
13.1. Properties and methods
In addition to the inherited processor properties and methods, triggers provide the following ones.
Property / Method | Description |
---|---|
|
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 |
|
The callback method used for processing the event, called when the specified event (or one of the events) happens. This method is mandatory. |
|
This optional callback method checks if an incoming event should processed by this trigger. The default implementation returns |
Every trigger should implement abstract onConfigure
and onRun
methods.
Property / Method | Description |
---|---|
|
Configures the |
|
Configures the |
Example in a script language
The code presented below defines a trigger named TriggerA
listening to events named "a"
.
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 a trigger named SampleJavaTrigger
listening to events named "e1"
.
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. |
sponge.enableJava(SampleJavaTrigger)
Example of a trigger builder
The code presented below defines and enables a trigger named TriggerA
.
def onLoad():
sponge.enable(TriggerBuilder("TriggerA").withEvent("a").withOnRun(lambda trigger, event: trigger.logger.debug("Received event: {}", event.name)))
14. 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.
14.1. Properties and methods
In addition to the inherited processor properties and methods, rules provide the following ones.
Property / Method | Description |
---|---|
|
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. |
|
The initialization callback method that is invoked while creating every new rule instance but after |
|
Sets String-based specifications of events whose sequence causes the rule to fire. The complete event specifications can be read using |
|
Sets a duration that may be used to set the time how long a rule lasts (represented as a |
|
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 |
|
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. |
|
Adds a single condition for an event. |
|
Adds conditions for all events. This method must be invoked after the event specifications. |
|
Adds a single condition for all events. This method must be invoked after the event specifications. |
|
Sets complete specifications of events whose sequence causes the rule to fire. The preferred way is to use String-based specifications of events. |
|
Sets a flag indicating that the rule should listen to ordered (ordered rule) or unordered (unordered rule) sequences of events. Defaults to |
|
The callback method invoked when a sequence of events specified by this rule has happened and all the conditions have been fulfilled. The argument |
|
Returns the instance of the event that already happened and that has a specified alias. This method may be used inside |
|
This property is a reference to the first event that has been accepted by this rule. It is a shortcut for the |
|
Returns a sequence of events that happened, as a list of event instances. The sequence may contain |
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.
|
Property / Method | Description |
---|---|
|
Configures the |
Rule builders don’t support multi condition configuration methods, i.e. withConditions and withAllConditions . You have to invoke single condition versions of these methods.
|
14.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 theEventMode
Java enumeration.Table 32. 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"
.
14.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
ornone
, 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]
.
Events specification | Event sequences |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This rule hasn’t been fired because the event |
14.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. |
14.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 returnsboolean
, 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 returnsboolean
, 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 returnsboolean
), 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.
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.
public class SameSourceJavaRule extends JRule { (1)
private static final Logger logger = LoggerFactory.getLogger(SameSourceJavaRule.class);
@Override
public void onConfigure() {
withEventSpecs(new RuleEventSpec("filesystemFailure", "e1"), new RuleEventSpec("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 RuleEventSpec class 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") . |
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 . |
sponge.enableJava(SameSourceJavaRule)
Example of a rule builder
The code presented below defines and enables a rule named SameSourceAllRule
. Note that Python doesn’t support multi-expression lambda functions.
def onLoad():
def sameSourceAllRuleE2Condition(rule, event):
event1 = rule.getEvent("e1")
return event.get("source") == event1.get("source") and Duration.between(event1.time, event.time).seconds <= 4
def sameSourceAllRuleOnRun(rule, event):
rule.logger.info("Monitoring log [{}]: Critical failure in {}! Events: {}", event.time, event.get("source"),
rule.eventSequence)
sponge.getVariable("hardwareFailureScriptCount").incrementAndGet()
sponge.enable(RuleBuilder("SameSourceAllRule").withEvents(["filesystemFailure e1", "diskFailure e2 :all"])
.withCondition("e1", lambda rule, event: int(event.get("severity")) > 5)
.withCondition("e2", sameSourceAllRuleE2Condition)
.withDuration(Duration.ofSeconds(8)).withOnRun(sameSourceAllRuleOnRun))
15. 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 theonEvent
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.
15.1. Properties and methods
In addition to the inherited processor properties and methods, correlators provide the following ones.
Property / Method | Description |
---|---|
|
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. |
|
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 |
|
Sets a time how long a correlator lasts (represented as a |
|
Sets a synchronous flag for a correlator. For details see a description of this flag for rules. |
|
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 |
|
Sets an instance synchronous flag. If |
|
Checks if the event should be accepted as the first event of a correlator, therefore starting a new working instance. The method |
|
The initialization callback method that is invoked while creating a new correlator instance but after |
|
The callback method invoked when an event that a correlator listens to happens. This method is mandatory. |
|
This property is a reference to the first event that has been accepted by this correlator. It is a shortcut for the |
|
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, |
|
The final method that should be invoked in |
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.
|
Property / Method | Description |
---|---|
|
Configures the |
|
Configures the |
|
Configures the |
Example in a script language
The code presented below defines a 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.
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 a correlator analogous to the one shown above but defined as a Java class.
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. |
sponge.enableJava(SampleJavaCorrelator)
Example of a correlator builder
The code presented below defines and enables a correlator named SampleCorrelator
. Note that Python doesn’t support multi-expression lambda functions.
def onLoad():
def onEvent(correlator, event):
counter = sponge.getVariable("counter")
if counter == 4:
correlator.finish()
sponge.setVariable("counter", counter + 1)
sponge.enable(CorrelatorBuilder("SampleCorrelator").withEvents(["filesystemFailure", "diskFailure"]).withMaxInstances(1)
.withOnAcceptAsFirst(lambda correlator, event: event.name == "filesystemFailure")
.withOnInit(lambda correlator: sponge.setVariable("counter", 0))
.withOnEvent(onEvent))
16. 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.
16.1. Properties and methods
Property / Method | Description |
---|---|
|
The property that is a name of a plugin. This is a shortcut for |
|
The configuration callback method that will be invoked after a plugin has been loaded. This method allows reading an XML configuration for the plugin. |
|
The initialization callback method that will be invoked after a configuration of a plugin. |
|
The callback method that will be invoked once after the startup of the engine. |
|
The callback method that will be invoked once before the shutdown of the engine. |
|
The callback method that will be invoked before every reloading of a knowledge base. |
|
The callback method that will be invoked after every reloading of a knowledge base. |
|
The read-only property that provides a plugin logger. This is a shortcut for |
Example in Java
<?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.
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. |
16.2. Using plugins
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")
.
A plugin is not accessible in a main body of a script knowledge base because it still won’t be initialized when knowledge bases are being loaded. It should be referenced in processors or callback functions. |
Because echoPlugin
implements the method getEcho()
, you may invoke it in two ways:
-
sponge.getPlugin("echoPlugin").echo
-
echoPlugin.echo
Example in a script language
<?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>
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
16.3. Plugin life cycle
Sponge loads plugins when starting the system according to the steps:
-
Creates the plugin class instance. The class must have a no-parameter constructor.
-
Configures the plugin by invoking the method
onConfigure()
. -
Initializes the plugin by invoking the method
onInit()
. -
Invokes the callback method
onStartup()
when starting the engine. -
After starting all plugins the methods
onStartup()
defined in all knowledge bases are invoked. -
In case of reloading a knowledge base, the method
onBeforeReload()
of each plugin is invoked before the methodonBeforeReload()
of knowledge bases. Invoking the methodsonAfterReload()
goes in reverse (first the methodsonAfterReload()
of all knowledge bases and then the methods defined in plugins). -
Before Sponge shuts down, methods
onShutdown()
of all knowledge bases are invoked and then the methodonShutdown()
is invoked for each plugin.
17. 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.
18. Embedding Sponge in custom applications
Sponge may be embedded in a custom Java application using a Maven dependency and the Engine Builder API.
18.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.18.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.18.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>
19. Integration
19.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.
@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. |
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
.
@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.18.0</version>
</dependency>
19.1.1. Automatic enabling of processors registered in Spring as beans
SpringSpongeEngine
supports automatic enabling of singleton processors (e.g. action, filter, trigger) registered as Spring beans. The enabling is performed during the engine startup. If a non sigleton processor (e.g. rule, correlator) is defined as a Spring bean, an exception will be thrown. Prosessors will be registred in the default knowledge base unless the processorBeansKnowledgeBaseName
is set in the builder and points to another existing knowledge base. Prosessor names will correspond to their simple class names.
19.2. Spring Boot
There are two Spring Boot starters that aim to facilitate integration of Sponge with Spring Boot applications.
19.2.1. The base Sponge starter
The base Sponge starter is provided by the sponge-spring-boot-starter
artifact. The starter runs a Sponge engine and enables its configuration according to Spring Boot standards.
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-boot-starter</artifactId>
<version>1.18.0</version>
</dependency>
Then, for example, you can just add classes with your Sponge processors such as:
@Component
public class LowerCase extends JAction {
private final StringService stringService;
@Autowired
public LowerCase(StringService stringService) {
this.stringService = stringService;
}
@Override
public void onConfigure() {
withLabel("Lower case").withArg(new StringType("text")).withResult(new StringType());
}
public String onCall(String text) {
return stringService.toLowerCase(text);
}
}
or create your own Sponge configuration XML file in the default location config/sponge.xml
.
Configuration
You can customize the Sponge engine in the application.properties
or application.yaml
file.
The component supports the options listed below.
Name | Description | Default | Type |
---|---|---|---|
sponge.home |
The Sponge home directory. |
. |
String |
sponge.config-file |
The Sponge configuration file. |
config/sponge.xml |
String |
sponge.ignore-configuration-file-not-found |
The flag whether a missing configuration file should be ignored, Defaults to |
true |
Boolean |
sponge.name |
The Sponge engine name. |
String |
|
sponge.label |
The Sponge engine label. |
String |
|
sponge.description |
The Sponge engine description. |
String |
|
sponge.license |
The Sponge instance license. |
String |
|
sponge.properties |
The Sponge properties. |
Map<String, Object> |
|
sponge.system-properties |
The Sponge system properties. |
Map<String, String> |
|
sponge.variable-properties |
The Sponge variable properties. |
Map<String, String> |
|
sponge.default-knowledge-base-name |
The Sponge default knowledge base name. |
default |
String |
sponge.auto-startup |
The Sponge auto startup flag. |
true |
Boolean |
sponge.phase |
The Sponge engine Spring SmartLifecycle phase. |
Integer.MAX_VALUE |
Integer |
sponge.processor-beans-knowledge-base-name |
The knowledge base name that Sponge processor Spring beans will be registered in. |
boot |
String |
sponge.engine.main-processing-unit-thread-count |
The number of the Main Processing Unit worker threads. |
10 |
Integer |
sponge.engine.event-clone-policy |
The event clone policy. |
SHALLOW |
EventClonePolicy |
sponge.engine.event-queue-capacity |
The event queue capacity. Defaults to -1 (infinity) |
-1 |
Integer |
sponge.engine.duration-thread-count |
The number of duration executor threads. |
2 |
Integer |
sponge.engine.async-event-set-processor-executor-thread-count |
The number of threads used by an event set processor asynchronous executor. |
10 |
Integer |
sponge.engine.event-set-processor-default-synchronous |
The event set processor default synchronous flag. |
false |
Boolean |
sponge.engine.auto-enable |
Auto-enable processors. |
true |
Boolean |
sponge.engine.executor-shutdown-timeout |
Executor shutdown timeout (in milliseconds). |
60000 |
Long |
19.2.2. The Sponge Remote Service starter
The base Sponge Remote Service starter is provided by the sponge-remote-service-spring-boot-starter
artifact. The starter includes the base Sponge sponge-spring-boot-starter
starter and runs the Sponge Remote API service along with the gRPC API service. The Remote API service starter depends on spring-boot-starter-web
, spring-boot-starter-security
and camel-servlet-starter
.
The service leverages the configuration of these starters, however it redefines:
-
the server port:
server.port=8080
(in order to calculate the default gRPC API port, i.e.server.port + 1
), -
the Camel servlet context path:
camel.component.servlet.mapping.context-path=/sponge/*
.
You may, of course, change these values in your application properties.
If you use your own Camel or Spring Boot Security configuration in your Spring Boot application, make sure that the configurations don’t interfere with each other. |
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-remote-service-spring-boot-starter</artifactId>
<version>1.18.0</version>
</dependency>
Configuration
The component supports the options listed below.
Name | Description | Default | Type |
---|---|---|---|
sponge.remote.version |
The API version. |
String |
|
sponge.remote.name |
The API name. |
String |
|
sponge.remote.description |
The API description. |
String |
|
sponge.remote.license |
The API license. |
String |
|
sponge.remote.pretty-print |
The pretty print option. |
false |
Boolean |
sponge.remote.publish-reload |
If |
false |
Boolean |
sponge.remote.allow-anonymous |
Should an anonymous user be allowed. |
true |
Boolean |
sponge.remote.admin-role |
The admin role. |
ROLE_ADMIN |
String |
sponge.remote.anonymous-role |
The anonymous role. |
ROLE_ANONYMOUS |
String |
sponge.remote.include-detailed-error-message |
Should the detailed error message (e.g. an exception stack trace) be present in error responses. |
false |
Boolean |
sponge.remote.auth-token-expiration-duration |
The duration after which an authentication token will expire. Defaults to 30 minutes. |
30m |
String |
sponge.remote.openApiProperties |
The Open API properties |
Map<String, String> |
|
sponge.remote.include-response-times |
The flag specifying if a response header should have request and response time set. |
false |
Boolean |
sponge.remote.register-service-discovery |
The flag specifying if the service should be registered in a service discovery. |
false |
Boolean |
sponge.remote.discovery.url |
The service URL used in a service discovery. |
String |
|
sponge.remote.ignore-unknown-args |
The flag specifying if the service should ignore unknown action arguments passed by the client. |
false |
Boolean |
sponge.remote.grpc.autoStart |
Should the gRPC API be started. |
true |
Boolean |
sponge.remote.grpc.port |
The gRPC API port. |
Remote API port + 1 |
Integer |
Example
See the example project for a sample how to use sponge-remote-service-spring-boot-starter
.
The purpose of this example is to allow users to call remote actions defined in the Spring Boot environment. In this scenario, if you already have Spring services, you will be able to interact with them, indirectly through Sponge actions, from the Sponge Remote mobile application. After the initial configuration you will be able to write your own actions.
The most important step to use the starter in your Spring Boot application is to add necessary Maven dependencies in your pom.xml
. Besides sponge-remote-service-spring-boot-starter
you can add for example sponge-jython
, sponge-groovy
, sponge-jruby
or sponge-nashorn
to be able to write knowledge bases in these scripting languages.
Then you can specify your Sponge XML configuration file in the default location config/sponge.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">
<knowledgeBases>
<knowledgeBase name="python">
<file>${sponge.home}/sponge/sponge.py</file>
<file>${sponge.home}/sponge/employees.py</file>
</knowledgeBase>
<knowledgeBase name="admin" label="Admin">
<file>classpath:sponge/engine/engine_admin_library.py</file>
<file>classpath:sponge/engine/engine_public_library.py</file>
</knowledgeBase>
<knowledgeBase name="security">
<file>${sponge.home}/sponge/remote_api_security.py</file>
</knowledgeBase>
</knowledgeBases>
</sponge>
You should setup Spring Boot Web security, e.g.:
package org.openksavi.sponge.examples.project.springboot;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.openksavi.sponge.springboot.remoteservice.SpongeRemoteWebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends SpongeRemoteWebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// Enable in memory based authentication with a user named "admin".
auth.inMemoryAuthentication().withUser("admin").password(encoder.encode("password")).roles("ADMIN");
}
}
Access control to Sponge in this example is configured in remote_api_security.py
. The code below (addRolesToKb
) gives ROLE_ADMIN
access to run remotely any actions from any knowledge bases and ROLE_ANONYMOUS
access to run actions only from knowledge bases boot
and python
.
def configureAccessService():
# Configure the RoleBasedAccessService.
# Simple access configuration: role -> knowledge base names regexps.
remoteApiServer.accessService.addRolesToKb({ "ROLE_ADMIN":[".*"], "ROLE_ANONYMOUS":["boot", "python"]})
# Simple access configuration: role -> event names regexps.
remoteApiServer.accessService.addRolesToSendEvent({ "ROLE_ADMIN":[".*"], "ROLE_ANONYMOUS":[]})
remoteApiServer.accessService.addRolesToSubscribeEvent({ "ROLE_ADMIN":[".*"], "ROLE_ANONYMOUS":[".*"]})
def onStartup():
# Configure the access service on startup.
configureAccessService()
def onAfterReload():
# Reconfigure the access service after each reload.
configureAccessService()
The boot
knowledge base will register Java-based actions that are annotated as Spring beans.
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.openksavi.sponge.action.ProvideArgsContext;
import org.openksavi.sponge.core.util.SpongeUtils;
import org.openksavi.sponge.examples.project.springboot.model.Employee;
import org.openksavi.sponge.examples.project.springboot.service.EmployeeService;
import org.openksavi.sponge.features.model.SubAction;
import org.openksavi.sponge.java.JAction;
import org.openksavi.sponge.type.IntegerType;
import org.openksavi.sponge.type.ListType;
import org.openksavi.sponge.type.StringType;
import org.openksavi.sponge.type.provided.ProvidedMeta;
import org.openksavi.sponge.type.provided.ProvidedValue;
import org.openksavi.sponge.type.value.AnnotatedValue;
@Component
public class ListEmployees extends JAction {
private final EmployeeService employeeService;
@Autowired
public ListEmployees(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@Override
public void onConfigure() {
withLabel("Employees").withArgs(
new StringType("search").withNullable().withLabel("Search").withFeature("responsive", true),
new ListType<Number>("employees").withLabel("Employees").withElement(new IntegerType("employee").withAnnotated()).withFeatures(
SpongeUtils.immutableMapOf(
"createAction", new SubAction("CreateEmployee"),
"readAction", new SubAction("ViewEmployee").withArg("employeeId", "@this"),
"updateAction", new SubAction("UpdateEmployee").withArg("employeeId", "@this"),
"deleteAction", new SubAction("DeleteEmployee").withArg("employeeId", "@this"),
"refreshable", true
)).withProvided(new ProvidedMeta().withValue().withOverwrite().withDependencies("search"))
).withNonCallable().withFeature("icon", "library");
}
@Override
public void onProvideArgs(ProvideArgsContext context) {
if (context.getProvide().contains("employees")) {
String search = (String) context.getCurrent().get("search");
List<Employee> employees = search != null ? employeeService.findByNameLike("%" + search + "%") : employeeService.all();
List<AnnotatedValue<Long>> employeesArg = employees.stream()
.map(employee -> new AnnotatedValue<>(employee.getId()).withValueLabel(employee.getName()))
.collect(Collectors.toList());
context.getProvided().put("employees", new ProvidedValue<>().withValue(employeesArg));
}
}
}
The python
knowledge base is defined in the following files.
def onInit():
sponge.addCategories(
CategoryMeta("spring").withLabel("Spring").withPredicate(lambda processor: processor.kb.name in ("boot", "python")),
CategoryMeta("admin").withLabel("Admin").withPredicate(lambda processor: processor.kb.name in ("admin"))
)
Categories group actions from different knowledge bases, so they can be accordingly shown in the mobile application.
from org.openksavi.sponge.examples.project.springboot.service import EmployeeService
from org.openksavi.sponge.examples.project.springboot.sponge import TypeUtils
def getEmployeeService():
return spring.context.getBean(EmployeeService)
class CreateEmployee(Action):
def onConfigure(self):
self.withLabel("Add a new employee")
# Create an initial default instance of an employee and provide it to GUI.
self.withArg(TypeUtils.createEmployeeRecordType("employee").withDefaultValue({})).withNoResult()
self.withFeatures({"visible":False, "callLabel":"Save", "cancelLabel":"Cancel", "icon":"plus-box"})
def onCall(self, employee):
getEmployeeService().createNew(TypeUtils.createEmployeeFromMap(employee))
class UpdateEmployee(Action):
def onConfigure(self):
self.withLabel("Modify the employee")
self.withArgs([
IntegerType("employeeId").withAnnotated().withFeature("visible", False),
# Must set withOverwrite to replace with the current value.
TypeUtils.createEmployeeRecordType("employee").withProvided(
ProvidedMeta().withValue().withOverwrite().withDependency("employeeId"))
]).withNoResult()
self.withFeatures({"visible":False, "callLabel":"Save", "cancelLabel":"Cancel", "icon":"square-edit-outline"})
def onCall(self, employeeId, employee):
getEmployeeService().update(employeeId.value, TypeUtils.createEmployeeFromMap(employee))
def onProvideArgs(self, context):
if "employee" in context.provide:
employeeMap = TypeUtils.createEmployeeMap(getEmployeeService().getById(context.current["employeeId"].value))
context.provided["employee"] = ProvidedValue().withValue(employeeMap)
class DeleteEmployee(Action):
def onConfigure(self):
self.withLabel("Delete the employee")
self.withArg(IntegerType("employeeId").withAnnotated().withFeature("visible", False)).withNoResult()
self.withFeatures({"visible":False, "icon":"delete", "confirmation":True})
def onCall(self, employeeId):
getEmployeeService().delete(employeeId.value)
After starting the Spring Boot application you can connect to the Sponge Remote service in the Sponge Remote mobile application, entering an URL of the format http://host:8080/sponge
in the Connections page. Change the host
to your server address. The main action lists the employees.

19.3. Apache Camel
19.3.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.18.0</version>
</dependency>
19.3.2. URI format
sponge:engineRef[?options]
Where engineRef
represents the name of the SpongeEngine
implementation instance located in the Camel registry.
19.3.3. Options
Name | Default value | Description |
---|---|---|
|
|
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. |
|
|
If set to |
19.3.4. Sponge support for Camel
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
.
Property / Method | Description |
---|---|
|
Emits (sends) the body to all current consumers. |
|
The Camel ProducerTemplate for working with Camel and sending Camel messages. |
|
Sends the body to an endpoint. The shortcut for |
|
Sends the body to an endpoint returning any result output body. The shortcut for |
|
Returns a Camel context. |
|
Returns the current list of consumers. |
For more information see the CamelPlugin
Javadoc.
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.
19.3.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.
@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");
}
};
}
}
camel_producer.py
class CamelTrigger(Trigger):
def onConfigure(self):
self.withEvent("spongeProducer")
def onRun(self, event):
print event.body
// 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");
Send me to the Sponge
Camel producer action
A Camel producer action will be invoked by Sponge synchronously when a Camel exchange comes to the Sponge engine. An action behaviour is similar to Camel processor. It is recommended that a Camel producer action implement the org.openksavi.sponge.camel.CamelAction
interface, that specifies the void onCall(Exchange exchange)
method.
To avoid any misconception please note that events in the Output Event Queue are not sent to the Camel route. |
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.
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 baseclass CustomAction(Action): def onCall(self, exchange): exchange.message.body = "OK"
Camel route that sets the action in the endpoint URIfrom("direct:start").routeId("spongeProducer") .to("sponge:spongeEngine?action=CustomAction") .log("Action result as a body: ${body}");
Camel route that sets the action in the headerfrom("direct:start").routeId("spongeProducer") .setHeader("CamelSpongeAction", constant("CustomAction")) .to("sponge:spongeEngine) .log("Action result as a body: ${body}");
19.3.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.
@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");
}
};
}
}
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.
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.
19.3.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())
19.4. Sponge Remote API
The Sponge Remote API provides users a remote access to key Sponge functionalities.
19.4.1. Endpoints
The Sponge Remote API provides the JSON-RPC 2.0 endpoint and method endpoints that accept simplified (non strict) JSON-RPC 2.0 messages.
The reason that the API supports both styles is that the JSON-RPC 2.0 compatibility is intended for applications that connect to Sponge while the non strict JSON-RPC 2.0 API is rather intended for manual, command-line uses.
JSON-RPC 2.0 endpoint
The /jsonrpc
endpoint is intended to be JSON-RPC 2.0 compatible, however:
-
only named JSON-RPC 2.0 params are supported (params as a list are not supported),
-
batch requests are not supported.
The request HTTP content type should be application/json
.
Method endpoints
The other endpoints correspond to the supported Remote API methods and provide an RPC style API based on JSON-RPC 2.0 that adopts some REST concepts:
-
a request doesn’t have to contain a method name (because it is is defined by a particular endpoint) nor the
jsonrpc
field, -
it uses different endpoints for different methods like REST,
-
requests with no
id
are not regarded as notifications.
The method endpoints metadata are used when the OpenAPI specification for the service is generated.
19.4.2. Message
Request
A Remote API request supports standard JSON-RPC request members.
Name | Description |
---|---|
|
Required for the JSON-RPC 2.0 endpoint. Optional for method endpoints. |
|
Required for the JSON-RPC 2.0 endpoint. Optional for method endpoints, but if present for a method endpoint it must have the same value as the corresponsing endpoint. |
|
Named JSON-RPC 2.0 method parameters. A parameters object can have a special request |
|
Required for the JSON-RPC 2.0 endpoint if a request is not a notification. Optional for method endpoints, because they don’t support notifications and handle all requests as non notifications. |
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A username that may be used in a user/password authentication mode. In that case, if there is no username present, the anonymous user is assumed. |
|
|
No |
A user password that may be used in a user/password authentication mode. |
|
|
No |
An authentication token that may be used in a token-based authentication mode. |
|
|
No |
Request features that will be stored in a Remote API session. |
Request features can be used for:
-
Setting a requested language if a knowledge base supports it.
-
Sending an API key to the service. For example, on the server side you could add an
OnSessionOpenListener
implementing theonSessionOpen(RemoteApiSession session)
method that checks a value ofsession.getFeatures().get("apiKey")
. If the API key is incorrect an exception will be thrown.
Request and response features are not converted by the feature converter. |
Response
A Remote API response supports standard JSON-RPC response members.
Name | Description |
---|---|
|
The version of the JSON-RPC protocol. The value is exactly |
|
A result object that contains a |
|
The error object is present only on error. |
|
It has the same value as the |
Property | Type | Description |
---|---|---|
|
|
The response header. |
|
depends on the method |
The response result value. |
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
An optional request time, i.e. a server time (as a Java Instant) of starting processing a request. |
|
|
No |
An optional response time, i.e. a server time (as a Java Instant) of finishing processing a request. |
|
|
No |
Response features that will be obtained from a Remote API session. |
Request and response features are not converted by the feature converter. |
A Remote API error object supports the standard JSON-RPC error object members, i.e. code
, message
and optional data
. The error data is represented as a map.
Key | Value type | Description |
---|---|---|
|
|
The optional detailed error message, e.g. an exception stack trace. |
19.4.3. Methods
The following table contains a summary of the Remote API methods. See the specification generated using Swagger and Swagger2Markup.
The Remote API methods:
-
version
, -
features
, -
login
, -
logout
, -
knowledgeBases
, -
actions
, -
call
, -
isActionActive
, -
provideActionArgs
, -
send
, -
eventTypes
, -
reload
.
The OpenAPI specification of the Remote API is included in the Appendix A of the Sponge Reference Documentation.
The Sponge Remote API service can publish custom Remote API methods as well.
The version
method
Returns the Sponge Remote API version if it is set in the configuration, otherwise returns the Sponge engine version.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
Name | Type | Description |
---|---|---|
|
|
A version. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"version","id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"header" : {
"requestTime" : "2020-05-31T13:59:34.453Z",
"responseTime" : "2020-05-31T13:59:34.578Z"
},
"value" : "1.16.0"
},
"id" : 1
}
# Request
curl http://localhost:8888/jsonrpc?jsonrpc=2.0&method=version&id=1
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : "1.16.0"
},
"id" : "1"
}
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/version
# Response
{
"jsonrpc" : "2.0",
"result" : {
"header" : {
"requestTime" : "2020-05-31T14:00:41.246Z",
"responseTime" : "2020-05-31T14:00:41.246Z"
},
"value" : "1.16.0-rc3"
},
"id" : null
}
# Request
curl http://localhost:8888/version
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : "1.16.0"
},
"id" : null
}
The features
method
Returns the API features.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
Name | Type | Description |
---|---|---|
|
|
API features. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"features","id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : {
"spongeVersion" : "1.16.0",
"apiVersion" : null,
"grpcEnabled" : true,
"name" : "Sponge Remote API",
"description" : "Sponge Remote API description",
"license" : "Apache 2.0"
}
},
"id" : 1
}
The login
method
User login. Used in a token-based authentication scenario.
Name | Type | Required | Description |
---|---|---|---|
|
|
Yes |
A request header. It is required because it contains |
Name | Type | Description |
---|---|---|
|
|
A login value object. |
|
|
A response header. |
Name | Type | Description |
---|---|---|
|
|
An authentication token. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"login","params":{"header":{"username":"admin","password":"password"}},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : {
"authToken" : "eyJhbGciOiJIUzUxMiIsInppcCI6IkRFRiJ9.eNqqVkosLckITi0uzszP80xRsjKsBQAAAP__.YaE4Ka_RNk9REnVuzycXkXDTKAfIPHeTJzIRdC22llmK1hCtN3GBIE3cyM-vNJUMWWdgDPNwqFc9J3xwfSx2TA"
}
},
"id" : 1
}
The logout
method
User logout. Used in a token-based authentication scenario.
Name | Type | Required | Description |
---|---|---|---|
|
|
Yes |
A request header. It is required because it contains an |
Name | Type | Description |
---|---|---|
|
|
A logout status. It is always |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"logout","params":{"header":{"authToken":"eyJhbGciOiJIUzUxMiIsInppcCI6IkRFRiJ9.eNqqVkosLckITi0uzszP80xRsjKsBQAAAP__.YaE4Ka_RNk9REnVuzycXkXDTKAfIPHeTJzIRdC22llmK1hCtN3GBIE3cyM-vNJUMWWdgDPNwqFc9J3xwfSx2TA"}},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : true
},
"id" : 1
}
The knowledgeBases
method
Returns the knowledge bases which the user may use (i.e. may call actions registered in these knowledge bases).
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
Name | Type | Description |
---|---|---|
|
|
A list of available knowledge bases metadata. |
|
|
A response header. |
Name | Type | Description |
---|---|---|
|
|
A knowledge base name. |
|
|
A knowledge base label. |
|
|
A knowledge base description. |
|
|
A knowledge base version. |
|
|
A knowledge base sequence number (e.g. for GUI list order). |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"knowledgeBases","id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : [ {
"name" : "example",
"label" : "Example",
"description" : null,
"version" : 2,
"sequenceNumber" : 1
} ]
},
"id" : 1
}
# Request
curl http://localhost:8888/jsonrpc?jsonrpc=2.0&method=knowledgeBases&id=1
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/knowledgeBases
# Request
curl http://localhost:8888/knowledgeBases
The actions
method
Returns the metadata of actions that are available to the user.
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.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
|
|
No |
An action name or a regular expression. If you want to get metadata for specified actions, set this parameter to an action name or a Java-compatible regular expression. |
|
|
No |
A metadata required flag. If you want to get only actions that have argument and result metadata specified in their configuration, set this parameter to |
|
|
No |
A flag for requesting registered types used in the actions in the result (defaults to |
Name | Type | Description |
---|---|---|
|
|
A |
|
|
A response header. |
Name | Type | Description |
---|---|---|
|
|
Available actions metadata. For more information see the |
|
|
Registered types used in the actions. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"actions","id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : {
"actions" : [ {
"name" : "LowerCase",
"label" : "Convert to lower case",
"description" : "Converts a string to lower case.",
"knowledgeBase" : {
"name" : "example",
"label" : "Example",
"description" : null,
"version" : 2,
"sequenceNumber" : 1
},
"category" : {
"name" : "category1",
"label" : "Category 1",
"description" : "Category 1 description",
"features" : { },
"sequenceNumber" : 0
},
"features" : { },
"args" : [ {
"kind" : "STRING",
"registeredType" : null,
"name" : "text",
"label" : "A text that will be changed to lower case",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : null
} ],
"result" : {
"kind" : "STRING",
"registeredType" : null,
"name" : null,
"label" : "Lower case text",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : null
},
"callable" : true,
"activatable" : false,
"qualifiedVersion" : {
"knowledgeBaseVersion" : 2,
"processorVersion" : null
}
}, {
"name" : "UpperCase",
"label" : "Convert to upper case",
"description" : "Converts a string to upper case.",
"knowledgeBase" : {
"name" : "example",
"label" : "Example",
"description" : null,
"version" : 2,
"sequenceNumber" : 1
},
"category" : {
"name" : "category1",
"label" : "Category 1",
"description" : "Category 1 description",
"features" : { },
"sequenceNumber" : 0
},
"features" : { },
"args" : [ {
"kind" : "STRING",
"registeredType" : null,
"name" : "text",
"label" : "Text to upper case",
"description" : "The text that will be converted to upper case.",
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : 256
} ],
"result" : {
"kind" : "STRING",
"registeredType" : null,
"name" : null,
"label" : "Upper case text",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : null
},
"callable" : true,
"activatable" : false,
"qualifiedVersion" : {
"knowledgeBaseVersion" : 2,
"processorVersion" : 2
}
} ],
"types" : null
}
},
"id" : 1
}
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"actions","params":{"header":{"username":"john","password":"password"}},"id":1}'
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"actions","params":{"name":".*Case"},"id":1}'
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/actions
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/actions -d '{"params":{"header":{"username":"john","password":"password"}}}'
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/actions -d '{"params":{"name":".*Case"}}'
The call
method
Calls an action.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
|
|
Yes |
An action name. |
|
list or map |
No |
Action arguments as a list or a map. The map represents named arguments. This parameter is required for actions that have non-nullable or non-optional arguments. |
|
|
No |
Expected qualified version of the action. |
Name | Type | Description |
---|---|---|
|
depends on the action result |
An action result. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"call","params":{"name":"UpperCase","args":["test1"]},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : "TEST1"
},
"id" : 1
}
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"call","params":{"name":"UpperCase","args":{"text":"test1"}},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : "TEST1"
},
"id" : 1
}
# Request
curl -G "http://localhost:8888/jsonrpc?jsonrpc=2.0&method=call&id=1" --data-urlencode "params=`echo '{"name":"OutputStreamResultAction"}' | base64`"
# Response
Sample text file
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/call -d '{"params":{"name":"UpperCase","args":["test1"]}}'
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/call -d '{"params":{"name":"UpperCase","args":{"text":"test1"}}}'
# Request
curl -G "http://localhost:8888/call?" --data-urlencode "params=`echo '{"name":"OutputStreamResultAction"}' | base64`"
Output stream type
If an action result type is OutputStreamType
the response won’t be compatible with the JSON-RPC 2.0. The HTTP response will contain a raw result of the action.
Input stream type
An action argument of the InputStreamType
type can be used to upload files (including large files) to the server. Files can be uploaded via HTML forms and JavaScript FormData
. However there are several limitations:
-
Only trailing action arguments may be of type
InputStreamType
. They must be accessed in theonCall
method sequentially, in the same order as in the action metadata, because they are lazily loaded due to the requirements of the underlying libraries. -
Action arguments metadata are required.
-
The value of the first field of an HTML form must be a JSON-RPC 2.0 request. The name of this field is not significant. Action arguments in this JSON-RPC 2.0 request mustn’t contain any entries for input stream arguments. In most cases this field would be hidden.
-
All
file
form fields must be present after the JSON-RPC 2.0 field and they must be put in the same order as specified in the action metadata. Afile
form field shouldn’t bemultiple
. -
Supported only for the non strict JSON-RPC 2.0 endpoint:
/call
.
<!DOCTYPE html>
<html>
<body>
<form id="uploadForm">
<input type="hidden" id="jsonrpc" name="jsonrpc" value='{"jsonrpc":"2.0","method":"call","params":{"name":"InputStreamArgAction","args":["VALUE"]},"id":1}'>
<label for="file">Filename:</label>
<input type="file" name="fileStream" id="file" />
<input id="submit" type="submit" value="Upload">
<p/>
<div id="message"></div>
</form>
<script type="text/javascript">
document.getElementById("uploadForm").onsubmit = async (e) => {
e.preventDefault();
document.getElementById("message").innerHTML = "Uploading file...";
let response = await fetch("/call", {
method: "POST",
body: new FormData(uploadForm)
});
let json = await response.json();
document.getElementById("message").innerHTML = json.result.value;
};
</script>
</body>
</html>
from java.io import File
from org.apache.commons.io import FileUtils
class InputStreamArgAction(Action):
def onConfigure(self):
self.withLabel("Input stream arg").withArgs([
StringType("value"),
InputStreamType("fileStream")
]).withResult(StringType())
def onCall(self, value, fileStream):
uploadDir = "{}/upload/".format(sponge.home)
FileUtils.copyInputStreamToFile(fileStream.inputStream, File(uploadDir + fileStream.filename))
return "Uploaded {}".format(fileStream.filename)
The isActionActive
method
Informs if an action (or actions) in a given context is active.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
|
|
Yes |
Query entries. |
Name | Type | Description |
---|---|---|
|
|
An action name. |
|
|
A context value. |
|
|
A context context type. |
|
|
Action arguments in the context. |
|
|
Features. |
|
|
An action qualified version. |
Name | Type | Description |
---|---|---|
|
|
Actions activity statuses. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"isActionActive","params":{"entries":[{"name":"UpperCase"}]},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : [ true ]
},
"id" : 1
}
The provideActionArgs
method
Provides action arguments. Returns provided arguments, i.e. values along with value sets of action arguments.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
|
|
Yes |
An action name. |
|
|
No |
Names of action arguments to provide. |
|
|
No |
Names of action arguments to submit. |
|
|
No |
Current values of action arguments in a client code. |
|
|
No |
Types of dynamic values for provide and current. |
|
|
No |
Features for arguments. |
|
|
No |
An action expected qualified version. |
|
|
No |
A flag indicating if this is the initial provide action arguments request for a single action. |
Name | Type | Description |
---|---|---|
|
|
Provided action arguments. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"provideActionArgs","params":{"name":"ProvideByAction","provide":["value"]},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : {
"value" : {
"value" : null,
"valuePresent" : false,
"annotatedValueSet" : [ {
"value" : "value1",
"valueLabel" : null,
"valueDescription" : null,
"features" : { },
"typeLabel" : null,
"typeDescription" : null
}, {
"value" : "value2",
"valueLabel" : null,
"valueDescription" : null,
"features" : { },
"typeLabel" : null,
"typeDescription" : null
}, {
"value" : "value3",
"valueLabel" : null,
"valueDescription" : null,
"features" : { },
"typeLabel" : null,
"typeDescription" : null
} ],
"annotatedElementValueSet" : null
}
}
},
"id" : 1
}
The send
method
Sends a new event.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
|
|
Yes |
An event name (type). |
|
|
No |
Event attributes. |
|
|
No |
An event label. |
|
|
No |
An event description. |
|
|
No |
Event features. |
Name | Type | Description |
---|---|---|
|
|
A new event id. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"send","params":{"header":{"username":"john","password":"password"},"name":"alarm","attributes":{"a1":"test1","a2":"test2", "a3":4}},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : "1590944366954-434"
},
"id" : 1
}
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/send -d '{"params":{"header":{"username":"john","password":"password"},"name":"alarm","attributes":{"a1":"test1","a2":"test2", "a3":4}}}'
The eventTypes
method
Returns the registered event types.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
|
|
No |
An event name or a Java-compatible regular expression. |
Name | Type | Description |
---|---|---|
|
|
Available event types. |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"eventTypes","params":{"name":".*"},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : {
"notification" : {
"kind" : "RECORD",
"registeredType" : null,
"name" : null,
"label" : null,
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"fields" : [ {
"kind" : "STRING",
"registeredType" : null,
"name" : "source",
"label" : "Source",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : null
}, {
"kind" : "INTEGER",
"registeredType" : null,
"name" : "severity",
"label" : "Severity",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : true,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minValue" : null,
"maxValue" : null,
"exclusiveMin" : false,
"exclusiveMax" : false
}, {
"kind" : "RECORD",
"registeredType" : "Person",
"name" : "person",
"label" : null,
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : true,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"fields" : [ {
"kind" : "STRING",
"registeredType" : null,
"name" : "firstName",
"label" : "First name",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : null
}, {
"kind" : "STRING",
"registeredType" : null,
"name" : "surname",
"label" : "Surname",
"description" : null,
"annotated" : false,
"format" : null,
"defaultValue" : null,
"nullable" : false,
"readOnly" : false,
"features" : { },
"optional" : false,
"provided" : null,
"minLength" : null,
"maxLength" : null
} ]
} ]
}
}
},
"id" : 1
}
The reload
method
Reloads all knowledge bases. Depending on the configuration, this method may not be published. It should be available only to administrators.
Name | Type | Required | Description |
---|---|---|---|
|
|
No |
A request header. |
Name | Type | Description |
---|---|---|
|
|
A reload status. It is always |
|
|
A response header. |
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"reload","params":{"header":{"username":"john","password":"password"}},"id":1}'
# Response
{
"jsonrpc" : "2.0",
"result" : {
"value" : true
},
"id" : 1
}
# Request
curl -i -k -X POST -H "Content-type:application/json" http://localhost:8888/jsonrpc -d '{"jsonrpc":"2.0","method":"reload","id":1}'
# Response
{
"jsonrpc" : "2.0",
"error" : {
"code" : 1001,
"message" : "No privileges to reload Sponge knowledge bases",
"data" : null
},
"id" : 1
}
19.4.4. Error codes
In addition to the pre-defined JSON-RPC 2.0 error codes, the Sponge Remote API defines the following error codes.
Code | Description |
---|---|
1001 |
A generic error. |
1002 |
Invalid or expired authentication token. |
1003 |
The action version in the engine differs from the one passed to the Remote API from a client code. |
1004 |
Invalid username or password. |
1005 |
An action to be called is inactive. |
19.4.5. HTTP POST and GET
The Remote API supports both HTTP POST and HTTP GET. However HTTP GET is not recommended, because it is not really suited for RPC (see the JSON-RPC 2.0 Transport: HTTP proposal/draft).
Param | Description |
---|---|
|
Maps to a JSON |
|
Maps to a JSON |
|
A named params JSON object that is: 1) Base64 encoded, 2) then URL encoded. |
|
Maps to a JSON |
19.4.6. HTTP status codes
Code | Description |
---|---|
|
In case of a success. |
|
In case of an error. |
|
In case of a notification. |
19.4.7. OpenAPI specification
An online API specification in the OpenAPI 2.0 (Swagger) JSON format will be available (depending on the configuration) at endpoint /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 customized methods. |
19.4.8. Security
Authentication mode
The Remote API supports a username/password and an authentication token authentication modes.
Name | Description |
---|---|
Username/password |
Every request has to contain a username and a password. Invoking the |
Authentication token |
Every request has to contain an authentication token, returned by the |
19.4.9. API features
Name | Type | Description |
---|---|---|
|
|
The Sponge engine version. |
|
|
The Sponge Remote API version that is set in the configuration (can be |
|
|
The Sponge Remote API protocol version. For compatibility, both server and client should use the same protocol version. |
|
|
The Remote API service name. |
|
|
The Remote API service description. |
|
|
The Remote API service license. |
|
|
Set to |
19.5. Sponge Remote API server
The Remote API server provides a Sponge Remote API service. The server plugin (RemoteApiServerPlugin
) uses Apache Camel REST DSL in order to configure the service.
The default name of the Remote API plugin (which can be used in knowledge bases) is remoteApiServer
.
Name | Type | Description |
---|---|---|
|
|
If |
|
|
The Camel REST component id. Defaults to |
|
|
The Remote API host. It can be overwritten by a Java system property |
|
|
The Remote API port. Defaults to |
|
|
The Remote API path. Defaults to |
|
|
The Remote API name. It can be overwritten by a Java system property |
|
|
The Remote API description. |
|
|
The license text of a particular Remote API service. |
|
|
The pretty print option. Defaults to |
|
|
Public actions. |
|
|
Public event names. |
|
|
The SSL configuration. |
|
|
If |
|
|
The name of the class extending |
|
|
The |
|
|
The |
|
|
The |
|
|
The duration (in seconds) after which an authentication token will expire. The value |
|
|
The flag specifying if a response header should have request and response time set. Defaults to |
|
|
The flag specifying if the service should be registered in a service discovery. It allows client applications to automatically find nearby (i.e. on the local network) Sponge services. Defaults to |
|
|
The service URL used in a service discovery. It can be overwritten by a Java system property |
|
|
The flag specifying if the service should ignore unknown action arguments passed by the client. Defaults to |
|
|
The flag specifying if an error response should contain a detailed error message. Defaults to |
|
|
The flag specifying if an error response message should contain an error location. Defaults to |
|
|
The flag specifying if the HTTP request headers should be copied to the HTTP response. Defaults to |
|
|
The flag specifying if CORS is enabled, i.e. CORS headers will be included in the response HTTP headers. Defaults to |
|
|
The flag specifying if the Open API specification should also contain GET operations. Defaults to |
|
|
The suffix for an Open API operation ID for GET operations. Defaults to |
|
|
The regexp specifying which endpoints should be included in the Open API specification. Defaults to |
<sponge>
<plugins>
<plugin name="remoteApiServer" class="org.openksavi.sponge.remoteapi.server.RemoteApiServerPlugin">
<configuration>
<port>1836</port>
<autoStart>false</autoStart>
</configuration>
</plugin>
</plugins>
</sponge>
@Configuration
public static class Config extends SpongeCamelConfiguration {
@Bean
public SpongeEngine spongeEngine() {
return SpringSpongeEngine.builder().plugins(camelPlugin(), remoteApiPlugin())
.config("sponge_config.xml").build();
}
@Bean
public RemoteApiServerPlugin remoteApiPlugin() {
RemoteApiServerPlugin plugin = new RemoteApiServerPlugin();
plugin.setSecurityProvider(new SimpleSpringInMemorySecurityProvider());
return plugin;
}
}
For more information see the RemoteApiServerPlugin
Javadoc.
Maven configuration
Maven users will need to add the following dependency to their pom.xml
:
<dependency>
<groupId>org.openksavi.sponge</groupId>
<artifactId>sponge-remote-api-server</artifactId>
<version>1.18.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.
19.5.1. Custom operations
You can define a custom Remote API operation (using the ActionDelegateOperation.builder()
in the route builder) that delegates a Remote API request to an action call (e.g. to allow implementing an operation body in a scripting language but providing a static Remote API interface).
19.5.2. OpenAPI specification
After starting the plugin, the online API specification in the OpenAPI 2.0 (Swagger) JSON format will be accesible.
19.5.3. JSON/Java mapping
The Remote 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 Remote 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.
|
19.5.4. Session
For each request the Remote 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 Remote API server plugin.
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 " + remoteApiServer.session.user.name + ": " + text.lower()
The Camel Exchange
instance can be accessed by remoteApiServer.session.exchange
.
In order to handle a session lifecycle you can implement and set the on session open and the on session close listeners in the RemoteApiService
.
19.5.5. Security
The Remote API provides only simple security out of the box and only if turned on. All requests allow passing a username and a password. If the username 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 SecurityProvider
interface as well as the SecurityService
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.
Simple security strategy
The simple security strategy uses in-memory user data or user data stored in a password file. 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.
# 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 remoteApiServer.canAccessResource(ROLES_TO_KB, userContext, kbName)
class RemoteApiCanSendEvent(Action):
def onCall(self, userContext, eventName):
return remoteApiServer.canAccessResource(ROLES_TO_SEND_EVENT, userContext, eventName)
class RemoteApiCanSubscribeEvent(Action):
def onCall(self, userContext, eventName):
return remoteApiServer.canAccessResource(ROLES_TO_SUBSCRIBE_EVENT, userContext, eventName)
def onStartup():
# Load users from a password file.
remoteApiServer.service.securityService.loadUsers()
A password file is specified by a password.file
configuration property.
For more information see examples in the source code.
Adding a Remote API user to a password file
A Remote API user password file is a way to configure users for a Sponge Remote API simple security strategy. Each user has its entry in a separate line. The entry contains colon-separated: a username, a comma-separated list of groups and a hashed password.
admin:admin:86975030682e27eca6fa4fb90e9d4b4aa3b3efc381149385347c7573b0b7002d48b1462c7f2e20db7a48cffdcc329bb1b6868551b7372d19a2781571919cc831
The best way of adding a Remote API user to a password file is to use a predefined knowledge base kb_add_remote_api_user.py
in a Docker container. The knowledge base requires an argument specifying a password file.
docker run -it --rm -v `pwd`:/opt/tmp openksavi/sponge -k "classpath*:/org/openksavi/sponge/remoteapi/server/kb_add_remote_api_user.py" -q /opt/tmp/password.txt
A password can be generated manually and added to a password file as well.
# Note that the username must be lower case.
echo -n username-password | shasum -a 512 | awk '{ print $1 }'
19.5.6. HTTPS
In production mode you should configure HTTPS. Otherwise your passwords could be sent in plain text over the network as a part of the Remote API JSON requests.
19.5.7. Service discovery
The Sponge Remote API can be registered using the the mDNS/DNS-SD service discovery to provide a zero-configuration connection setup for Sponge Remote API clients in a local network.
19.5.8. Environment
Standalone
This is the default configuration that uses the embedded Jetty server.
Servlet container
The Sponge Remote API service may also be deployed into a servlet container (e.g. Tomcat) as a web application. See the Remote API Demo Service example.
19.6. Sponge Remote API client for Java
The Sponge Remote API client for Java simplifies connecting to a remote Sponge Remote API server from applications written in Java. The default implementation uses the OkHttp library. The Remote API client uses JSON-RPC 2.0 POST methods.
The fully featured Sponge Remote API client is the client for Dart. Clients for other languages may have less features. For more information check the client API. |
try (SpongeClient client = new DefaultSpongeClient(SpongeClientConfiguration.builder()
.url("http://localhost:8080")
.build())) { (1)
String upperCaseText = client.call(String.class, "UpperCase", Arrays.asList("text")); (2)
}
1 | Create a new Remote API client. |
2 | Call the remote action. |
SpongeClient client = new DefaultSpongeClient(SpongeClientConfiguration.builder()
.url(String.format("http://localhost:%d", PORT))
.username(username)
.password(password)
.build());
DefaultSpongeClient
performs best when you create a single instance and reuse it for all of your Remote API calls.
For more information see the DefaultSpongeClient
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-remote-api-client</artifactId>
<version>1.18.0</version>
</dependency>
19.7. Sponge Remote API client for Dart
The Sponge Remote API client for Dart simplifies connecting to a remote Sponge Remote 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 Remote API client uses JSON-RPC 2.0 POST methods.
The fully featured Sponge Remote API client is the client for Dart. Clients for other languages may have less features. |
import 'package:sponge_client_dart/sponge_client_dart.dart';
void main() async {
// Create a new client for an anonymous user.
var client = SpongeClient(
SpongeClientConfiguration('http://localhost:8888'));
// Get the Sponge Remote API version.
var version = await client.getVersion();
print('Sponge Remote API 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', args: ['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 = SpongeClient(
SpongeClientConfiguration('http://localhost:8888')
..username = 'john'
..password = 'password',
);
}
Unless noted otherwise in the release notes, versions of the Remote API client for Dart that have the same major.minor
numbers as the Sponge version are compatible. Note that the Sponge Remote API version can be different than the Sponge version.
The Remote API client for Dart is published as sponge_client_dart
at pub.dartlang.org.
For more information see the SpongeResClient
Dartdoc and the project source code.
The example of using the Remote API client for Dart in the AngularDart web application is hosted at https://github.com/softelnet/sponge_client_angular_dart_example.
19.8. Sponge gRPC API
The Sponge gRPC API allows users to remotely subscribe to Sponge events.
The gRPC has been chosen to provide event subscriptions because the main Sponge Remote API has limited options for push notifications. The Sponge gRPC API can be seen as an addition to the Sponge Remote API.
Events are pushed online, i.e. if a client subscribes to an event type, only events that come after that time will be delivered. |
19.8.1. Operations summary
The following table contains a summary of the gRPC API operations.
Name | URI | Description |
---|---|---|
Get the Sponge Remote API version |
|
Returns the Sponge Remote API version. |
Subscribe events |
|
Subscribes to Sponge events and returns a stream of events. |
Subscribe events and manage a subscription |
|
Subscribes to Sponge events and returns a stream of events. A subscription is managed by a stream of requests. This operation uses bidirectional steaming and is not supported in web gRPC clients. |
19.8.2. Interface specification
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 (SubscribeRequest) returns (stream SubscribeResponse) {}
rpc SubscribeManaged (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;
// Event features as a JSON string.
ObjectValue features = 8;
}
message RequestHeader {
string username = 1;
string password = 2;
string auth_token = 3;
ObjectValue features = 4;
}
message ResponseHeader {
ObjectValue features = 1;
}
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;
}
19.8.3. Configuration
The convention is that the gRPC server port is the Remote API port plus 1
, e.g. if the Remote API port is 8080
then the gRPC API port will be 8081
.
A web gRPC client requires a proxy to forward a gRPC browser request to a backend Sponge gRPC service. In that case the convention is that the proxy port is the Remote API port plus 2
.
19.8.4. Error handling
An application error is returned to a client in a response message, just like in the Remote API. An internal error is returned as a gRPC exception with a status.
19.9. Sponge gRPC API server
The gRPC API server provides a Sponge gRPC API service. The gRPC API server plugin (GrpcApiServerPlugin
) starts the gRPC[https://grpc.io] server. The gRPC API server plugin requires the Remote API plugin because it reuses some parts of the configuration.
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.
Name | Type | Description |
---|---|---|
|
|
If |
|
|
The gRPC API port. Defaults to |
<sponge>
<plugins>
<plugin name="grpcApiServer" class="org.openksavi.sponge.grpcapi.server.GrpcApiServerPlugin">
<configuration>
<autoStart>false</autoStart>
</configuration>
</plugin>
</plugins>
</sponge>
@Configuration
public static class Config extends SpongeCamelConfiguration {
@Bean
public SpongeEngine spongeEngine() {
return SpringSpongeEngine.builder().plugins(camelPlugin(), remoteApiPlugin(), grpcApiPlugin())
.config("sponge_config.xml").build();
}
@Bean
public RemoteApiServerPlugin remoteApiPlugin() {
return new RemoteApiServerPlugin();
}
@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.
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.18.0</version>
</dependency>
19.9.1. 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.
19.9.2. Security
If the Remote API server is published as HTTPS, the gRPC server will be published as secure (TLS) using the same SSL/TLS configuration as the Remote API.
keytool -exportcert -rfc -file remote_api_selfsigned.pem -keystore remote_api_selfsigned.jks -alias remote_api -keypass sponge -storepass sponge
19.9.3. Envoy proxy
The Envoy proxy for web clients can be run in Docker, e.g.:
docker run -it --rm --name envoy -p 8890:8890 -v "$(pwd)/envoy-config.yaml:/etc/envoy/envoy.yaml:ro" envoyproxy/envoy
The example of the Envoy proxy configuration is available in the Sponge source repository.
19.10. 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.
// Create a new Sponge Remote API client.
try (SpongeClient spongeClient = new DefaultSpongeClient(SpongeClientConfiguration.builder()
.url("http://localhost:8080")
.build())) {
// Create a new Sponge gRPC API client associated with the Remote API client.
// It is assumed that this gRPC service is insecure because the Remote API is published as HTTP.
try (SpongeGrpcClient grpcClient = new DefaultSpongeGrpcClient(spongeClient)) {
// Get the Sponge Remote API version.
String version = grpcClient.getVersion();
}
}
The client follows the convention that a gRPC service port is a Remote API port plus 1
. If the gRPC service uses a different port, set this port in the client configuration.
new DefaultSpongeGrpcClient(spongeClient, 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.18.0</version>
</dependency>
19.11. 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.
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 Remote API client.
var spongeClient = SpongeClient(
SpongeClientConfiguration('http://localhost:8888'));
// Create a new Sponge gRPC API client associated with the Remote API client.
// Don't use insecure channel in production.
var grpcClient = DefaultSpongeGrpcClient(spongeClient,
channelOptions:
ChannelOptions(credentials: const ChannelCredentials.insecure()));
// Get the Sponge Remote API 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 Remote API port plus 1
. If the gRPC service uses a different port, set this port in the client configuration.
DefaultSpongeGrpcClient(spongeClient,
configuration: SpongeGrpcClientConfiguration(port: 9000),
channelOptions: ChannelOptions(credentials: const ChannelCredentials.insecure()));
This project uses the Dart implementation of gRPC.
The DefaultSpongeGrpcClient
doesn’t support a web platform.
To use a Sponge gRPC API client in a web client you have to instantiate WebSpongeGrpcClient
instead of DefaultSpongeGrpcClient
and import package:sponge_grpc_client_dart/sponge_grpc_client_dart_web.dart
.
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.
19.12. 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
).
Name | Type | Description |
---|---|---|
|
|
The process name. |
|
|
The process executable. |
|
|
Zero or more process arguments. |
|
|
The process working directory. If |
|
name, value |
Zero or more additional environment variables for the subprocess. |
|
|
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 |
|
|
The standard input redirect type (see the following tables). There are convenience methods |
|
|
The standard output redirect type (see the following tables). There are convenience methods |
|
|
The standard error redirect type (see the following tables). There are convenience methods |
|
|
The the charset of the subprocess streams used if the redirect type is |
|
|
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 |
|
|
Sets the Java regular expression of a line from the process output text stream that signals an error and should cause throwing an exception. |
|
|
The timeout for waiting for a specific line from the process output stream (in seconds). If |
|
|
The input string that will be set as the process standard input. Applicable only if the input redirect type is STRING. |
|
|
he input bytes that will be set as the process standard input. Applicable only if the input redirect type is BINARY. |
Value | Description |
---|---|
|
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. |
|
Sets the destination for subprocess standard input to be the same as those of the current Java process. |
|
Sets the subprocess input as the |
|
Sets the subprocess input as the |
|
Sets the subprocess input as the |
|
Sets the subprocess input as a stream. This is a special case of |
Value | Description |
---|---|
|
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. |
|
Sets the destination for subprocess standard output to be the same as those of the current Java process. |
|
Writes all subprocess standard output to the |
|
Writes all subprocess standard output to the |
|
Writes all subprocess standard output to the |
|
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). |
Value | Description |
---|---|
|
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. |
|
Sets the destination for subprocess error output to be the same as those of the current Java process. |
|
Writes all subprocess error output to the |
|
Writes all subprocess error output to the |
|
Throw an exception if the error output is not empty. The thread that started the subprocess will wait for the subprocess to exit. |
|
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.
In order to run a new process you have to create a process instance builder by invoking sponge.process()
. Then you can run a new process:
-
Synchronously by invoking the
run()
method. The current thread waits for the process exit. -
Asynchronously by invoking the
runAsync()
method. The current thread waits for the process exit only if necessary (in compliance with the process configuration).
ProcessInstance process = engine.getOperations().process("echo", "TEST").outputAsString().run();
String output = process.getOutputString();
process = sponge.process("echo", "TEST").outputAsString().run()
print process.outputString
ProcessInstance process = engine.getOperations().process("printenv")
.arguments("TEST_VARIABLE").env("TEST_VARIABLE", "TEST").outputAsString().run();
process = sponge.process("printenv").arguments("TEST_VARIABLE")
.env("TEST_VARIABLE", "TEST").outputAsString().run()
For more examples see ProcessInstanceTest.java.
19.13. 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.18.0</version>
</dependency>
19.13.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.
|
Name | Type | Description |
---|---|---|
|
|
A Java interface that is a facade to the Py4J entry point object configured on the CPython side. |
|
|
Java side server port. |
|
|
CPython side server port. |
|
XML element/ |
The simple SSL security configuration. |
|
|
Simple security keystore file location on the classpath. |
|
|
Simple security keystore password. |
|
|
Simple security key password. |
|
|
Simple security algorithm. The default value is |
|
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: |
|
|
|
If |
|
|
If |
|
|
The manual or generated Py4J auth token (for both sides). |
|
|
If |
GatewayServerPy4JPlugin
GatewayServerPy4JPlugin
provides integration with CPython using Py4J GatewayServer
.
For more information see the GatewayServerPy4JPlugin
Javadoc.
Sponge side example
<sponge>
<plugins>
<plugin name="py4j" class="org.openksavi.sponge.py4j.GatewayServerPy4JPlugin" />
</plugins>
</sponge>
CPython side example
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()
.
ClientServerPy4JPlugin
ClientServerPy4JPlugin
provides integration with CPython using Py4J ClientServer
.
Name | Type | Description |
---|---|---|
|
|
Auto start of Py4J JavaServer. |
For more information see the ClientServerPy4JPlugin
Javadoc.
Sponge side example
<sponge>
<plugins>
<plugin name="py4j" class="org.openksavi.sponge.py4j.ClientServerPy4JPlugin">
<configuration>
<facadeInterface>org.openksavi.sponge.py4j.PythonService</facadeInterface>
</configuration>
</plugin>
</plugins>
</sponge>
public interface PythonService {
String toUpperCase(String text);
}
# 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
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)
19.13.2. Executing an external Python script
The plugin may run a CPython script as a subprocess.
<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>
19.14. 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 and 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.
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)
<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.18.0</version>
</dependency>
19.15. 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
.
Name | Type | Description |
---|---|---|
|
|
If |
|
|
If |
|
|
A name of a MIDI ShortMessage Sponge event sent by this plugin to the engine. The default value is |
|
|
A name of a MIDI MetaMessage Sponge event sent by this plugin to the engine. The default value is |
|
|
A name of a MIDI SysexMessage Sponge event sent by this plugin to the engine. The default value is |
For more information see the MidiPlugin
Javadoc.
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.18.0</version>
</dependency>
19.16. 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 and 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).
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)
<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.18.0</version>
</dependency>
19.17. 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 and 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.
# 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)
<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.18.0</version>
</dependency>
19.18. 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.18.0</version>
</dependency>
19.18.1. The Digits recognition Remote API service example
This example shows how to expose the TensorFlow machine learning model trained for the MNIST database as a Remote 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).
# 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
Filename | Description |
---|---|
The Java interface of the image classifier Python service. This interface is used by Py4J to expose Python functionality to a Java process. |
|
The Sponge knowledge base that contains definitions of actions that will be exposed in the Sponge Remote API service. The |
|
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 Remote API server. |
|
The main Sponge knowledge base file (compatible with Jython) for that example. |
|
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. |
|
The Python script file (compatible with CPython) that loads the model. If the model file |
|
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 Remote API configuration used for this example is not secure. In the production environment you should use HTTPS as well as user authentication. |
19.19. 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
.
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)
<plugin name="gsm" class="org.openksavi.sponge.gsmmodem.GammuGsmModemPlugin" />
ProcessInstance
APIdef sendSms(recipient, message):
process = sponge.process("gammu").arguments("sendsms", "TEXT", recipient,
None if gsm.canEncodeGsm(message) else "-unicode")
.inputAsString(message).outputAsString().run()
if process.exitCode != 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.18.0</version>
</dependency>
20. 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.
20.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.
20.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.
20.3. Processors
When defining a processor that is not a singleton, its class implementation should provide lightweight creating of new instances.
20.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.
20.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.
20.6. Rules
Rules should be used when triggers functionality is not sufficient.
20.7. Correlators
Correlators should be used when filters, triggers and rules functionality is not sufficient for the problem you try to solve.
20.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.
21. Scripting languages
21.1. Supported scripting languages
Language | Implementation |
---|---|
Python |
Jython |
Ruby |
JRuby |
Groovy |
Groovy |
JavaScript |
Nashorn |
21.2. Python
21.2.1. Limitations
Spring Boot jar
If you run your application with a Spring Boot jar, you should add the following dependency to requiresUnpack
to prevent a ScriptEngine error: ScriptingEngine is null
. For more details see jython-module-not-found-when-packaged-with-spring-boot.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<requiresUnpack>
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-slim</artifactId>
</dependency>
</requiresUnpack>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
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.18.0</version>
</dependency>
21.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.18.0</version>
</dependency>
21.4. Groovy
21.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.
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.18.0</version>
</dependency>
21.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. |
21.5.1. Limitations
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
.
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 . |
Abstract processors
The support for abstract processors is not implemented for processors written in JavaScript.
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.
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.18.0</version>
</dependency>
22. 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 .
|
23. Standard plugins
Name | Description |
---|---|
|
Provides integration with CPython using Py4J ClientServer. |
|
Provides integration with CPython using Py4J GatewayServer. |
|
Provides integration with Apache Camel. |
|
A GSM modem plugin that uses Gammu. |
|
Provides GrovePi integration. GrovePi is an open source platform for connecting Grove Sensors to the Raspberry Pi. |
|
Sponge gRPC API server plugin. |
|
MIDI plugin. |
|
Pi4J plugin. |
|
ReactiveX plugin. |
|
Sponge Remote API server plugin. |
|
A GSM modem plugin that uses a serial port. Experimental. |
|
Provides integration with the Spring framework. |
|
A plugin for the Sponge standalone command-line application. |
24. Predefined knowledge base artifacts
Name | ArtifactId | Description |
---|---|---|
|
|
The Engine Knowledge Base artifact provides |
|
|
The User Management Knowledge Base artifact provides actions for user log in, log out and the default implementation of a sign up with an email action. This artifact is intended to use with the Sponge Remote API. |
|
|
The MPD/MPC Knowledge Base uses the |
25. Applications
The Sponge project contains a few predefined Sponge-based application services that can be used ad hoc. Their source codes are placed under the sponge-app
multi-module project.
25.1. Remote API Demo Service
The Demo Service use case shows how to deploy the Remote API as a servlet. It uses the simple security strategy.
The demo can be launched in the read only mode by setting the Java system property -Ddemo.readOnly=true
.
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
The hosted Demo Service runs in the read only mode. |
Running in Docker
The Docker image openksavi/sponge-demo
contains a predefined Remote API Demo Service running in Tomcat. It can be used as a Sponge mobile client application playground in your local environment.
This image doesn’t work on Raspberry Pi. To run the Demo Service on Raspberry Pi see the next chapter. |
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.
|
docker pull openksavi/sponge-demo
# 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 Remote API and the port 8081
is used by the gRPC API.
# Test the service.
curl http://localhost:8080/version
# Invoke shell in the container.
docker exec -it sponge-demo /bin/bash
# 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/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.
Running in Docker on Raspberry Pi
The Docker image openksavi/sponge-demo-lite
contains the Remote API Demo Service Light that can be run on Raspberry Pi. It can be used as a Sponge mobile client application playground in your Raspberry Pi device. The Demo Service Light provides the same actions as the Demo Service except those related to the digit recognition. It doesn’t contain Tensorflow and runs as a standalone application.
docker pull openksavi/sponge-demo-lite
docker run --name sponge-demo-lite -d --restart always --network=host openksavi/sponge-demo-lite -Dsponge.remoteApiServer.serviceDiscoveryUrl="http://$(hostname -I | awk '{print $1}'):1836"
The default port 1836
is used by the Remote API and the port 1837
is used by the gRPC API.
docker run --name sponge-demo-lite -d --restart always --network=host openksavi/sponge-demo-lite -Dsponge.remoteApiServer.port=1846 -Dsponge.remoteApiServer.serviceDiscoveryUrl="http://$(hostname -I | awk '{print $1}'):1846"
In that case the gRPC API will be published on port 1847
.
curl http://localhost:1836/version
Setting up manually and deploying in Tomcat
First, you have to create the web application and Sponge scripts.
cd sponge-app/sponge-app-demo-service
mvn clean install -Pall
The resulting archive target/sponge-demo-api.war
is the web application providing the Demo Remote 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 # Optionally set the read only mode # demo.readOnly=true
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 http://localhost:8080/version
Running in Jetty
You may also run this example using the Jetty server started by the maven command:
cd sponge-app/sponge-app-demo-service
mvn jetty:run
Tiles servers usage in maps
The example action Action with a geo map
(ActionWithGeoMap
) uses the tiles servers to show a base map:
-
OpenStreetMap. Please see the OpenStreetMap Tile Usage Policy requirements.
-
Google. Please see the Google Maps Terms of Service.
You may use other online XYZ tile map servers as well.
25.2. MPD Client Service
The Sponge MPD Client Service provides a basic set of Music Player Daemon (MPD) client related actions. It can be used in combination with the generic Sponge mobile client application as a simple music player. It includes the sponge-kb-mpd-mpc
knowledge base artifact.
The Sponge MPD service uses the mpc
client commandline to communicate with the MPD server. For performance reasons the Sponge MPD service should be installed on the same machine that runs the MPD server.
One of the aims of the Sponge MPD Client Service application is to showcase the ability of Sponge to publish a set of commandline tool invocations as a Remote API and use it in a mobile application. It is not a replacement for existing MPD clients available for mobile devices because they connect to an MPD server directly. The architecture shown in this example introduces an additional layer between the MPD server and the GUI MPD client (in that case the generic Sponge mobile client application) that can have a noticable impact on the performance. |
Running in Docker
The Docker image openksavi/sponge-mpd
contains the predefined Sponge MPD service.
The service runs in Docker on Raspberry Pi with an already configured MPD. There is a number of Linux distributions for Raspberry PI that provide well configured music player features. For this example the moOde audio player has been chosen.
The architecture of this solution is: Raspberry Pi with the MPD server (running in the moOde audio distribution) and Docker containing the Sponge MPD service.
Installing the Sponge MPD service:
sudo curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh sudo usermod -aG docker pi
-
Relogin.
-
Run the Sponge MPD service in Docker.
docker pull openksavi/sponge-mpd docker run --name sponge-mpd -d --restart always --network=host openksavi/sponge-mpd \ -Dsponge.remoteApiServer.serviceDiscoveryUrl="http://$(hostname -I | awk '{print $1}'):1836"
Setting the sponge.remoteApiServer.serviceDiscoveryUrl
property allows client applications (e.g. the Sponge Remote mobile application) to automatically find this service on the local network. If you want to change the service name, provide the sponge.remoteApiServer.name
property as well, e.g. -Dsponge.remoteApiServer.name="Docker MPD"
.
-
Check the Sponge MPD service logs.
docker logs sponge-mpd -f
If you have a Musixmatch API key, you can configure the service to access song lyrics by adding -DmusixmatchApiKey=YOUR_MUSIXMATCH_API_KEY
to the docker run
command line. Song lyrics can be shown in the Sponge Remote application.
Limitations:
-
The MPD connection configuration (e.g. the MPD host) is not persisted between restarts.
-
This Docker image has been tested only on a Linux host.
26. Examples
26.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
inonStartup
function of the knowledge base namednewsGenerator
. Each event has custom attributes:source
andtitle
. -
Every event named
news
is filtered to discard news that have empty or short (according tonewsFilterWordThreshold
configuration property) titles. This is done byNewsFilter
filter. -
Events named
news
are logged byLogNewsTrigger
trigger. -
When there are no new
news
events (that passed filters) for a specified time, thenalarm
event is sent. This is done byNoNewNewsAlarmRule
rule. -
LatestNewsCorrelator
correlator listens tonews
events and stores the latest news instoragePlugin
plugin in a Pythondeque
. The number of latest news is configured aslatestNewsMaxSize
property. -
When
alarm
event happens, this fact is logged byAlarmTrigger
trigger usingechoPlugin
plugin andEmphasizeAction
action.
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();
}
}
<?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>
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))
from java.util.concurrent.atomic import AtomicBoolean
# Set initial values for variables.
def onInit():
sponge.setVariable("alarmSounded", AtomicBoolean(False))
# 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
/**
* 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;
}
}
});
# 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)
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;
}
}
26.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 thesimulator
knowledge base. -
When
alarm
event happens, not onlyAlarmTrigger
(as described in the previous example) handles that event, but here alsoForwardAlarmTrigger
trigger, defined in theconsumer
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.
-
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();
}
}
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.support.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();
}
}
<?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&consumer.delay=1000</property>
<!-- Duration of reading RSS feeds from sources (in seconds). -->
<property name="durationOfReadingRss" variable="true">20</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>
# 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"})
# 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)
26.3. IoT on Raspberry Pi project
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 Remote 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
Sensor / actuator | Description |
---|---|
Connected to the port D2. |
|
Connected to the port A1. |
|
Connected to the port A0. |
|
Connected to the port A2. |
|
Connected to the port D4. |
|
Connected to the port D5. |
|
Connected to the port D7. |
|
Connected to the port I2C-1. |
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 commands are invoked as the pi
user.
-
For SMS sending the
gammu
utility should be installed. -
Mosquitto MQTT broker.
-
The Pi4J 1.2 library, that is used in this example, works only on Oracle JDK 8.
$ 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.18.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.
Remote API Actions
The subset of Sponge actions is published via the Sponge Remote 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.
Name | Description |
---|---|
SetGrovePiMode |
Sets the GrovePi mode ( |
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 |
OsDmesg |
Executes |
The Remote 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.18.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.18.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.
26.4. 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.
Name | Description |
---|---|
Shows how to use actions. |
|
Shows how to use correlators. The correlator creates an event log - a list of events that it listens to. |
|
Shows how to use correlators with duration. |
|
|
Shows event clone policies. |
Shows sending events using Cron. |
|
Shows how to remove scheduled events. |
|
Shows how to use a deduplication filter to prevent from processing many events that carry the same information. |
|
Shows how to use a Java-based filter. |
|
Shows how to use script-based filters. |
|
|
Hello world action complete example. |
|
Hello world trigger complete example. |
Shows how to use knowledge base callback functions. |
|
Shows how to load an additional knowledge base file. |
|
Shows knowledge base operations. |
|
|
Shows how to use a scripting language specific library (e.g. |
|
Shows how to define and use a Java-based plugin. |
|
Shows how to define and use a script-based plugin. |
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. |
|
|
Shows how to define and use rules that have different event modes, durations etc. |
Heartbeat complete example. |
|
Shows how to define and use rules that have |
|
Shows how to define and use rules that have |
|
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. |
|
|
Shows how to define and use triggers. |
Shows how to define and use triggers that specify events they listen to as a pattern based on a regular expression. |
26.5. Functionality examples
The features examples show how to use some of Sponge functionalities. They are not implemented in all supported scripting languages.
Name | Description |
---|---|
|
Shows how to send a chain of events, each carrying a Fibonacci number as an attribute. |
|
Shows how to set engine parameters in the XML configuration file. |
|
Shows how to use event name patterns and how to enable/disable processors manually. |
Shows how to integrate with Spring framework. |
|
Shows how to handle messages coming from Apache Camel route by a Sponge trigger. |
|
Shows how to handle messages coming from Sponge by an Apache Camel route. |
|
Shows how to integrate with Apache Camel to send and handle Sponge events based on RSS feeds. This example uses a Spring configuration. |
|
Shows how to handle messages coming from Apache Camel route by a Sponge trigger using an overridden Camel producer action. |
|
Shows how to handle messages coming from Apache Camel route by a Sponge trigger using a custom Camel producer action. |
|
Shows sending Camel messages to many endpoints in a single Sponge trigger. |
|
Shows how to integrate with CPython program using Py4J - Java server. |
|
Shows how to integrate with CPython program using Py4J - Python server. |
|
Shows how to integrate with CPython program using Py4J - Java server with TLS security. |
|
|
Shows how to generate MIDI sounds in a Sponge knowledge base. |
|
Shows how to process MIDI messages created by an external MIDI input device. |
|
Shows how MIDI messages created by a MIDI sequencer playing a MIDI file could be processed in a Sponge knowledge base. |
26.6. Standalone examples
The standalone examples show how to use some of Sponge features in the standalone command-line application.
Name | Description |
---|---|
|
This example is based on complete example project of embedding Sponge - News, but adjusted to a standalone version. |
|
This example is based on complete example project of embedding Sponge - Camel RSS News, but adjusted to a standalone version. |
|
Camel routes in Groovy Spring configuration. |
|
Camel context and routes in XML Spring configuration. |
27. Maven artifacts
The groupId
of Sponge Maven artifacts is org.openksavi.sponge
.
ArtifactId | Central Maven Repository | Description |
---|---|---|
|
Yes |
The parent project. |
|
Yes |
The Bill Of Materials style pom.xml. |
|
Yes |
The Sponge API. |
|
Yes |
The Sponge core implementation. This artifact includes a shaded Guava library. |
|
Yes |
The support for Python-based scripting knowledge bases using Jython. |
|
Yes |
The support for Python-based scripting knowledge bases using Jython. It contains several shaded dependencies. It is an experimental artifact in case of dependency conflicts in an embedded Sponge. |
|
Yes |
The support for Ruby-based scripting knowledge bases using JRuby. |
|
Yes |
The support for Groovy-based scripting knowledge bases. |
|
Yes |
The support for JavaScript-based scripting knowledge bases using Nashorn. |
|
Yes |
The support for Kotlin-based non scripting knowledge bases. |
|
Yes |
The wrappers for Operating System signals. |
|
Yes |
The Apache Camel integration. |
|
Yes |
The Spring framework integration. |
|
Yes |
The Spring Boot starter for Sponge. |
|
Yes |
The Spring Boot starter for Sponge Remote API service. |
|
Yes |
The CPython integration that uses Py4J. |
|
Yes |
The MIDI integration. |
|
Yes |
The Pi4J (for Raspberry Pi) library integration. |
|
Yes |
The ReactiveX integration. |
|
Yes |
The predefined action and type features. |
|
Yes |
The Sponge Remote API client. |
|
Yes |
The Sponge Remote API server. |
|
Yes |
The Sponge Remote API tools. |
|
Yes |
The Sponge gRPC API client. |
|
Yes |
The Sponge gRPC API server. |
|
Yes |
The TensorFlow integration. |
|
Yes |
The base classes for the standalone command-line application. |
|
Yes |
The Sponge logging used by the standalone application. |
|
Yes |
Base classes used in examples. |
|
Yes |
The Sponge test support. |
|
No |
The standalone command-line application. |
|
No |
Dependencies for external libraries used by the standalone command-line application. |
|
No |
The GrovePi (for Raspberry Pi) library integration. |
|
No |
Complete example projects. |
|
No |
Contains documentation, release configuration, project pages etc. |
|
No |
Sponge integration tests. |
28. 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 can place JAR files into the lib
directory. You should use only compatible versions of these libraries.
The standalone command-line application in the interactive (REPL) mode supports history of entered commands/expressions (by the upwards arrow).
28.1. Command-line options
Option | Description |
---|---|
|
Use the given Sponge XML configuration file. Only one configuration file may be provided. |
|
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. |
|
Use given Spring configuration file. This option may be used more than once to provide many Spring configuration files. |
|
Create an Apache Camel context. |
|
Run in an interactive (REPL) 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. |
|
Run the given script language intepreter in an interactive mode as a new, empty knowledge base. Supported languages: python, groovy, ruby, javascript. Applicable only in an interactive mode. |
|
Supresses logging to the console. Applicable only in a non interactive mode. |
|
Print exception stack traces to the console. |
|
Enable more debugging info. Print all logs to the console (including exception stack traces). Applicable only in an interactive mode. Options |
|
Print help message and exit. |
|
Print the version information and exit. |
|
Set the Java system property. |
Any left-over non-recognized options and arguments are accessible in a knowledge base as a list of strings sponge.engine.args
.
28.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 |
---|---|
|
|
|
Same as |
|
|
# 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 with debug.
./sponge -k filtersKb=../examples/script/py/filters.py -i -d
# 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
28.3. Environment variables
Optionally you may set the environment variable SPONGE_HOME
.
cd sponge-1.18.0
export SPONGE_HOME=`pwd`
cd sponge-1.18.0
set SPONGE_HOME=%cd%
28.4. Standalone plugin configuration parameters
The standalone command-line application uses the StandalonePlugin
that creates Spring and Camel contexts if necessary. A default standalone plugin is created by the application, however a Sponge XML configuration file can define a standalone plugin explicitly.
Name | Type | Description |
---|---|---|
|
XML element |
Spring configuration. A Spring context is created only when there is a |
|
|
The optional |
|
|
The optional |
|
|
Spring configuration files. The Spring context implementation used here is |
28.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 "standalone"
.
<?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="standalone" 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 a Spring bean name that will reference the engine instance.
28.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 settingcamel
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 could use Camel routes to send events to Sponge from an external systems, for example by configuring Camel Rest DSL.
28.6.1. Spring XML configuration
<?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="standalone" 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>
<?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>
28.6.2. Spring Groovy configuration
<?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="standalone" class="org.openksavi.sponge.standalone.StandalonePlugin">
<configuration>
<spring camel="true">
<file>examples/standalone/camel_route_groovy/SpringCamelGroovyConfigExample.groovy</file>
</spring>
</configuration>
</plugin>
</plugins>
</sponge>
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)
}
28.6.3. Management of Camel routes in an interactive mode
> print(camel.context.status)
> print(camel.context.routes)
> camel.context.stopRoute("rss")
> print(camel.context.removeRoute("rss"))
> print(camel.context.routes)
28.7. Logging and exception reporting
28.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
:
<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.
./sponge -c ../examples/script/py/triggers_hello_world.xml -Dlogback.configurationFile=custom_logback.xml
28.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.
28.8. Remote API
You may enable the Sponge Remote 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 Remote API (autoStart
must be turned off) is required because the Remote API server must start after the Camel context has started.
For more information see examples in the source code.
28.9. Running examples
# Change directory to Sponge bin/.
# Run with the specified Sponge XML configuration file.
./sponge -c ../examples/standalone/news/config/config.xml
# Change directory to Sponge bin/.
# Run with the specified Sponge XML configuration file.
./sponge -c ../examples/standalone/camel_rss_news/config/config.xml
28.10. Directory structure
Directory | Description |
---|---|
|
Shell scripts. |
|
Configuration files. |
|
Documentation. |
|
Example configurations and knowledge base files. |
|
Libraries used by Sponge. |
|
Log files. |
28.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.
28.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.
Component | Description |
---|---|
Executing system commands |
|
Grape |
|
HTTP |
|
JDBC |
|
JMS |
|
JMX |
|
Mustache |
|
Netty |
|
Netty HTTP |
|
Paho/MQTT |
|
Quartz |
|
RSS |
|
SNMP |
|
SQL |
|
SSH |
|
Input/output/error/file stream |
|
Velocity |
|
XMPP/Jabber |
Data format | Description |
---|---|
JSON |
|
CSV |
|
Tar format |
|
Syslog |
28.11.2. Other components
Component |
Description |
Provides an API for sending emails. |
29. Third party software
Sponge uses third party software released under various open-source licenses.
Package | License | Description |
---|---|---|
Apache 2.0 |
Used for reading configuration files. |
|
Apache 2.0 |
Used for sending emails. |
|
Supports writing scripting knowledge bases in Python. |
||
EPL 1.0, GPL 2 or LGPL 2.1 |
Supports writing scripting knowledge bases in Ruby. |
|
Apache 2.0 |
Supports writing scripting knowledge bases in Groovy. |
|
GPL with a linking exception |
Supports writing scripting knowledge bases in JavaScript. |
|
Apache 2.0 |
Supports writing knowledge bases in Kotlin. |
|
Apache 2.0 |
Used for scheduling events (e.g. provides cron functionality). |
|
Apache 2.0 |
Used as an integration facade to external systems. |
|
Apache 2.0 |
Used for integration with Spring framework. |
|
Apache 2.0 |
Used for services and as a utilities library. |
|
BSD |
Used for handling console input in an interactive mode. |
|
BSD |
Used for integration with CPython programs. |
|
LGPL 3.0 |
Used for integration with Raspberry Pi hardware. |
|
Apache 2.0 |
Used for integration with GrovePi (for Raspberry Pi) hardware. |
|
Apache 2.0 |
An open source machine learning framework. |
|
Apache 2.0 |
RxJava is a Java VM implementation of Reactive Extensions. |
|
Reflections is a Java runtime metadata analysis library. |
||
Apache 2.0 |
JSON Web Token for Java. |
|
Apache 2.0 |
An implementation of multi-cast DNS in Java. It supports service discovery and service registration. |
The complete list of these libraries may be found in the THIRD-PARTY.txt
and licenses.xml
files of the standalone distribution.