Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,36 @@
*/
package org.apache.unomi.groovy.actions;

import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.apache.unomi.api.Event;
import org.apache.unomi.api.actions.Action;
import org.apache.unomi.api.actions.ActionDispatcher;
import org.apache.unomi.api.services.DefinitionsService;
import org.apache.unomi.groovy.actions.services.GroovyActionsService;
import org.apache.unomi.metrics.MetricAdapter;
import org.apache.unomi.metrics.MetricsService;
import org.apache.unomi.services.actions.ActionExecutorDispatcher;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
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.
* High-performance ActionDispatcher for pre-compiled Groovy scripts.
* Executes scripts without GroovyShell overhead using isolated instances.
*/
@Component(service = ActionDispatcher.class)
public class GroovyActionDispatcher implements ActionDispatcher {

private static final Logger LOGGER = LoggerFactory.getLogger(GroovyActionDispatcher.class.getName());
private static final Logger GROOVY_ACTION_LOGGER = LoggerFactory.getLogger("GroovyAction");

private static final String GROOVY_PREFIX = "groovy";

private MetricsService metricsService;
private GroovyActionsService groovyActionsService;
private DefinitionsService definitionsService;
private ActionExecutorDispatcher actionExecutorDispatcher;

@Reference
public void setMetricsService(MetricsService metricsService) {
Expand All @@ -54,30 +57,52 @@ public void setGroovyActionsService(GroovyActionsService groovyActionsService) {
this.groovyActionsService = groovyActionsService;
}

@Reference
public void setDefinitionsService(DefinitionsService definitionsService) {
this.definitionsService = definitionsService;
}

@Reference
public void setActionExecutorDispatcher(ActionExecutorDispatcher actionExecutorDispatcher) {
this.actionExecutorDispatcher = actionExecutorDispatcher;
}

public String getPrefix() {
return GROOVY_PREFIX;
}

public Integer execute(Action action, Event event, String actionName) {
GroovyCodeSource groovyCodeSource = groovyActionsService.getGroovyCodeSource(actionName);
if (groovyCodeSource == null) {
LOGGER.warn("Couldn't find a Groovy action with name {}, action will not execute !", actionName);
} else {
GroovyShell groovyShell = groovyActionsService.getGroovyShell();
groovyShell.setVariable("action", action);
groovyShell.setVariable("event", event);
Script script = groovyShell.parse(groovyCodeSource);
try {
return new MetricAdapter<Integer>(metricsService, this.getClass().getName() + ".action.groovy." + actionName) {
@Override
public Integer execute(Object... args) throws Exception {
return (Integer) script.invokeMethod("execute", null);
}
}.runWithTimer();
} catch (Exception e) {
LOGGER.error("Error executing Groovy action with key={}", actionName, e);
}
Class<? extends Script> scriptClass = groovyActionsService.getCompiledScript(actionName);
if (scriptClass == null) {
LOGGER.warn("Couldn't find a Groovy action with name {}, action will not execute!", actionName);
return 0;
}

try {
Script script = scriptClass.getDeclaredConstructor().newInstance();
setScriptVariables(script, action, event);

return new MetricAdapter<Integer>(metricsService, this.getClass().getName() + ".action.groovy." + actionName) {
@Override
public Integer execute(Object... args) throws Exception {
return (Integer) script.invokeMethod("execute", null);
}
}.runWithTimer();

} catch (Exception e) {
LOGGER.error("Error executing Groovy action with key={}", actionName, e);
}
return 0;
}

/**
* Sets required variables on script instance.
*/
private void setScriptVariables(Script script, Action action, Event event) {
script.setProperty("action", action);
script.setProperty("event", event);
script.setProperty("actionExecutorDispatcher", actionExecutorDispatcher);
script.setProperty("definitionsService", definitionsService);
script.setProperty("logger", GROOVY_ACTION_LOGGER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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.Script;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
* Metadata container for compiled Groovy scripts with hash-based change detection.
* <p>
* This class encapsulates all metadata associated with a compiled Groovy script,
* including content hash for efficient change detection and the compiled class
* for direct execution without recompilation.
* </p>
*
* <p>
* Thread Safety: This class is immutable and thread-safe. All fields are final
* and the class provides no methods to modify its state after construction.
* </p>
*
* @since 2.7.0
*/
public final class ScriptMetadata {
private final String actionName;
private final String scriptContent;
private final String contentHash;
private final long creationTime;
private final Class<? extends Script> compiledClass;

/**
* Constructs a new ScriptMetadata instance.
*
* @param actionName the unique name/identifier of the action
* @param scriptContent the raw Groovy script content
* @param compiledClass the compiled Groovy script class
* @throws IllegalArgumentException if any parameter is null
*/
public ScriptMetadata(String actionName, String scriptContent, Class<? extends Script> compiledClass) {
if (actionName == null) {
throw new IllegalArgumentException("Action name cannot be null");
}
if (scriptContent == null) {
throw new IllegalArgumentException("Script content cannot be null");
}
if (compiledClass == null) {
throw new IllegalArgumentException("Compiled class cannot be null");
}

this.actionName = actionName;
this.scriptContent = scriptContent;
this.contentHash = calculateHash(scriptContent);
this.creationTime = System.currentTimeMillis();
this.compiledClass = compiledClass;
}

/**
* Calculates SHA-256 hash of the given content.
*
* @param content the content to hash
* @return Base64 encoded SHA-256 hash
* @throws RuntimeException if SHA-256 algorithm is not available
*/
private String calculateHash(String content) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not available", e);
}
}

/**
* Determines if the script content has changed compared to new content.
* <p>
* This method uses SHA-256 hash comparison for efficient change detection
* without storing or comparing the full script content.
* </p>
*
* @param newContent the new script content to compare against
* @return {@code true} if content has changed, {@code false} if unchanged
* @throws IllegalArgumentException if newContent is null
*/
public boolean hasChanged(String newContent) {
if (newContent == null) {
throw new IllegalArgumentException("New content cannot be null");
}
return !contentHash.equals(calculateHash(newContent));
}

/**
* Returns the action name/identifier.
*
* @return the action name, never null
*/
public String getActionName() {
return actionName;
}

/**
* Returns the original script content.
*
* @return the script content, never null
*/
public String getScriptContent() {
return scriptContent;
}

/**
* Returns the SHA-256 hash of the script content.
*
* @return Base64 encoded content hash, never null
*/
public String getContentHash() {
return contentHash;
}

/**
* Returns the timestamp when this metadata was created.
*
* @return creation timestamp in milliseconds since epoch
*/
public long getCreationTime() {
return creationTime;
}

/**
* Returns the compiled Groovy script class.
* <p>
* This class can be used to create new script instances for execution
* without requiring recompilation.
* </p>
*
* @return the compiled script class, never null
*/
public Class<? extends Script> getCompiledClass() {
return compiledClass;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,92 @@
*/
package org.apache.unomi.groovy.actions.services;

import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyShell;
import groovy.util.GroovyScriptEngine;
import groovy.lang.Script;
import org.apache.unomi.groovy.actions.GroovyAction;
import org.apache.unomi.groovy.actions.ScriptMetadata;


/**
* A service to load groovy files and manage {@link GroovyAction}
* Service interface for managing Groovy action scripts.
* <p>
* This service provides functionality to load, compile, cache, and execute
* Groovy scripts as actions within the Apache Unomi framework. It implements
* optimized compilation and caching strategies to achieve high performance.
* </p>
*
* <p>
* Key features:
* <ul>
* <li>Pre-compilation of scripts at startup</li>
* <li>Hash-based change detection for selective recompilation</li>
* <li>Thread-safe compilation and execution</li>
* <li>Unified caching architecture for compiled scripts</li>
* </ul>
* </p>
*
* <p>
* Thread Safety: Implementations must be thread-safe as this service
* is accessed concurrently during script execution.
* </p>
*
* @see GroovyAction
* @see ScriptMetadata
* @since 2.7.0
*/
public interface GroovyActionsService {

/**
* Save a groovy action from a groovy file
* Saves a Groovy action script with compilation and validation.
* <p>
* This method compiles the script, validates it has the required
* annotations, persists it, and updates the internal cache.
* If the script content hasn't changed, recompilation is skipped.
* </p>
*
* @param actionName actionName
* @param groovyScript script to save
* @param actionName the unique identifier for the action
* @param groovyScript the Groovy script source code
* @throws IllegalArgumentException if actionName or groovyScript is null
* @throws RuntimeException if compilation or persistence fails
*/
void save(String actionName, String groovyScript);

/**
* Remove a groovy action
* Removes a Groovy action and all associated metadata.
* <p>
* This method removes the action from both the cache and persistent storage,
* and cleans up any registered action types in the definitions service.
* </p>
*
* @param id of the action to remove
* @param actionName the unique identifier of the action to remove
* @throws IllegalArgumentException if id is null
*/
void remove(String id);
void remove(String actionName);

/**
* Get a groovy code source object by an id
* Retrieves a pre-compiled script class from cache.
* <p>
* This is the preferred method for script execution as it returns
* pre-compiled classes without any compilation overhead. Returns
* {@code null} if the script is not found in the cache.
* </p>
*
* @param id of the action to get
* @return Groovy code source
* @param actionName the unique identifier of the action
* @return the compiled script class, or {@code null} if not found in cache
* @throws IllegalArgumentException if id is null
*/
GroovyCodeSource getGroovyCodeSource(String id);
Class<? extends Script> getCompiledScript(String actionName);

/**
* Get an instantiated groovy shell object
* Retrieves script metadata for monitoring and change detection.
* <p>
* The returned metadata includes content hash, compilation timestamp,
* and the compiled class reference. This is useful for monitoring
* tools and debugging.
* </p>
*
* @return GroovyShell
* @param actionName the unique identifier of the action
* @return the script metadata, or {@code null} if not found
* @throws IllegalArgumentException if actionName is null
*/
GroovyShell getGroovyShell();
ScriptMetadata getScriptMetadata(String actionName);
}
Loading
Loading