You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2021/09/16 07:52:32 UTC
[unomi] branch master updated: UNOMI-508 provide Unomi Groovy
actions endpoint (#337)
This is an automated email from the ASF dual-hosted git repository.
shuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new 1d0322d UNOMI-508 provide Unomi Groovy actions endpoint (#337)
1d0322d is described below
commit 1d0322d388c25f55a0c5f02af0d802963df03997
Author: jsinovassin <58...@users.noreply.github.com>
AuthorDate: Thu Sep 16 09:47:29 2021 +0200
UNOMI-508 provide Unomi Groovy actions endpoint (#337)
* add endpoint to allow to create groovy actions
* handle feedback
* add integrations tests
* use IOUtils library
* improve log message
* remove action type on action deletion
---
.../main/java/org/apache/unomi/api/Parameter.java | 6 +
extensions/groovy-actions/karaf-kar/pom.xml | 23 ++-
extensions/groovy-actions/pom.xml | 1 +
.../groovy-actions/{services => rest}/pom.xml | 80 +++++----
.../groovy/actions/rest/GroovyActionsEndPoint.java | 94 +++++++++++
extensions/groovy-actions/services/pom.xml | 33 +++-
.../apache/unomi/groovy/actions/GroovyAction.java | 58 +++++++
.../groovy/actions/GroovyActionDispatcher.java | 89 ++++++++++
.../actions}/GroovyBundleResourceConnector.java | 2 +-
.../unomi/groovy/actions/annotations/Action.java | 69 ++++++++
.../groovy/actions/annotations/Parameter.java | 43 +++++
.../actions/listener/GroovyActionListener.java | 165 +++++++++++++++++++
.../actions/services/GroovyActionsService.java | 47 ++++++
.../services/impl/GroovyActionsServiceImpl.java | 165 +++++++++++++++++++
.../services/actions/groovy/GroovyAction.java | 59 -------
.../actions/groovy/GroovyActionDispatcher.java | 179 ---------------------
.../META-INF/cxs/mappings/groovyAction.json | 28 ++++
.../resources/OSGI-INF/blueprint/blueprint.xml | 40 ++++-
.../resources/org.apache.unomi.groovy.actions.cfg | 19 +++
.../test/java/org/apache/unomi/itests/AllITs.java | 1 +
.../test/java/org/apache/unomi/itests/BaseIT.java | 2 +
.../unomi/itests/GroovyActionsServiceIT.java | 113 +++++++++++++
itests/src/test/resources/groovy/MyAction.groovy | 39 +++++
package/pom.xml | 10 ++
.../main/resources/etc/custom.system.properties | 7 +
25 files changed, 1086 insertions(+), 286 deletions(-)
diff --git a/api/src/main/java/org/apache/unomi/api/Parameter.java b/api/src/main/java/org/apache/unomi/api/Parameter.java
index a24fe9b..4833c5a 100644
--- a/api/src/main/java/org/apache/unomi/api/Parameter.java
+++ b/api/src/main/java/org/apache/unomi/api/Parameter.java
@@ -35,6 +35,12 @@ public class Parameter implements Serializable {
public Parameter() {
}
+ public Parameter(String id, String type, boolean multivalued) {
+ this.id = id;
+ this.type = type;
+ this.multivalued = multivalued;
+ }
+
public String getId() {
return id;
}
diff --git a/extensions/groovy-actions/karaf-kar/pom.xml b/extensions/groovy-actions/karaf-kar/pom.xml
index 5a5e409..b34afa2 100644
--- a/extensions/groovy-actions/karaf-kar/pom.xml
+++ b/extensions/groovy-actions/karaf-kar/pom.xml
@@ -16,7 +16,8 @@
~ limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-groovy-actions-root</artifactId>
@@ -26,7 +27,9 @@
<artifactId>unomi-groovy-actions</artifactId>
<name>Apache Unomi :: Extensions :: Groovy Actions :: Apache Karaf Feature and KAR archive</name>
- <description>Apache Karaf Feature and KAR archive for the Apache Unomi Context Server extension that provides the possibility to use Groovy for actions</description>
+ <description>Apache Karaf Feature and KAR archive for the Apache Unomi Context Server extension that provides the possibility to use
+ Groovy for actions
+ </description>
<packaging>kar</packaging>
<dependencies>
@@ -52,6 +55,11 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-groovy-actions-rest</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>io.github.http-builder-ng</groupId>
<artifactId>http-builder-ng-core</artifactId>
<version>1.0.4</version>
@@ -61,7 +69,11 @@
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
-
+ <dependency>
+ <groupId>com.sun.activation</groupId>
+ <artifactId>javax.activation</artifactId>
+ <version>1.2.0</version>
+ </dependency>
</dependencies>
<build>
@@ -72,6 +84,11 @@
<artifactId>karaf-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
+ <dependencyFeatures>
+ <dependencyFeature>
+ unomi-kar
+ </dependencyFeature>
+ </dependencyFeatures>
<includeTransitiveDependency>false</includeTransitiveDependency>
</configuration>
</plugin>
diff --git a/extensions/groovy-actions/pom.xml b/extensions/groovy-actions/pom.xml
index 8c1c0de..6b39867 100644
--- a/extensions/groovy-actions/pom.xml
+++ b/extensions/groovy-actions/pom.xml
@@ -31,6 +31,7 @@
<modules>
<module>services</module>
+ <module>rest</module>
<module>karaf-kar</module>
</modules>
diff --git a/extensions/groovy-actions/services/pom.xml b/extensions/groovy-actions/rest/pom.xml
similarity index 50%
copy from extensions/groovy-actions/services/pom.xml
copy to extensions/groovy-actions/rest/pom.xml
index 2658a0e..f0a7055 100644
--- a/extensions/groovy-actions/services/pom.xml
+++ b/extensions/groovy-actions/rest/pom.xml
@@ -16,18 +16,18 @@
~ limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-groovy-actions-root</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>1.6.0-SNAPSHOT</version>
</parent>
-
<modelVersion>4.0.0</modelVersion>
- <artifactId>unomi-groovy-actions-services</artifactId>
- <name>Apache Unomi :: Extensions :: Groovy Actions :: Service</name>
- <description>Service implementation for the Apache Unomi Context Server extension that provides the possibility to implement actions in Groovy</description>
+ <artifactId>unomi-groovy-actions-rest</artifactId>
+ <name>Apache Unomi :: Extensions :: Groovy Actions :: REST API</name>
+ <description>REST API for the Apache Unomi Context Server extension that integrates with groovy actions</description>
<packaging>bundle</packaging>
<dependencies>
@@ -37,47 +37,59 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+
<dependency>
- <groupId>org.osgi</groupId>
- <artifactId>osgi.core</artifactId>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-groovy-actions-services</artifactId>
+ <version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.osgi</groupId>
- <artifactId>osgi.cmpn</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-frontend-jaxws</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-transports-http</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-rs-security-cors</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.jaxrs</groupId>
+ <artifactId>jackson-jaxrs-json-provider</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.unomi</groupId>
- <artifactId>unomi-metrics</artifactId>
+ <artifactId>unomi-persistence-spi</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
-
<dependency>
- <groupId>org.codehaus.groovy</groupId>
- <artifactId>groovy</artifactId>
- <version>${groovy.version}</version>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.cmpn</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
</dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.felix</groupId>
- <artifactId>maven-bundle-plugin</artifactId>
- <extensions>true</extensions>
- <configuration>
- <instructions>
- <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
- <Import-Package>
- *
- </Import-Package>
- </instructions>
- </configuration>
- </plugin>
- </plugins>
- </build>
-</project>
+</project>
\ No newline at end of file
diff --git a/extensions/groovy-actions/rest/src/main/java/org/apache/unomi/groovy/actions/rest/GroovyActionsEndPoint.java b/extensions/groovy-actions/rest/src/main/java/org/apache/unomi/groovy/actions/rest/GroovyActionsEndPoint.java
new file mode 100644
index 0000000..f08d46b
--- /dev/null
+++ b/extensions/groovy-actions/rest/src/main/java/org/apache/unomi/groovy/actions/rest/GroovyActionsEndPoint.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.unomi.groovy.actions.rest;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.Multipart;
+import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
+import org.apache.unomi.groovy.actions.services.GroovyActionsService;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jws.WebMethod;
+import javax.jws.WebService;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+
+@WebService
+@Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
+@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = true)
+@Path("/groovyActions")
+@Component(service = GroovyActionsEndPoint.class, property = "osgi.jaxrs.resource=true")
+public class GroovyActionsEndPoint {
+
+ private static final Logger logger = LoggerFactory.getLogger(GroovyActionsEndPoint.class.getName());
+
+ @Reference
+ private GroovyActionsService groovyActionsService;
+
+ public GroovyActionsEndPoint() {
+ logger.info("Initializing groovy actions service endpoint...");
+ }
+
+ @WebMethod(exclude = true)
+ public void setGroovyActionsService(GroovyActionsService groovyActionsService) {
+ this.groovyActionsService = groovyActionsService;
+ }
+
+ /**
+ * Save a groovy action file and create an actionType entry to allow to call this action
+ *
+ * @param file the file to save
+ * @return
+ */
+ @POST
+ @Path("/")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response save(@Multipart(value = "file") Attachment file) {
+ try {
+ groovyActionsService
+ .save(file.getContentDisposition().getFilename().replace(".groovy", ""), IOUtils.toString(file.getDataHandler().getInputStream()));
+ } catch (IOException e) {
+ logger.error("Error while processing groovy file", e);
+ return Response.serverError().build();
+ }
+ return Response.ok().build();
+ }
+
+ /**
+ * Deletes the rule identified by the specified identifier.
+ *
+ * @param actionId the identifier of the groovy action that we want to delete
+ */
+ @DELETE
+ @Path("/{actionId}")
+ public void remove(@PathParam("actionId") String actionId) {
+ groovyActionsService.remove(actionId);
+ }
+}
diff --git a/extensions/groovy-actions/services/pom.xml b/extensions/groovy-actions/services/pom.xml
index 2658a0e..a5d1c8d 100644
--- a/extensions/groovy-actions/services/pom.xml
+++ b/extensions/groovy-actions/services/pom.xml
@@ -16,7 +16,8 @@
~ limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-groovy-actions-root</artifactId>
@@ -27,7 +28,9 @@
<artifactId>unomi-groovy-actions-services</artifactId>
<name>Apache Unomi :: Extensions :: Groovy Actions :: Service</name>
- <description>Service implementation for the Apache Unomi Context Server extension that provides the possibility to implement actions in Groovy</description>
+ <description>Service implementation for the Apache Unomi Context Server extension that provides the possibility to implement actions in
+ Groovy
+ </description>
<packaging>bundle</packaging>
<dependencies>
@@ -54,13 +57,32 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
-
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-persistence-spi</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>${groovy.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
</dependencies>
<build>
@@ -72,9 +94,8 @@
<configuration>
<instructions>
<Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
- <Import-Package>
- *
- </Import-Package>
+ <Unomi-Source-Folders>${project.basedir}</Unomi-Source-Folders>
+ <DynamicImport-Package>*</DynamicImport-Package>
</instructions>
</configuration>
</plugin>
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyAction.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyAction.java
new file mode 100644
index 0000000..842eb6c
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyAction.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions;
+
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.MetadataItem;
+
+/**
+ * Object which represents a Groovy action (including its script)
+ */
+public class GroovyAction extends MetadataItem {
+ public static final String ITEM_TYPE = "groovyAction";
+
+ private String name;
+ private String script;
+
+ /**
+ * Instantiates a new Groovy action.
+ */
+ public GroovyAction() {
+ }
+
+ public GroovyAction(String name, String script) {
+ super(new Metadata(name));
+ this.name = name;
+ this.script = script;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getScript() {
+ return script;
+ }
+
+ public void setScript(String script) {
+ this.script = script;
+ }
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyActionDispatcher.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyActionDispatcher.java
new file mode 100644
index 0000000..0a2b02c
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyActionDispatcher.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions;
+
+import groovy.lang.GroovyCodeSource;
+import groovy.lang.GroovyObject;
+import groovy.util.GroovyScriptEngine;
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.actions.ActionDispatcher;
+import org.apache.unomi.groovy.actions.services.GroovyActionsService;
+import org.apache.unomi.metrics.MetricAdapter;
+import org.apache.unomi.metrics.MetricsService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleWiring;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An implementation of an ActionDispatcher for the Groovy language. This dispatcher will load the groovy action script matching to an
+ * actionName. If a script if found, it will be executed.
+ */
+public class GroovyActionDispatcher implements ActionDispatcher {
+
+ private static final Logger logger = LoggerFactory.getLogger(GroovyActionDispatcher.class.getName());
+
+ private static final String GROOVY_PREFIX = "groovy";
+
+ private MetricsService metricsService;
+
+ private GroovyActionsService groovyActionsService;
+
+ private BundleContext bundleContext;
+
+ public void setMetricsService(MetricsService metricsService) {
+ this.metricsService = metricsService;
+ }
+
+ public void setGroovyActionsService(GroovyActionsService groovyActionsService) {
+ this.groovyActionsService = groovyActionsService;
+ }
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public String getPrefix() {
+ return GROOVY_PREFIX;
+ }
+
+ public Integer execute(Action action, Event event, String actionName) {
+ GroovyAction groovyAction = groovyActionsService.getGroovyAction(actionName);
+ if (groovyAction == null) {
+ logger.warn("Couldn't find a Groovy action with name {}, action will not execute !", actionName);
+ } else {
+ try {
+ return new MetricAdapter<Integer>(metricsService, this.getClass().getName() + ".action.groovy." + actionName) {
+ @Override
+ public Integer execute(Object... args) throws Exception {
+ GroovyBundleResourceConnector bundleResourceConnector = new GroovyBundleResourceConnector(bundleContext);
+ GroovyScriptEngine engine = new GroovyScriptEngine(bundleResourceConnector,
+ bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader());
+
+ Class clazzScript = engine.getGroovyClassLoader().parseClass(new GroovyCodeSource(groovyAction.getScript(), actionName, "/groovy/script"));
+ GroovyObject groovyObj = (GroovyObject) clazzScript.newInstance();
+ return Integer.valueOf((String) groovyObj.invokeMethod("execute", new Object[] { action, event }));
+ }
+ }.runWithTimer();
+ } catch (Exception e) {
+ logger.error("Error executing Groovy action with key=" + actionName, e);
+ }
+ }
+ return null;
+ }
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyBundleResourceConnector.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyBundleResourceConnector.java
similarity index 98%
rename from extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyBundleResourceConnector.java
rename to extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyBundleResourceConnector.java
index af316fd..e790d96 100644
--- a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyBundleResourceConnector.java
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyBundleResourceConnector.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.services.actions.groovy;
+package org.apache.unomi.groovy.actions;
import groovy.util.ResourceConnector;
import groovy.util.ResourceException;
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/annotations/Action.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/annotations/Action.java
new file mode 100644
index 0000000..58f1ae6
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/annotations/Action.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation is designed to describe the groovy actions which are created from groovy file, the informations added with this
+ * annotation will be processed to create an action type entry in elastic search.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Action {
+
+ /**
+ * @return id of the action
+ */
+ String id();
+
+ /**
+ * @return name of the action
+ */
+ String name() default "";
+
+ /**
+ * @return description of the action
+ */
+ String description() default "";
+
+ /**
+ * Action Executor to allow to define which action will be called by the action dispatcher
+ * The groovy action have to be prefixed by groovy:
+ * @return actionExecutor of the action
+ */
+ String actionExecutor();
+
+ /**
+ * @return action is hidden
+ */
+ boolean hidden() default false;
+
+ /**
+ * Parameters specific to the action
+ * The value of the parameters can be retrieved in the action like the following:
+ * action.getParameterValues().get(<parameter name>);
+ * @return parameters
+ */
+ Parameter[] parameters() default {};
+
+ /**
+ * List of tags that help to classify the action
+ * @return systemTags
+ */
+ String[] systemTags() default {};
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/annotations/Parameter.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/annotations/Parameter.java
new file mode 100644
index 0000000..7515e2a
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/annotations/Parameter.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Annotation designed to allow to add parameters to an action
+ * @return parameters
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Parameter {
+
+ /**
+ * @return Id of the parameter
+ */
+ String id();
+
+ /**
+ * @return type of the parameter
+ */
+ String type();
+
+ /**
+ * @return multivalued parameter
+ */
+ boolean multivalued();
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/listener/GroovyActionListener.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/listener/GroovyActionListener.java
new file mode 100644
index 0000000..c870640
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/listener/GroovyActionListener.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions.listener;
+
+import groovy.util.GroovyScriptEngine;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.unomi.groovy.actions.GroovyAction;
+import org.apache.unomi.groovy.actions.GroovyBundleResourceConnector;
+import org.apache.unomi.groovy.actions.services.GroovyActionsService;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.SynchronousBundleListener;
+import org.osgi.framework.wiring.BundleWiring;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+
+/**
+ * An implementation of a BundleListener for the Groovy language.
+ * It will load the groovy files in the folder META-INF/cxs/actions.
+ * The description of the action will be loaded from the ActionDescriptor annotation present in the groovy file.
+ * The script will be stored in the ES index groovyAction
+ */
+public class GroovyActionListener implements SynchronousBundleListener {
+
+ private static final Logger logger = LoggerFactory.getLogger(GroovyActionListener.class.getName());
+ public static final String ENTRIES_LOCATION = "META-INF/cxs/actions";
+ private PersistenceService persistenceService;
+
+ private GroovyActionsService groovyActionsService;
+ private BundleContext bundleContext;
+
+ public void setPersistenceService(PersistenceService persistenceService) {
+ this.persistenceService = persistenceService;
+ }
+
+ public void setGroovyActionsService(GroovyActionsService groovyActionsService) {
+ this.groovyActionsService = groovyActionsService;
+ }
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public void postConstruct() {
+ logger.debug("postConstruct {}", bundleContext.getBundle());
+ createIndex();
+ loadGroovyActions(bundleContext);
+ for (Bundle bundle : bundleContext.getBundles()) {
+ if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
+ loadGroovyActions(bundle.getBundleContext());
+ }
+ }
+
+ bundleContext.addBundleListener(this);
+ logger.info("Groovy Action Dispatcher initialized.");
+ }
+
+ public void preDestroy() {
+ processBundleStop(bundleContext);
+ bundleContext.removeBundleListener(this);
+ logger.info("Groovy Action Dispatcher shutdown.");
+ }
+
+ private void processBundleStartup(BundleContext bundleContext) {
+ if (bundleContext == null) {
+ return;
+ }
+ loadGroovyActions(bundleContext);
+ }
+
+ private void processBundleStop(BundleContext bundleContext) {
+ if (bundleContext == null) {
+ return;
+ }
+ unloadGroovyActions(bundleContext);
+ }
+
+ public void bundleChanged(BundleEvent event) {
+ switch (event.getType()) {
+ case BundleEvent.STARTED:
+ processBundleStartup(event.getBundle().getBundleContext());
+ break;
+ case BundleEvent.STOPPING:
+ if (!event.getBundle().getSymbolicName().equals("org.apache.unomi.groovy-actions-services")) {
+ processBundleStop(event.getBundle().getBundleContext());
+ }
+ break;
+ }
+ }
+
+ public void createIndex() {
+ if (persistenceService.createIndex(GroovyAction.ITEM_TYPE)) {
+ logger.info("GroovyAction index created");
+ } else {
+ logger.info("GroovyAction index already exists");
+ }
+ }
+
+ private void addGroovyAction(URL groovyActionURL) {
+ try {
+ groovyActionsService.save(FilenameUtils.getName(groovyActionURL.getPath()),IOUtils.toString(groovyActionURL.openStream()));
+ } catch (IOException e) {
+ logger.error("Failed to load the groovy action {}", groovyActionURL.getPath(), e);
+ }
+ }
+
+ private void removeGroovyActions(BundleContext bundleContext, URL groovyActionURL) {
+ GroovyBundleResourceConnector bundleResourceConnector = new GroovyBundleResourceConnector(bundleContext);
+ GroovyScriptEngine engine = new GroovyScriptEngine(bundleResourceConnector,
+ bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader());
+ try {
+ Class classScript = engine.getGroovyClassLoader().parseClass(IOUtils.toString(groovyActionURL.openStream()));
+ groovyActionsService.remove(classScript.getName());
+ logger.info("The script {} has been removed.", classScript);
+ } catch (IOException e) {
+ logger.error("Failed to parse groovy action file", e);
+ }
+ }
+
+ private void loadGroovyActions(BundleContext bundleContext) {
+ Enumeration<URL> bundleGroovyActions = bundleContext.getBundle().findEntries(ENTRIES_LOCATION, "*.groovy", true);
+ if (bundleGroovyActions == null) {
+ return;
+ }
+ while (bundleGroovyActions.hasMoreElements()) {
+ URL groovyActionURL = bundleGroovyActions.nextElement();
+ logger.debug("Found Groovy action at {}, loading... ", groovyActionURL.getPath());
+ addGroovyAction(groovyActionURL);
+ }
+ }
+
+ private void unloadGroovyActions(BundleContext bundleContext) {
+ Enumeration<URL> bundleGroovyActions = bundleContext.getBundle().findEntries(ENTRIES_LOCATION, "*.groovy", true);
+ if (bundleGroovyActions == null) {
+ return;
+ }
+
+ while (bundleGroovyActions.hasMoreElements()) {
+ URL groovyActionURL = bundleGroovyActions.nextElement();
+ logger.debug("Found Groovy action at {}, loading... ", groovyActionURL.getPath());
+ removeGroovyActions(bundleContext, groovyActionURL);
+ }
+ }
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/GroovyActionsService.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/GroovyActionsService.java
new file mode 100644
index 0000000..99c6102
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/GroovyActionsService.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions.services;
+
+import org.apache.unomi.groovy.actions.GroovyAction;
+
+import java.io.File;
+
+/**
+ * A service to load groovy files and manage {@link GroovyAction}
+ */
+public interface GroovyActionsService {
+
+ /**
+ * Save a groovy action from a groovy file
+ * @param actionName actionName
+ * @param groovyScript script to save
+ */
+ void save(String actionName, String groovyScript);
+
+ /**
+ * Remove a groovy action
+ * @param id of the action to remove
+ */
+ void remove(String id);
+
+ /**
+ * Get a groovy action by an id
+ * @param id of the action to get
+ * @return Groovy action
+ */
+ GroovyAction getGroovyAction(String id);
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/impl/GroovyActionsServiceImpl.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/impl/GroovyActionsServiceImpl.java
new file mode 100644
index 0000000..7f63e57
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/services/impl/GroovyActionsServiceImpl.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.groovy.actions.services.impl;
+
+import groovy.lang.GroovyCodeSource;
+import groovy.util.GroovyScriptEngine;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.api.services.SchedulerService;
+import org.apache.unomi.groovy.actions.GroovyAction;
+import org.apache.unomi.groovy.actions.GroovyBundleResourceConnector;
+import org.apache.unomi.groovy.actions.annotations.Action;
+import org.apache.unomi.groovy.actions.services.GroovyActionsService;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Implementation of the GroovyActionService. Allows to create a groovy action from a groovy file
+ */
+public class GroovyActionsServiceImpl implements GroovyActionsService {
+
+ private BundleContext bundleContext;
+
+ private static final Logger logger = LoggerFactory.getLogger(GroovyActionsServiceImpl.class.getName());
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ @Reference
+ private DefinitionsService definitionsService;
+
+ @Reference
+ private PersistenceService persistenceService;
+
+ @Reference
+ private SchedulerService schedulerService;
+
+ private List<GroovyAction> groovyActions;
+
+ private Integer groovyActionsRefreshInterval = 1000;
+
+ public void setDefinitionsService(DefinitionsService definitionsService) {
+ this.definitionsService = definitionsService;
+ }
+
+ public void setPersistenceService(PersistenceService persistenceService) {
+ this.persistenceService = persistenceService;
+ }
+
+ public void setGroovyActionsRefreshInterval(Integer groovyActionsRefreshInterval) {
+ this.groovyActionsRefreshInterval = groovyActionsRefreshInterval;
+ }
+
+ public void setSchedulerService(SchedulerService schedulerService) {
+ this.schedulerService = schedulerService;
+ }
+
+ public void postConstruct() {
+ logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
+
+ initializeTimers();
+ logger.info("Groovy action service initialized.");
+ }
+
+ @Override
+ public void save(String actionName, String groovyScript) {
+ handleFile(actionName, groovyScript);
+ }
+
+ private void handleFile(String actionName, String groovyScript) {
+ Class classScript = buildClassScript(groovyScript, actionName);
+ saveActionType((Action) classScript.getAnnotation(Action.class));
+
+ saveScript(actionName, groovyScript);
+ logger.info("The script {} has been loaded.", actionName);
+ }
+
+ private void saveActionType(Action action) {
+ Metadata metadata = new Metadata(null, action.id(), action.name().equals("") ? action.id() : action.name(), action.description());
+ metadata.setHidden(action.hidden());
+ metadata.setReadOnly(true);
+ metadata.setSystemTags(new HashSet<>(Arrays.asList(action.systemTags())));
+ ActionType actionType = new ActionType(metadata);
+ actionType.setActionExecutor(action.actionExecutor());
+
+ actionType.setParameters(Stream.of(action.parameters())
+ .map(parameter -> new org.apache.unomi.api.Parameter(parameter.id(), parameter.type(), parameter.multivalued()))
+ .collect(Collectors.toList()));
+ definitionsService.setActionType(actionType);
+ }
+
+ @Override
+ public void remove(String id) {
+ removeActionType(id);
+ persistenceService.remove(id, GroovyAction.class);
+ }
+
+ @Override
+ public GroovyAction getGroovyAction(String id) {
+ return groovyActions.stream().filter(groovyAction -> groovyAction.getItemId().equals(id)).findFirst().orElse(null);
+ }
+
+ private void removeActionType(String actionId) {
+
+ GroovyAction groovyAction = getGroovyAction(actionId);
+ Class classScript = buildClassScript(groovyAction.getScript(), groovyAction.getItemId());
+ definitionsService.removeActionType(((Action) classScript.getAnnotation(Action.class)).id());
+ }
+
+ private Class buildClassScript(String groovyScript, String actionName) {
+ GroovyBundleResourceConnector bundleResourceConnector = new GroovyBundleResourceConnector(bundleContext);
+
+ GroovyCodeSource groovyCodeSource = new GroovyCodeSource(groovyScript, actionName, "/groovy/script");
+ GroovyScriptEngine engine = new GroovyScriptEngine(bundleResourceConnector,
+ bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader());
+ return engine.getGroovyClassLoader().parseClass(groovyCodeSource);
+ }
+
+ private void saveScript(String name, String script) {
+ GroovyAction groovyScript = new GroovyAction(name, script);
+ persistenceService.save(groovyScript);
+ }
+
+ private void refreshGroovyActions() {
+ groovyActions = persistenceService.getAllItems(GroovyAction.class);
+ }
+
+ private void initializeTimers() {
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ refreshGroovyActions();
+ }
+ };
+ schedulerService.getScheduleExecutorService().scheduleWithFixedDelay(task, 0, groovyActionsRefreshInterval, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyAction.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyAction.java
deleted file mode 100644
index 472af5d..0000000
--- a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyAction.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.services.actions.groovy;
-
-import org.osgi.framework.BundleContext;
-
-import java.net.URL;
-
-/**
- * This class represents a Groovy action, containing all the contextual variables needed to execute such an action.
- * It is not designed to be used outside of the Groovy Action dispatcher.
- */
-public class GroovyAction {
-
- private String name;
- private String path;
- private URL url;
- private BundleContext bundleContext;
-
- public GroovyAction(URL url, BundleContext bundleContext) {
- this.url = url;
- this.bundleContext = bundleContext;
- this.path = url.getPath();
- this.name = url.getPath().substring(url.getPath().lastIndexOf('/')+1);
- if (this.name.endsWith(".groovy")) {
- this.name = this.name.substring(0, this.name.length() - ".groovy".length());
- }
- }
-
- public String getName() {
- return name;
- }
-
- public String getPath() {
- return path;
- }
-
- public URL getUrl() {
- return url;
- }
-
- public BundleContext getBundleContext() {
- return bundleContext;
- }
-}
diff --git a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyActionDispatcher.java b/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyActionDispatcher.java
deleted file mode 100644
index 8b31bf5..0000000
--- a/extensions/groovy-actions/services/src/main/java/org/apache/unomi/services/actions/groovy/GroovyActionDispatcher.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.services.actions.groovy;
-
-import groovy.lang.Binding;
-import groovy.util.GroovyScriptEngine;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.actions.Action;
-import org.apache.unomi.api.actions.ActionDispatcher;
-import org.apache.unomi.metrics.MetricAdapter;
-import org.apache.unomi.metrics.MetricsService;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleEvent;
-import org.osgi.framework.SynchronousBundleListener;
-import org.osgi.framework.wiring.BundleWiring;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * An implementation of an ActionDispatcher for the Groovy language. It will use actionName and match them against
- * groovy script file names deployed in the same directory as the action descriptors (META-INF/cxs/actions)
- */
-public class GroovyActionDispatcher implements ActionDispatcher, SynchronousBundleListener {
-
- private static final Logger logger = LoggerFactory.getLogger(GroovyActionDispatcher.class.getName());
-
- private Map<String, GroovyAction> groovyActionsByName = new ConcurrentHashMap<>();
- private Map<BundleContext, List<GroovyAction>> groovyActionsByBundle = new ConcurrentHashMap<>();
- private MetricsService metricsService;
- private BundleContext bundleContext;
-
- public void setMetricsService(MetricsService metricsService) {
- this.metricsService = metricsService;
- }
-
- public void setBundleContext(BundleContext bundleContext) {
- this.bundleContext = bundleContext;
- }
-
- public String getPrefix() {
- return "groovy";
- }
-
- public Integer execute(Action action, Event event, String actionName) {
- GroovyAction groovyAction = groovyActionsByName.get(actionName);
- if (groovyAction == null) {
- logger.warn("Couldn't find a Groovy action with name {}, action will not execute !", actionName);
- } else {
- try {
- return new MetricAdapter<Integer>(metricsService, this.getClass().getName() + ".action.groovy." + actionName) {
- @Override
- public Integer execute(Object... args) throws Exception {
- Binding binding = new Binding();
- binding.setVariable("groovyAction", groovyAction);
- binding.setVariable("action", action);
- binding.setVariable("event", event);
- GroovyBundleResourceConnector bundleResourceConnector = new GroovyBundleResourceConnector(groovyAction.getBundleContext());
- GroovyScriptEngine engine = new GroovyScriptEngine(bundleResourceConnector, groovyAction.getBundleContext().getBundle().adapt(BundleWiring.class).getClassLoader());
- return (Integer) engine.run(groovyAction.getPath(), binding);
- }
- }.runWithTimer();
- } catch (Exception e) {
- logger.error("Error executing Groovy action with key=" + actionName, e);
- }
- }
-
- return null;
- }
-
- public void postConstruct() {
- logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
-
- for (Bundle bundle : bundleContext.getBundles()) {
- if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
- loadGroovyActions(bundleContext);
- }
- }
-
- bundleContext.addBundleListener(this);
-
- logger.info("Groovy Action Dispatcher initialized.");
- }
-
- public void preDestroy() {
- bundleContext.removeBundleListener(this);
- logger.info("Groovy Action Dispatcher shutdown.");
- }
-
- private void processBundleStartup(BundleContext bundleContext) {
- if (bundleContext == null) {
- return;
- }
- loadGroovyActions(bundleContext);
- }
-
- private void processBundleStop(BundleContext bundleContext) {
- if (bundleContext == null) {
- return;
- }
- unloadGroovyActions(bundleContext);
- }
-
- public void bundleChanged(BundleEvent event) {
- switch (event.getType()) {
- case BundleEvent.STARTED:
- processBundleStartup(event.getBundle().getBundleContext());
- break;
- case BundleEvent.STOPPING:
- processBundleStop(event.getBundle().getBundleContext());
- break;
- }
- }
-
-
- private void addGroovyAction(BundleContext bundleContext, URL groovyActionURL) {
- GroovyAction groovyAction = new GroovyAction(groovyActionURL, bundleContext);
- if (groovyActionsByName.containsKey(groovyAction.getName())) {
- logger.warn("Found an existing Groovy action with name {}. Will overwrite it!", groovyAction.getName());
- }
- groovyActionsByName.put(groovyAction.getName(), groovyAction);
- List<GroovyAction> bundleGroovyActions = groovyActionsByBundle.get(bundleContext);
- if (bundleGroovyActions == null) {
- bundleGroovyActions = new ArrayList<>();
- }
- bundleGroovyActions.add(groovyAction);
- groovyActionsByBundle.put(bundleContext, bundleGroovyActions);
- }
-
- private void removeGroovyActions(BundleContext bundleContext) {
- List<GroovyAction> bundleGroovyActions = groovyActionsByBundle.get(bundleContext);
- if (bundleGroovyActions == null) {
- return;
- }
- for (GroovyAction groovyAction : bundleGroovyActions) {
- groovyActionsByName.remove(groovyAction.getName());
- }
- groovyActionsByBundle.remove(bundleContext);
- }
-
- private void loadGroovyActions(BundleContext bundleContext) {
- Enumeration<URL> bundleGroovyActions = bundleContext.getBundle().findEntries("META-INF/cxs/actions", "*.groovy", true);
- if (bundleGroovyActions == null) {
- return;
- }
-
- while (bundleGroovyActions.hasMoreElements()) {
- URL groovyActionURL = bundleGroovyActions.nextElement();
- logger.debug("Found Groovy action at " + groovyActionURL + ", loading... ");
- addGroovyAction(bundleContext, groovyActionURL);
- }
- }
-
- private void unloadGroovyActions(BundleContext bundleContext) {
- removeGroovyActions(bundleContext);
- }
-
-}
diff --git a/extensions/groovy-actions/services/src/main/resources/META-INF/cxs/mappings/groovyAction.json b/extensions/groovy-actions/services/src/main/resources/META-INF/cxs/mappings/groovyAction.json
new file mode 100644
index 0000000..6cd8d3f
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/resources/META-INF/cxs/mappings/groovyAction.json
@@ -0,0 +1,28 @@
+{
+ "dynamic_templates": [
+ {
+ "all": {
+ "match": "*",
+ "match_mapping_type": "string",
+ "mapping": {
+ "type": "text",
+ "analyzer": "folding",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "name": {
+ "type": "text"
+ },
+ "script": {
+ "type": "text"
+ }
+ }
+}
\ No newline at end of file
diff --git a/extensions/groovy-actions/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/groovy-actions/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 17d1acd..48df30c 100644
--- a/extensions/groovy-actions/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/groovy-actions/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -17,19 +17,51 @@
-->
<blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.1.0"
xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
- <reference id="metricsService" interface="org.apache.unomi.metrics.MetricsService" />
+ <cm:property-placeholder persistent-id="org.apache.unomi.groovy.actions" update-strategy="reload">
+ <cm:default-properties>
+ <cm:property name="services.groovy.actions.refresh.interval" value="1000"/>
+ </cm:default-properties>
+ </cm:property-placeholder>
+
+ <reference id="metricsService" interface="org.apache.unomi.metrics.MetricsService"/>
+ <reference id="definitionsService" interface="org.apache.unomi.api.services.DefinitionsService"/>
+ <reference id="persistenceService" interface="org.apache.unomi.persistence.spi.PersistenceService"/>
+ <reference id="schedulerService" interface="org.apache.unomi.api.services.SchedulerService"/>
- <bean id="groovyActionDispatcherImpl" class="org.apache.unomi.services.actions.groovy.GroovyActionDispatcher"
- init-method="postConstruct" destroy-method="preDestroy">
- <property name="metricsService" ref="metricsService" />
+ <bean id="groovyActionsServiceImpl" class="org.apache.unomi.groovy.actions.services.impl.GroovyActionsServiceImpl"
+ init-method="postConstruct">
+ <property name="bundleContext" ref="blueprintBundleContext"/>
+ <property name="definitionsService" ref="definitionsService"/>
+ <property name="persistenceService" ref="persistenceService"/>
+ <property name="schedulerService" ref="schedulerService"/>
+ <property name="groovyActionsRefreshInterval" value="${services.groovy.actions.refresh.interval}"/>
+ </bean>
+ <service id="groovyActionsService" ref="groovyActionsServiceImpl"
+ interface="org.apache.unomi.groovy.actions.services.GroovyActionsService"/>
+
+ <bean id="groovyActionDispatcherImpl" class="org.apache.unomi.groovy.actions.GroovyActionDispatcher">
+ <property name="metricsService" ref="metricsService"/>
+ <property name="groovyActionsService" ref="groovyActionsServiceImpl"/>
<property name="bundleContext" ref="blueprintBundleContext"/>
</bean>
<service id="groovyActionDispatcher" ref="groovyActionDispatcherImpl">
<interfaces>
<value>org.apache.unomi.api.actions.ActionDispatcher</value>
+ </interfaces>
+ </service>
+
+ <bean id="groovyActionListenerImpl" class="org.apache.unomi.groovy.actions.listener.GroovyActionListener"
+ init-method="postConstruct" destroy-method="preDestroy">
+ <property name="persistenceService" ref="persistenceService"/>
+ <property name="bundleContext" ref="blueprintBundleContext"/>
+ <property name="groovyActionsService" ref="groovyActionsServiceImpl"/>
+ </bean>
+ <service id="groovyActionListener" ref="groovyActionListenerImpl">
+ <interfaces>
<value>org.osgi.framework.SynchronousBundleListener</value>
</interfaces>
</service>
diff --git a/extensions/groovy-actions/services/src/main/resources/org.apache.unomi.groovy.actions.cfg b/extensions/groovy-actions/services/src/main/resources/org.apache.unomi.groovy.actions.cfg
new file mode 100644
index 0000000..7e0154b
--- /dev/null
+++ b/extensions/groovy-actions/services/src/main/resources/org.apache.unomi.groovy.actions.cfg
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The interval in milliseconds to reload the groovy actions in memory
+services.groovy.actions.refresh.interval=${org.apache.unomi.groovy.actions.refresh.interval:-1000}
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index 4642094..d632872 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -51,6 +51,7 @@ import org.junit.runners.Suite.SuiteClasses;
ContextServletIT.class,
SecurityIT.class,
RuleServiceIT.class,
+ GroovyActionsServiceIT.class,
GraphQLEventIT.class,
GraphQLListIT.class,
GraphQLProfileIT.class,
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index 34c700b..88bfba0 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -158,6 +158,8 @@ public abstract class BaseIT {
"src/test/resources/testCopyPropertiesWithoutSystemTags.json")),
replaceConfigurationFile("data/tmp/testLoginEventCondition.json", new File(
"src/test/resources/testLoginEventCondition.json")),
+ replaceConfigurationFile("data/tmp/groovy/MyAction.groovy", new File(
+ "src/test/resources/groovy/MyAction.groovy")),
keepRuntimeFolder(),
// configureConsole().ignoreLocalConsole(),
logLevel(LogLevel.INFO),
diff --git a/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java
new file mode 100644
index 0000000..c81039d
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/GroovyActionsServiceIT.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package org.apache.unomi.itests;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.groovy.actions.GroovyAction;
+import org.apache.unomi.groovy.actions.services.GroovyActionsService;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class GroovyActionsServiceIT extends BaseIT {
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected GroovyActionsService groovyActionsService;
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected DefinitionsService definitionsService;
+
+ @Before
+ public void setUp() throws InterruptedException {
+ refreshPersistence();
+ }
+
+ @After
+ public void cleanUp() throws InterruptedException {
+ refreshPersistence();
+ }
+
+ private String loadGroovyAction(String pathname) throws IOException {
+ return IOUtils.toString(new FileInputStream(new File(pathname)));
+ }
+
+ @Test
+ public void testGroovyActionsService_saveActionAndTestSavedValues() throws IOException, InterruptedException {
+ groovyActionsService.save("MyAction", loadGroovyAction("data/tmp/groovy/MyAction.groovy"));
+
+ Thread.sleep(2000);
+
+ GroovyAction groovyAction = groovyActionsService.getGroovyAction("MyAction");
+
+ ActionType actionType = definitionsService.getActionType("scriptGroovyAction");
+
+ Assert.assertEquals("MyAction", groovyAction.getItemId());
+ Assert.assertEquals("MyAction", groovyAction.getName());
+ Assert.assertTrue(groovyAction.getScript().contains("A test Groovy"));
+
+ Assert.assertTrue(actionType.getMetadata().getId().contains("scriptGroovyAction"));
+ Assert.assertEquals(2, actionType.getMetadata().getSystemTags().size());
+ Assert.assertTrue(actionType.getMetadata().getSystemTags().contains("tag1"));
+ Assert.assertEquals(2, actionType.getParameters().size());
+ Assert.assertEquals("param1", actionType.getParameters().get(0).getId());
+
+ Assert.assertEquals("groovy:MyAction", actionType.getActionExecutor());
+ Assert.assertFalse(actionType.getMetadata().isHidden());
+ }
+
+ @Test
+ public void testGroovyActionsService_removeGroovyAction() throws IOException, InterruptedException {
+ groovyActionsService.save("MyAction", loadGroovyAction("data/tmp/groovy/MyAction.groovy"));
+
+ Thread.sleep(2000);
+
+ GroovyAction groovyAction = groovyActionsService.getGroovyAction("MyAction");
+
+ Assert.assertNotNull(groovyAction);
+
+ groovyActionsService.remove("MyAction");
+
+ Thread.sleep(2000);
+
+ groovyAction = groovyActionsService.getGroovyAction("MyAction");
+
+ Assert.assertNull(groovyAction);
+
+ ActionType actionType = definitionsService.getActionType("scriptGroovyAction");
+
+ Assert.assertNull(actionType);
+
+ }
+}
diff --git a/itests/src/test/resources/groovy/MyAction.groovy b/itests/src/test/resources/groovy/MyAction.groovy
new file mode 100644
index 0000000..8cfd1ee
--- /dev/null
+++ b/itests/src/test/resources/groovy/MyAction.groovy
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+
+import org.apache.unomi.api.services.EventService
+import org.apache.unomi.groovy.actions.annotations.Action
+import org.apache.unomi.groovy.actions.annotations.Parameter
+
+import java.util.logging.Logger
+
+@Action(id = "scriptGroovyAction",
+ description = "A test Groovy Action",
+ actionExecutor = "groovy:MyAction",
+ hidden = false,
+ systemTags = ["tag1", "tag2"],
+ parameters = [@Parameter(id = "param1", type = "string", multivalued = false), @Parameter(id = "param2", type = "string", multivalued =
+ false)])
+class MyAction {
+ Logger logger = Logger.getdLogger("")
+
+ String execute(action, event) {
+ logger.info("Groovy action for event type: " + event.getEventType())
+ EventService.NO_CHANGE
+ }
+}
diff --git a/package/pom.xml b/package/pom.xml
index 862f231..7246304 100644
--- a/package/pom.xml
+++ b/package/pom.xml
@@ -117,6 +117,15 @@
<dependency>
<groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-groovy-actions</artifactId>
+ <version>${project.version}</version>
+ <classifier>features</classifier>
+ <type>xml</type>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
<artifactId>cdp-graphql-feature</artifactId>
<version>${project.version}</version>
<classifier>features</classifier>
@@ -354,6 +363,7 @@
<feature>unomi-kar</feature>
<feature>unomi-router-karaf-feature</feature>
<feature>unomi-web-tracker-karaf-kar</feature>
+ <feature>unomi-groovy-actions</feature>
<feature>cdp-graphql-feature</feature>
<feature>unomi-rest-ui</feature>
<feature>unomi-graphql-ui</feature>
diff --git a/package/src/main/resources/etc/custom.system.properties b/package/src/main/resources/etc/custom.system.properties
index 19e4824..238808d 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -278,6 +278,13 @@ org.apache.unomi.geonames.forceImport=${env:UNOMI_GEONAMES_FORCEIMPORT:-false}
# The interval in milliseconds to use to check if the database is ready to be loaded
org.apache.unomi.geonames.refresh.interval=${env:UNOMI_GEONAMES_REFRESH_INTERVAL:-5000}
+
+#######################################################################################################################
+## Groovy action settings ##
+#######################################################################################################################
+# The interval in milliseconds to reload the groovy actions in memory
+org.apache.unomi.groovy.actions.refresh.interval=${env:UNOMI_GROOVY_ACTION_REFRESH_INTERVAL:-1000}
+
#######################################################################################################################
## MaxMind IP Database settings ##
#######################################################################################################################