Skip to content

Commit

Permalink
Introduce a profile for the generic SCRIPT transformation (openhab#3292)
Browse files Browse the repository at this point in the history
* Introduce a generic script profile

Signed-off-by: Jan N. Klug <[email protected]>
  • Loading branch information
J-N-K authored Mar 20, 2023
1 parent cb38d19 commit 7bad9ba
Show file tree
Hide file tree
Showing 9 changed files with 714 additions and 35 deletions.
6 changes: 6 additions & 0 deletions bundles/org.openhab.core.automation.module.script/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<artifactId>org.openhab.core.transform</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
*/
package org.openhab.core.automation.module.script;

import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -20,6 +24,7 @@
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.script.Compilable;
import javax.script.CompiledScript;
Expand All @@ -29,8 +34,12 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
import org.openhab.core.automation.module.script.profile.ScriptProfile;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.ConfigOptionProvider;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.transform.Transformation;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationRegistry;
Expand All @@ -39,6 +48,8 @@
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -48,10 +59,13 @@
*
* @author Jan N. Klug - Initial contribution
*/
@Component(service = TransformationService.class, property = { "openhab.transform=SCRIPT" })
@Component(service = { TransformationService.class, ScriptTransformationService.class,
ConfigOptionProvider.class }, property = { "openhab.transform=SCRIPT" })
@NonNullByDefault
public class ScriptTransformationService implements TransformationService, RegistryChangeListener<Transformation> {
public class ScriptTransformationService
implements TransformationService, RegistryChangeListener<Transformation>, ConfigOptionProvider {
public static final String OPENHAB_TRANSFORMATION_SCRIPT = "openhab-transformation-script-";
private static final String PROFILE_CONFIG_URI = "profile:transform:SCRIPT";
public static final String SUPPORTED_CONFIGURATION_TYPE = "script";

private static final Pattern SCRIPT_CONFIG_PATTERN = Pattern
Expand All @@ -65,6 +79,8 @@ public class ScriptTransformationService implements TransformationService, Regis
private final Map<String, ScriptRecord> scriptCache = new ConcurrentHashMap<>();

private final TransformationRegistry transformationRegistry;
private final Map<String, String> supportedScriptTypes = new ConcurrentHashMap<>();

private final ScriptEngineManager scriptEngineManager;

@Activate
Expand Down Expand Up @@ -160,10 +176,10 @@ public void deactivate() {

// compile the script here _after_ setting context attributes, so that the script engine
// can bind the attributes as variables during compilation. This primarily affects jruby.
if (compiledScript == null && scriptEngineContainer.getScriptEngine() instanceof Compilable) {
if (compiledScript == null
&& scriptEngineContainer.getScriptEngine()instanceof Compilable scriptEngine) {
// no compiled script available but compiling is supported
compiledScript = ((Compilable) scriptEngineContainer.getScriptEngine())
.compile(scriptRecord.script);
compiledScript = scriptEngine.compile(scriptRecord.script);
scriptRecord.compiledScript = compiledScript;
}

Expand Down Expand Up @@ -211,14 +227,13 @@ private void disposeScriptRecord(ScriptRecord scriptRecord) {
}

private void disposeScriptEngine(ScriptEngine scriptEngine) {
if (scriptEngine instanceof AutoCloseable) {
if (scriptEngine instanceof AutoCloseable closableScriptEngine) {
// we cannot not use ScheduledExecutorService.execute here as it might execute the task in the calling
// thread (calling ScriptEngine.close in the same thread may result in a deadlock if the ScriptEngine
// tries to Thread.join)
scheduler.schedule(() -> {
AutoCloseable closeable = (AutoCloseable) scriptEngine;
try {
closeable.close();
closableScriptEngine.close();
} catch (Exception e) {
logger.error("Error while closing script engine", e);
}
Expand All @@ -228,6 +243,38 @@ private void disposeScriptEngine(ScriptEngine scriptEngine) {
}
}

@Override
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
@Nullable Locale locale) {
if (PROFILE_CONFIG_URI.equals(uri.toString())) {
if (ScriptProfile.CONFIG_TO_HANDLER_SCRIPT.equals(param)
|| ScriptProfile.CONFIG_TO_ITEM_SCRIPT.equals(param)) {
return transformationRegistry.getTransformations(List.of(SUPPORTED_CONFIGURATION_TYPE)).stream()
.map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
}
if (ScriptProfile.CONFIG_SCRIPT_LANGUAGE.equals(param)) {
return supportedScriptTypes.entrySet().stream().map(e -> new ParameterOption(e.getKey(), e.getValue()))
.collect(Collectors.toList());
}
}
return null;
}

/**
* As {@link ScriptEngineFactory}s are added/removed, this method will cache all available script types
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
Map.Entry<String, String> parameterOption = ScriptEngineFactoryHelper.getParameterOption(engineFactory);
if (parameterOption != null) {
supportedScriptTypes.put(parameterOption.getKey(), parameterOption.getValue());
}
}

public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
supportedScriptTypes.remove(ScriptEngineFactoryHelper.getPreferredMimeType(engineFactory));
}

private static class ScriptRecord {
public String script = "";
public @Nullable ScriptEngineContainer scriptEngineContainer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.module.script.internal;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.script.ScriptEngine;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link ScriptEngineFactoryHelper} contains helper methods for handling script engines
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ScriptEngineFactoryHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(ScriptEngineFactoryHelper.class);

private ScriptEngineFactoryHelper() {
// prevent instantiation of static utility class
}

public static Map.@Nullable Entry<String, String> getParameterOption(ScriptEngineFactory engineFactory) {
List<String> scriptTypes = engineFactory.getScriptTypes();
if (!scriptTypes.isEmpty()) {
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptTypes.get(0));
if (scriptEngine != null) {
Map.Entry<String, String> parameterOption = Map.entry(getPreferredMimeType(engineFactory),
getLanguageName(scriptEngine.getFactory()));
LOGGER.trace("ParameterOptions: {}", parameterOption);
return parameterOption;
} else {
LOGGER.trace("setScriptEngineFactory: engine was null");
}
} else {
LOGGER.trace("addScriptEngineFactory: scriptTypes was empty");
}

return null;
}

public static String getPreferredMimeType(ScriptEngineFactory factory) {
List<String> mimeTypes = new ArrayList<>(factory.getScriptTypes());
mimeTypes.removeIf(mimeType -> !mimeType.contains("application") || "application/python".equals(mimeType));
return mimeTypes.isEmpty() ? factory.getScriptTypes().get(0) : mimeTypes.get(0);
}

public static String getLanguageName(javax.script.ScriptEngineFactory factory) {
return String.format("%s (%s)",
factory.getLanguageName().substring(0, 1).toUpperCase() + factory.getLanguageName().substring(1),
factory.getLanguageVersion());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.Visibility;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
import org.openhab.core.automation.module.script.internal.handler.AbstractScriptModuleHandler;
import org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler;
import org.openhab.core.automation.module.script.internal.handler.ScriptConditionHandler;
Expand Down Expand Up @@ -146,21 +147,14 @@ private void notifyModuleTypesRemoved() {
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
List<String> scriptTypes = engineFactory.getScriptTypes();
if (!scriptTypes.isEmpty()) {
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptTypes.get(0));
if (scriptEngine != null) {
boolean notifyListeners = parameterOptions.isEmpty();
parameterOptions.put(getPreferredMimeType(engineFactory), getLanguageName(scriptEngine.getFactory()));
logger.trace("ParameterOptions: {}", parameterOptions);
if (notifyListeners) {
notifyModuleTypesAdded();
}
} else {
logger.trace("setScriptEngineFactory: engine was null");
Map.Entry<String, String> parameterOption = ScriptEngineFactoryHelper.getParameterOption(engineFactory);
if (parameterOption != null) {
boolean notifyListeners = parameterOptions.isEmpty();
parameterOptions.put(parameterOption.getKey(), parameterOption.getValue());
logger.trace("ParameterOptions: {}", parameterOptions);
if (notifyListeners) {
notifyModuleTypesAdded();
}
} else {
logger.trace("addScriptEngineFactory: scriptTypes was empty");
}
}

Expand All @@ -169,7 +163,7 @@ public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
if (!scriptTypes.isEmpty()) {
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptTypes.get(0));
if (scriptEngine != null) {
parameterOptions.remove(getPreferredMimeType(engineFactory));
parameterOptions.remove(ScriptEngineFactoryHelper.getPreferredMimeType(engineFactory));
logger.trace("ParameterOptions: {}", parameterOptions);
if (parameterOptions.isEmpty()) {
notifyModuleTypesRemoved();
Expand All @@ -181,16 +175,4 @@ public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
logger.trace("unsetScriptEngineFactory: scriptTypes was empty");
}
}

private String getPreferredMimeType(ScriptEngineFactory factory) {
List<String> mimeTypes = new ArrayList<>(factory.getScriptTypes());
mimeTypes.removeIf(mimeType -> !mimeType.contains("application") || "application/python".equals(mimeType));
return mimeTypes.isEmpty() ? factory.getScriptTypes().get(0) : mimeTypes.get(0);
}

private String getLanguageName(javax.script.ScriptEngineFactory factory) {
return String.format("%s (%s)",
factory.getLanguageName().substring(0, 1).toUpperCase() + factory.getLanguageName().substring(1),
factory.getLanguageVersion());
}
}
Loading

0 comments on commit 7bad9ba

Please sign in to comment.