Add dynamic scripting-language transformation service (#3487)

* Add dynamic scripting language transformation service

This replaced SCRIPT transformation with one specific to each language

e.g. JS, RB, GROOVY, etc.

Co-authored-by: Jan N. Klug <github@klug.nrw>
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
pull/3548/head
jimtng 2023-04-13 05:56:06 +10:00 committed by GitHub
parent fa37a467ac
commit fbaf992666
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 306 additions and 210 deletions

View File

@ -12,8 +12,13 @@
*/
package org.openhab.core.automation.module.script;
import static org.openhab.core.automation.module.script.profile.ScriptProfileFactory.PROFILE_CONFIG_URI_PREFIX;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -34,11 +39,15 @@ import javax.script.ScriptException;
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.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionBuilder;
import org.openhab.core.config.core.ConfigDescriptionProvider;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigOptionProvider;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.transform.Transformation;
import org.openhab.core.transform.TransformationException;
@ -48,8 +57,6 @@ import org.osgi.service.component.annotations.Activate;
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;
@ -59,35 +66,50 @@ import org.slf4j.LoggerFactory;
*
* @author Jan N. Klug - Initial contribution
*/
@Component(service = { TransformationService.class, ScriptTransformationService.class,
ConfigOptionProvider.class }, property = { "openhab.transform=SCRIPT" })
@NonNullByDefault
public class ScriptTransformationService
implements TransformationService, RegistryChangeListener<Transformation>, ConfigOptionProvider {
@Component(factory = "org.openhab.core.automation.module.script.transformation.factory", service = {
TransformationService.class, ScriptTransformationService.class, ConfigOptionProvider.class,
ConfigDescriptionProvider.class })
public class ScriptTransformationService implements TransformationService, ConfigOptionProvider,
ConfigDescriptionProvider, RegistryChangeListener<Transformation> {
public static final String SCRIPT_TYPE_PROPERTY_NAME = "openhab.transform.script.scriptType";
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
.compile("(?<scriptType>.*?):(?<scriptUid>.*?)(\\?(?<params>.*?))?");
private static final URI CONFIG_DESCRIPTION_TEMPLATE_URI = URI.create(PROFILE_CONFIG_URI_PREFIX + "SCRIPT");
private static final Pattern INLINE_SCRIPT_CONFIG_PATTERN = Pattern.compile("\\|(?<inlineScript>.+)");
private static final Pattern SCRIPT_CONFIG_PATTERN = Pattern.compile("(?<scriptUid>.+?)(\\?(?<params>.*?))?");
private final Logger logger = LoggerFactory.getLogger(ScriptTransformationService.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
private final String scriptType;
private final URI profileConfigUri;
private final Map<String, ScriptRecord> scriptCache = new ConcurrentHashMap<>();
private final TransformationRegistry transformationRegistry;
private final Map<String, String> supportedScriptTypes = new ConcurrentHashMap<>();
private final ScriptEngineManager scriptEngineManager;
private final ConfigDescriptionRegistry configDescRegistry;
@Activate
public ScriptTransformationService(@Reference TransformationRegistry transformationRegistry,
@Reference ScriptEngineManager scriptEngineManager) {
@Reference ConfigDescriptionRegistry configDescRegistry, @Reference ScriptEngineManager scriptEngineManager,
Map<String, Object> config) {
String scriptType = ConfigParser.valueAs(config.get(SCRIPT_TYPE_PROPERTY_NAME), String.class);
if (scriptType == null) {
throw new IllegalStateException(
"'" + SCRIPT_TYPE_PROPERTY_NAME + "' must not be null in service configuration");
}
this.transformationRegistry = transformationRegistry;
this.configDescRegistry = configDescRegistry;
this.scriptEngineManager = scriptEngineManager;
this.scriptType = scriptType;
this.profileConfigUri = URI.create(PROFILE_CONFIG_URI_PREFIX + scriptType.toUpperCase());
transformationRegistry.addRegistryChangeListener(this);
}
@ -101,28 +123,34 @@ public class ScriptTransformationService
@Override
public @Nullable String transform(String function, String source) throws TransformationException {
Matcher configMatcher = SCRIPT_CONFIG_PATTERN.matcher(function);
if (!configMatcher.matches()) {
throw new TransformationException("Script Type must be prepended to transformation UID.");
String scriptUid;
String inlineScript = null;
String params = null;
Matcher configMatcher = INLINE_SCRIPT_CONFIG_PATTERN.matcher(function);
if (configMatcher.matches()) {
inlineScript = configMatcher.group("inlineScript");
// prefix with | to avoid clashing with a real filename
scriptUid = "|" + Integer.toString(inlineScript.hashCode());
} else {
configMatcher = SCRIPT_CONFIG_PATTERN.matcher(function);
if (!configMatcher.matches()) {
throw new TransformationException("Invalid syntax for the script transformation: '" + function + "'");
}
scriptUid = configMatcher.group("scriptUid");
params = configMatcher.group("params");
}
String scriptType = configMatcher.group("scriptType");
String scriptUid = configMatcher.group("scriptUid");
ScriptRecord scriptRecord = scriptCache.computeIfAbsent(scriptUid, k -> new ScriptRecord());
scriptRecord.lock.lock();
try {
if (scriptRecord.script.isBlank()) {
if (scriptUid.startsWith("|")) {
// inline script -> strip inline-identifier
scriptRecord.script = scriptUid.substring(1);
if (inlineScript != null) {
scriptRecord.script = inlineScript;
} else {
// get script from transformation registry
Transformation transformation = transformationRegistry.get(scriptUid);
if (transformation != null) {
if (!SUPPORTED_CONFIGURATION_TYPE.equals(transformation.getType())) {
throw new TransformationException("Configuration does not have correct type 'script' but '"
+ transformation.getType() + "'.");
}
scriptRecord.script = transformation.getConfiguration().getOrDefault(Transformation.FUNCTION,
"");
}
@ -160,7 +188,6 @@ public class ScriptTransformationService
ScriptContext executionContext = engine.getContext();
executionContext.setAttribute("input", source, ScriptContext.ENGINE_SCOPE);
String params = configMatcher.group("params");
if (params != null) {
for (String param : params.split("&")) {
String[] splitString = param.split("=");
@ -169,7 +196,9 @@ public class ScriptTransformationService
"Parameter '{}' does not consist of two parts for configuration UID {}, skipping.",
param, scriptUid);
} else {
executionContext.setAttribute(splitString[0], splitString[1], ScriptContext.ENGINE_SCOPE);
param = URLDecoder.decode(splitString[0], StandardCharsets.UTF_8);
String value = URLDecoder.decode(splitString[1], StandardCharsets.UTF_8);
executionContext.setAttribute(param, value, ScriptContext.ENGINE_SCOPE);
}
}
}
@ -208,6 +237,44 @@ public class ScriptTransformationService
clearCache(element.getUID());
}
@Override
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
@Nullable Locale locale) {
if (!uri.equals(profileConfigUri)) {
return null;
}
if (ScriptProfile.CONFIG_TO_HANDLER_SCRIPT.equals(param) || ScriptProfile.CONFIG_TO_ITEM_SCRIPT.equals(param)) {
return transformationRegistry.getTransformations(List.of(scriptType.toLowerCase())).stream()
.map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
}
return null;
}
@Override
public Collection<ConfigDescription> getConfigDescriptions(@Nullable Locale locale) {
ConfigDescription configDescription = getConfigDescription(profileConfigUri, locale);
if (configDescription != null) {
return List.of(configDescription);
}
return Collections.emptyList();
}
@Override
public @Nullable ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale) {
if (!uri.equals(profileConfigUri)) {
return null;
}
ConfigDescription template = configDescRegistry.getConfigDescription(CONFIG_DESCRIPTION_TEMPLATE_URI, locale);
if (template == null) {
return null;
}
return ConfigDescriptionBuilder.create(uri).withParameters(template.getParameters())
.withParameterGroups(template.getParameterGroups()).build();
}
private void clearCache(String uid) {
ScriptRecord scriptRecord = scriptCache.remove(uid);
if (scriptRecord != null) {
@ -243,38 +310,6 @@ public class ScriptTransformationService
}
}
@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;

View File

@ -0,0 +1,97 @@
/**
* 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;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import javax.script.ScriptEngine;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
import org.openhab.core.transform.TransformationService;
import org.osgi.service.component.ComponentFactory;
import org.osgi.service.component.ComponentInstance;
import org.osgi.service.component.annotations.Activate;
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;
/**
* The {@link ScriptTransformationServiceFactory} registers a {@link ScriptTransformationService}
* for each newly added script engine.
*
* @author Jimmy Tanagra - Initial contribution
*/
@Component(immediate = true, service = { ScriptTransformationServiceFactory.class })
@NonNullByDefault
public class ScriptTransformationServiceFactory {
private final ComponentFactory<ScriptTransformationService> scriptTransformationFactory;
private final Map<ScriptEngineFactory, ComponentInstance<ScriptTransformationService>> scriptTransformations = new ConcurrentHashMap<>();
@Activate
public ScriptTransformationServiceFactory(
@Reference(target = "(component.factory=org.openhab.core.automation.module.script.transformation.factory)") ComponentFactory<ScriptTransformationService> factory) {
this.scriptTransformationFactory = factory;
}
@Deactivate
public void deactivate() {
scriptTransformations.values().forEach(this::unregisterService);
scriptTransformations.clear();
}
/**
* As {@link ScriptEngineFactory}s are added/removed, this method will cache all available script types
* and registers a transformation service for the script engine.
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
Optional<String> scriptType = ScriptEngineFactoryHelper.getPreferredExtension(engineFactory);
if (scriptType.isEmpty()) {
return;
}
scriptTransformations.computeIfAbsent(engineFactory, factory -> {
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptType.get());
if (scriptEngine == null) {
return null;
}
String languageName = ScriptEngineFactoryHelper.getLanguageName(scriptEngine.getFactory());
Dictionary<String, Object> properties = new Hashtable<>();
properties.put(TransformationService.SERVICE_PROPERTY_NAME, scriptType.get().toUpperCase());
properties.put(TransformationService.SERVICE_PROPERTY_LABEL, "SCRIPT " + languageName);
properties.put(ScriptTransformationService.SCRIPT_TYPE_PROPERTY_NAME, scriptType.get());
return scriptTransformationFactory.newInstance(properties);
});
}
public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
ComponentInstance<ScriptTransformationService> toBeUnregistered = scriptTransformations.remove(engineFactory);
if (toBeUnregistered != null) {
unregisterService(toBeUnregistered);
}
}
private void unregisterService(ComponentInstance<ScriptTransformationService> instance) {
instance.getInstance().deactivate();
instance.dispose();
}
}

View File

@ -13,8 +13,10 @@
package org.openhab.core.automation.module.script.internal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.script.ScriptEngine;
@ -67,4 +69,10 @@ public class ScriptEngineFactoryHelper {
factory.getLanguageName().substring(0, 1).toUpperCase() + factory.getLanguageName().substring(1),
factory.getLanguageVersion());
}
public static Optional<String> getPreferredExtension(ScriptEngineFactory factory) {
// return an Optional because GenericScriptEngineFactory has no scriptTypes
return factory.getScriptTypes().stream().filter(type -> !type.contains("/"))
.min(Comparator.comparing(String::length));
}
}

View File

@ -41,7 +41,6 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class ScriptProfile implements StateProfile {
public static final String CONFIG_SCRIPT_LANGUAGE = "scriptLanguage";
public static final String CONFIG_TO_ITEM_SCRIPT = "toItemScript";
public static final String CONFIG_TO_HANDLER_SCRIPT = "toHandlerScript";
@ -54,14 +53,15 @@ public class ScriptProfile implements StateProfile {
private final List<Class<? extends Command>> acceptedCommandTypes;
private final List<Class<? extends Command>> handlerAcceptedCommandTypes;
private final String scriptLanguage;
private final String toItemScript;
private final String toHandlerScript;
private final ProfileTypeUID profileTypeUID;
private final boolean isConfigured;
public ScriptProfile(ProfileCallback callback, ProfileContext profileContext,
public ScriptProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, ProfileContext profileContext,
TransformationService transformationService) {
this.profileTypeUID = profileTypeUID;
this.callback = callback;
this.transformationService = transformationService;
@ -69,19 +69,11 @@ public class ScriptProfile implements StateProfile {
this.acceptedDataTypes = profileContext.getAcceptedDataTypes();
this.handlerAcceptedCommandTypes = profileContext.getHandlerAcceptedCommandTypes();
this.scriptLanguage = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_SCRIPT_LANGUAGE),
String.class, "");
this.toItemScript = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_ITEM_SCRIPT),
String.class, "");
this.toHandlerScript = ConfigParser
.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_HANDLER_SCRIPT), String.class, "");
if (scriptLanguage.isBlank()) {
logger.error("Script language is not defined. Profile will discard all states and commands.");
isConfigured = false;
return;
}
if (toItemScript.isBlank() && toHandlerScript.isBlank()) {
logger.error(
"Neither 'toItem' nor 'toHandler' script defined. Profile will discard all states and commands.");
@ -94,7 +86,7 @@ public class ScriptProfile implements StateProfile {
@Override
public ProfileTypeUID getProfileTypeUID() {
return ScriptProfileFactory.SCRIPT_PROFILE_UID;
return profileTypeUID;
}
@Override
@ -149,7 +141,7 @@ public class ScriptProfile implements StateProfile {
private @Nullable String executeScript(String script, Type input) {
if (!script.isBlank()) {
try {
return transformationService.transform(scriptLanguage + ":" + script, input.toFullString());
return transformationService.transform(script, input.toFullString());
} catch (TransformationException e) {
if (e.getCause() instanceof ScriptException) {
logger.error("Failed to process script '{}': {}", script, e.getCause().getMessage());

View File

@ -14,7 +14,9 @@ package org.openhab.core.automation.module.script.profile;
import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -28,48 +30,58 @@ import org.openhab.core.thing.profiles.ProfileTypeBuilder;
import org.openhab.core.thing.profiles.ProfileTypeProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.transform.TransformationService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
/**
* The {@link ScriptProfileFactory} creates {@link ScriptProfile} instances
*
* @author Jan N. Klug - Initial contribution
*/
@Component(service = { ScriptProfileFactory.class, ProfileFactory.class, ProfileTypeProvider.class })
@NonNullByDefault
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
public class ScriptProfileFactory implements ProfileFactory, ProfileTypeProvider {
public static final String PROFILE_CONFIG_URI_PREFIX = "profile:transform:";
public static final ProfileTypeUID SCRIPT_PROFILE_UID = new ProfileTypeUID(
TransformationService.TRANSFORM_PROFILE_SCOPE, "SCRIPT");
private static final ProfileType PROFILE_TYPE_SCRIPT = ProfileTypeBuilder.newState(SCRIPT_PROFILE_UID, "Script")
.build();
private final ScriptTransformationService transformationService;
@Activate
public ScriptProfileFactory(final @Reference ScriptTransformationService transformationService) {
this.transformationService = transformationService;
}
private final Map<String, ServiceRecord> services = new ConcurrentHashMap<>();
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext profileContext) {
if (SCRIPT_PROFILE_UID.equals(profileTypeUID)) {
return new ScriptProfile(callback, profileContext, transformationService);
}
return null;
String serviceId = profileTypeUID.getId();
ScriptTransformationService transformationService = services.get(serviceId).service();
return new ScriptProfile(profileTypeUID, callback, profileContext, transformationService);
}
@Override
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
return Set.of(SCRIPT_PROFILE_UID);
return services.keySet().stream()
.map(id -> new ProfileTypeUID(TransformationService.TRANSFORM_PROFILE_SCOPE, id)).toList();
}
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return Set.of(PROFILE_TYPE_SCRIPT);
return getSupportedProfileTypeUIDs().stream().map(uid -> {
String id = uid.getId();
String label = services.get(id).serviceLabel();
return ProfileTypeBuilder.newState(uid, label).build();
}).collect(Collectors.toList());
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void bindScriptTransformationService(ScriptTransformationService service, Map<String, Object> properties) {
String serviceId = (String) properties.get(TransformationService.SERVICE_PROPERTY_NAME);
String serviceLabel = (String) properties.get(TransformationService.SERVICE_PROPERTY_LABEL);
services.put(serviceId, new ServiceRecord(service, serviceLabel));
}
public void unbindScriptTransformationService(ScriptTransformationService service, Map<String, Object> properties) {
String serviceId = (String) properties.get(TransformationService.SERVICE_PROPERTY_NAME);
services.remove(serviceId);
}
private record ServiceRecord(ScriptTransformationService service, String serviceLabel) {
}
}

View File

@ -5,10 +5,6 @@
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:transform:SCRIPT">
<parameter name="scriptLanguage" type="text" required="true">
<label>Script Language</label>
<description>MIME-type ("application/vnd.openhab.dsl.rule") of the scripting language</description>
</parameter>
<parameter name="toItemScript" type="text">
<label>Thing To Item Transformation</label>
<description>The Script for transforming state updates and commands from the Thing handler to the item. The script

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = Script Language
profile.system.script.scriptLanguage.description = MIME-type ("application/vnd.openhab.dsl.rule") of the scripting language
profile.system.script.toItemScript.label = Thing To Item Transformation
profile.system.script.toItemScript.description = The Script for transforming state updates and commands from the Thing handler to the item. The script may return null to discard the updates/commands and not pass them through.
profile.system.script.toHandlerScript.label = Item To Thing Transformation

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = Script-sprog
profile.system.script.scriptLanguage.description = MIME-type ("application/vnd.openhab.dsl.rule") for script-sproget
profile.system.script.toItemScript.label = Til item-script
profile.system.script.toItemScript.description = Scriptet til at transformere tilstande og kommandoer fra handler til item.
profile.system.script.toHandlerScript.label = Til handler-script

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = Skriptsprache
profile.system.script.scriptLanguage.description = MIME-Typ ("application/vnd.openhab.dsl.rule") der Skriptsprache.
profile.system.script.toItemScript.label = Transformation Thing -> Item
profile.system.script.toItemScript.description = Das Skript für die Transformtion von States und Commands vom Thing zum Item.
profile.system.script.toHandlerScript.label = Transformation Item -> Thing

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = Skriptin kieli
profile.system.script.scriptLanguage.description = Skriptin kielen MIME-tyyppi ("application/vnd.openhab.dsl.rule")
profile.system.script.toItemScript.label = Item-skriptiksi
profile.system.script.toItemScript.description = Skripti, jota käytetään tilojen ja komentojen muuntoon käsittelijästä itemiksi.
profile.system.script.toHandlerScript.label = Käsittelijäskriptiksi

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = שפת תסריט
profile.system.script.scriptLanguage.description = סוג MIME ("application/vnd.openhab.dsl.rule") של שפת הסקריפט
profile.system.script.toItemScript.label = המרת Thing לפריט
profile.system.script.toItemScript.description = הסקריפט להפיכת עדכוני מצב ופקודות מהמטפל ב-Thing לפריט. הסקריפט עשוי לחזור null כדי למחוק את העדכונים/פקודות ולא להעביר אותם.
profile.system.script.toHandlerScript.label = שינוי פריט ל-thing

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = Linguaggio Script
profile.system.script.scriptLanguage.description = Tipo MIME ("application/vnd.openhab.dsl.rule") del linguaggio di scripting
profile.system.script.toItemScript.label = Trasformazione da Thing a Item
profile.system.script.toItemScript.description = Lo script per trasformare gli aggiornamenti dello stato e i comandi dal gestore Thing all'Item. Lo script può restituire null per scartare gli aggiornamenti/comandi e non passarli.
profile.system.script.toHandlerScript.label = Trasformazione da Item a Thing

View File

@ -1,5 +1,3 @@
profile.system.script.scriptLanguage.label = Język Skryptu
profile.system.script.scriptLanguage.description = MIME-Typ języka skryptu ("application/vnd.openhab.dsl.rule")
profile.system.script.toItemScript.label = Skrypt transformacji z kanału do elementu
profile.system.script.toItemScript.description = Skrypt do transformacji stanu lub wartości z kanału do elementu. Skrypt może zwrócić stan lub wartość *null* aby pominąć transformację i nie przekazać jej do elementu.
profile.system.script.toHandlerScript.label = Skrypt transformacji z elementu do kanału

View File

@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@ -36,6 +37,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.transform.Transformation;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationRegistry;
@ -50,7 +52,7 @@ import org.openhab.core.transform.TransformationRegistry;
@MockitoSettings(strictness = Strictness.LENIENT)
public class ScriptTransformationServiceTest {
private static final String SCRIPT_LANGUAGE = "customDsl";
private static final String SCRIPT_UID = "scriptUid";
private static final String SCRIPT_UID = "scriptUid." + SCRIPT_LANGUAGE;
private static final String INVALID_SCRIPT_UID = "invalidScriptUid";
private static final String INLINE_SCRIPT = "|inlineScript";
@ -59,7 +61,7 @@ public class ScriptTransformationServiceTest {
private static final String SCRIPT_OUTPUT = "output";
private static final Transformation TRANSFORMATION_CONFIGURATION = new Transformation(SCRIPT_UID, "label",
ScriptTransformationService.SUPPORTED_CONFIGURATION_TYPE, Map.of(Transformation.FUNCTION, SCRIPT));
SCRIPT_LANGUAGE, Map.of(Transformation.FUNCTION, SCRIPT));
private static final Transformation INVALID_TRANSFORMATION_CONFIGURATION = new Transformation(INVALID_SCRIPT_UID,
"label", "invalid", Map.of(Transformation.FUNCTION, SCRIPT));
@ -73,7 +75,10 @@ public class ScriptTransformationServiceTest {
@BeforeEach
public void setUp() throws ScriptException {
service = new ScriptTransformationService(transformationRegistry, scriptEngineManager);
Map<String, Object> properties = new HashMap<>();
properties.put(ScriptTransformationService.SCRIPT_TYPE_PROPERTY_NAME, SCRIPT_LANGUAGE);
service = new ScriptTransformationService(transformationRegistry, mock(ConfigDescriptionRegistry.class),
scriptEngineManager, properties);
when(scriptEngineManager.createScriptEngine(eq(SCRIPT_LANGUAGE), any())).thenReturn(scriptEngineContainer);
when(scriptEngineManager.isSupported(anyString()))
@ -96,14 +101,14 @@ public class ScriptTransformationServiceTest {
@Test
public void success() throws TransformationException {
String returnValue = Objects.requireNonNull(service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input"));
String returnValue = Objects.requireNonNull(service.transform(SCRIPT_UID, "input"));
assertThat(returnValue, is(SCRIPT_OUTPUT));
}
@Test
public void scriptExecutionParametersAreInjectedIntoEngineContext() throws TransformationException {
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID + "?param1=value1&param2=value2", "input");
service.transform(SCRIPT_UID + "?param1=value1&param2=value2", "input");
verify(scriptContext).setAttribute(eq("input"), eq("input"), eq(ScriptContext.ENGINE_SCOPE));
verify(scriptContext).setAttribute(eq("param1"), eq("value1"), eq(ScriptContext.ENGINE_SCOPE));
@ -111,6 +116,16 @@ public class ScriptTransformationServiceTest {
verifyNoMoreInteractions(scriptContext);
}
@Test
public void scriptExecutionParametersAreDecoded() throws TransformationException {
service.transform(SCRIPT_UID + "?param1=%26amp;&param2=%3dvalue", "input");
verify(scriptContext).setAttribute(eq("input"), eq("input"), eq(ScriptContext.ENGINE_SCOPE));
verify(scriptContext).setAttribute(eq("param1"), eq("&amp;"), eq(ScriptContext.ENGINE_SCOPE));
verify(scriptContext).setAttribute(eq("param2"), eq("=value"), eq(ScriptContext.ENGINE_SCOPE));
verifyNoMoreInteractions(scriptContext);
}
@Test
public void scriptSetAttributesBeforeCompiling() throws TransformationException, ScriptException {
abstract class CompilableScriptEngine implements ScriptEngine, Compilable {
@ -122,7 +137,7 @@ public class ScriptTransformationServiceTest {
InOrder inOrder = inOrder(scriptContext, scriptEngine);
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID + "?param1=value1", "input");
service.transform(SCRIPT_UID + "?param1=value1", "input");
inOrder.verify(scriptContext, times(2)).setAttribute(anyString(), anyString(), eq(ScriptContext.ENGINE_SCOPE));
inOrder.verify((Compilable) scriptEngine).compile(SCRIPT);
@ -132,7 +147,7 @@ public class ScriptTransformationServiceTest {
@Test
public void invalidScriptExecutionParametersAreDiscarded() throws TransformationException {
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID + "?param1=value1&invalid", "input");
service.transform(SCRIPT_UID + "?param1=value1&invalid", "input");
verify(scriptContext).setAttribute(eq("input"), eq("input"), eq(ScriptContext.ENGINE_SCOPE));
verify(scriptContext).setAttribute(eq("param1"), eq("value1"), eq(ScriptContext.ENGINE_SCOPE));
@ -141,41 +156,25 @@ public class ScriptTransformationServiceTest {
@Test
public void scriptsAreCached() throws TransformationException {
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
service.transform(SCRIPT_UID, "input");
service.transform(SCRIPT_UID, "input");
verify(transformationRegistry).get(SCRIPT_UID);
}
@Test
public void scriptCacheInvalidatedAfterChange() throws TransformationException {
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
service.transform(SCRIPT_UID, "input");
service.updated(TRANSFORMATION_CONFIGURATION, TRANSFORMATION_CONFIGURATION);
service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input");
service.transform(SCRIPT_UID, "input");
verify(transformationRegistry, times(2)).get(SCRIPT_UID);
}
@Test
public void noScriptTypeThrowsException() {
TransformationException e = assertThrows(TransformationException.class,
() -> service.transform(SCRIPT_UID, "input"));
assertThat(e.getMessage(), is("Script Type must be prepended to transformation UID."));
}
@Test
public void unknownScriptTypeThrowsException() {
TransformationException e = assertThrows(TransformationException.class,
() -> service.transform("foo" + ":" + SCRIPT_UID, "input"));
assertThat(e.getMessage(), is("Script type 'foo' is not supported by any available script engine."));
}
@Test
public void unknownScriptUidThrowsException() {
TransformationException e = assertThrows(TransformationException.class,
() -> service.transform(SCRIPT_LANGUAGE + ":" + "foo", "input"));
() -> service.transform("foo", "input"));
assertThat(e.getMessage(), is("Could not get script for UID 'foo'."));
}
@ -185,24 +184,16 @@ public class ScriptTransformationServiceTest {
when(scriptEngine.eval(SCRIPT)).thenThrow(new ScriptException("exception"));
TransformationException e = assertThrows(TransformationException.class,
() -> service.transform(SCRIPT_LANGUAGE + ":" + SCRIPT_UID, "input"));
() -> service.transform(SCRIPT_UID, "input"));
assertThat(e.getMessage(), is("Failed to execute script."));
assertThat(e.getCause(), instanceOf(ScriptException.class));
assertThat(e.getCause().getMessage(), is("exception"));
}
@Test
public void invalidConfigurationTypeThrowsTransformationException() {
TransformationException e = assertThrows(TransformationException.class,
() -> service.transform(SCRIPT_LANGUAGE + ":" + INVALID_SCRIPT_UID, "input"));
assertThat(e.getMessage(), is("Configuration does not have correct type 'script' but 'invalid'."));
}
@Test
public void inlineScriptProperlyProcessed() throws TransformationException, ScriptException {
service.transform(SCRIPT_LANGUAGE + ":" + INLINE_SCRIPT, "input");
service.transform(INLINE_SCRIPT, "input");
verify(scriptEngine).eval(INLINE_SCRIPT.substring(1));
}

View File

@ -13,11 +13,11 @@
package org.openhab.core.automation.module.script.profile;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_SCRIPT_LANGUAGE;
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_TO_HANDLER_SCRIPT;
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_TO_ITEM_SCRIPT;
@ -42,6 +42,7 @@ import org.openhab.core.library.types.PercentType;
import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
@ -67,11 +68,12 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptDefined() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL").build();
ProfileContext profileContext = ProfileContextBuilder.create().build();
setupInterceptedLogger(ScriptProfile.class, LogLevel.ERROR);
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
@ -86,40 +88,16 @@ public class ScriptProfileTest extends JavaTest {
"Neither 'toItem' nor 'toHandler' script defined. Profile will discard all states and commands.");
}
@Test
public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptLanguageDefined()
throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
.withAcceptedDataTypes(List.of(PercentType.class))
.withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
setupInterceptedLogger(ScriptProfile.class, LogLevel.ERROR);
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
scriptProfile.onCommandFromItem(OnOffType.ON);
verify(transformationServiceMock, never()).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
assertLogMessage(ScriptProfile.class, LogLevel.ERROR,
"Script language is not defined. Profile will discard all states and commands.");
}
@Test
public void scriptExecutionErrorForwardsNoValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToItemScript("inScript").withToHandlerScript("outScript").build();
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").build();
when(transformationServiceMock.transform(any(), any()))
.thenThrow(new TransformationException("intentional failure"));
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
@ -133,12 +111,13 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void scriptExecutionResultNullForwardsNoValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToItemScript("inScript").withToHandlerScript("outScript").build();
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").build();
when(transformationServiceMock.transform(any(), any())).thenReturn(null);
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
@ -152,14 +131,15 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void scriptExecutionResultForwardsTransformedValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToItemScript("inScript").withToHandlerScript("outScript")
.withAcceptedCommandTypes(List.of(OnOffType.class)).withAcceptedDataTypes(List.of(OnOffType.class))
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(OnOffType.class))
.withAcceptedDataTypes(List.of(OnOffType.class))
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@ -173,14 +153,14 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void onlyToItemScriptDoesNotForwardOutboundCommands() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToItemScript("inScript").withAcceptedCommandTypes(List.of(OnOffType.class))
.withAcceptedDataTypes(List.of(OnOffType.class))
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withAcceptedCommandTypes(List.of(OnOffType.class)).withAcceptedDataTypes(List.of(OnOffType.class))
.withHandlerAcceptedCommandTypes(List.of(DecimalType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@ -194,14 +174,14 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void onlyToHandlerScriptDoesNotForwardInboundCommands() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
.withAcceptedDataTypes(List.of(DecimalType.class))
ProfileContext profileContext = ProfileContextBuilder.create().withToHandlerScript("outScript")
.withAcceptedCommandTypes(List.of(DecimalType.class)).withAcceptedDataTypes(List.of(DecimalType.class))
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@ -215,14 +195,15 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void incompatibleStateOrCommandNotForwardedToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToItemScript("inScript").withToHandlerScript("outScript")
.withAcceptedCommandTypes(List.of(DecimalType.class)).withAcceptedDataTypes(List.of(PercentType.class))
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
.withAcceptedDataTypes(List.of(PercentType.class))
.withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
ScriptProfile scriptProfile = new ScriptProfile(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
@ -244,11 +225,6 @@ public class ScriptProfileTest extends JavaTest {
return new ProfileContextBuilder();
}
public ProfileContextBuilder withScriptLanguage(String scriptLanguage) {
configuration.put(CONFIG_SCRIPT_LANGUAGE, scriptLanguage);
return this;
}
public ProfileContextBuilder withToItemScript(String toItem) {
configuration.put(CONFIG_TO_ITEM_SCRIPT, toItem);
return this;

View File

@ -117,8 +117,9 @@ public class TransformationResource implements RESTResource {
try {
Collection<ServiceReference<TransformationService>> refs = bundleContext
.getServiceReferences(TransformationService.class, null);
Stream<String> services = refs.stream().map(ref -> (String) ref.getProperty("openhab.transform"))
.filter(Objects::nonNull).map(Objects::requireNonNull);
Stream<String> services = refs.stream()
.map(ref -> (String) ref.getProperty(TransformationService.SERVICE_PROPERTY_NAME))
.filter(Objects::nonNull).map(Objects::requireNonNull).sorted();
return Response.ok(new Stream2JSONInputStream(services)).build();
} catch (InvalidSyntaxException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();

View File

@ -61,7 +61,7 @@ public class TransformationHelper {
public static @Nullable TransformationService getTransformationService(@Nullable BundleContext context,
String transformationType) {
if (context != null) {
String filter = "(openhab.transform=" + transformationType + ")";
String filter = "(" + TransformationService.SERVICE_PROPERTY_NAME + "=" + transformationType + ")";
try {
Collection<ServiceReference<TransformationService>> refs = context
.getServiceReferences(TransformationService.class, filter);

View File

@ -32,8 +32,10 @@ import org.eclipse.jdt.annotation.Nullable;
@NonNullByDefault
public interface TransformationService {
public static final String TRANSFORM_FOLDER_NAME = "transform";
public static final String TRANSFORM_PROFILE_SCOPE = "transform";
String SERVICE_PROPERTY_NAME = "openhab.transform";
String SERVICE_PROPERTY_LABEL = "openhab.transform.label";
String TRANSFORM_FOLDER_NAME = "transform";
String TRANSFORM_PROFILE_SCOPE = "transform";
/**
* Transforms the input <code>source</code> by means of the given <code>function</code> and returns the transformed