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                                                                                      ##
 #######################################################################################################################