Introduce a profile for the generic SCRIPT transformation (#3292)

* Introduce a generic script profile

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/3469/head
J-N-K 2023-03-20 21:59:48 +01:00 committed by GitHub
parent cb38d19360
commit 7bad9baa85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 714 additions and 35 deletions

View File

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

View File

@ -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;
@ -20,6 +24,7 @@ import java.util.concurrent.locks.Lock;
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;
@ -29,8 +34,12 @@ 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.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;
@ -39,6 +48,8 @@ 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;
@ -48,10 +59,13 @@ import org.slf4j.LoggerFactory;
*
* @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
@ -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
@ -160,10 +176,10 @@ public class ScriptTransformationService implements TransformationService, Regis
// 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;
}
@ -211,14 +227,13 @@ public class ScriptTransformationService implements TransformationService, Regis
}
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);
}
@ -228,6 +243,38 @@ public class ScriptTransformationService implements TransformationService, Regis
}
}
@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,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());
}
}

View File

@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
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;
@ -146,21 +147,14 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
*/
@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");
}
}
@ -169,7 +163,7 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
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();
@ -181,16 +175,4 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
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());
}
}

View File

@ -0,0 +1,164 @@
/**
* 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.profile;
import java.util.List;
import javax.script.ScriptException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.ConfigParser;
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.thing.profiles.StateProfile;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.TypeParser;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ScriptProfile} is generic profile for managing values with scripts
*
* @author Jan N. Klug - Initial contribution
*/
@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";
private final Logger logger = LoggerFactory.getLogger(ScriptProfile.class);
private final ProfileCallback callback;
private final TransformationService transformationService;
private final List<Class<? extends State>> acceptedDataTypes;
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 boolean isConfigured;
public ScriptProfile(ProfileCallback callback, ProfileContext profileContext,
TransformationService transformationService) {
this.callback = callback;
this.transformationService = transformationService;
this.acceptedCommandTypes = profileContext.getAcceptedCommandTypes();
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.");
isConfigured = false;
return;
}
isConfigured = true;
}
@Override
public ProfileTypeUID getProfileTypeUID() {
return ScriptProfileFactory.SCRIPT_PROFILE_UID;
}
@Override
public void onStateUpdateFromItem(State state) {
}
@Override
public void onCommandFromItem(Command command) {
if (isConfigured) {
String returnValue = executeScript(toHandlerScript, command);
if (returnValue != null) {
// try to parse the value
Command newCommand = TypeParser.parseCommand(handlerAcceptedCommandTypes, returnValue);
if (newCommand != null) {
callback.handleCommand(newCommand);
}
}
}
}
@Override
public void onCommandFromHandler(Command command) {
if (isConfigured) {
String returnValue = executeScript(toItemScript, command);
if (returnValue != null) {
Command newCommand = TypeParser.parseCommand(acceptedCommandTypes, returnValue);
if (newCommand != null) {
callback.sendCommand(newCommand);
}
}
}
}
@Override
public void onStateUpdateFromHandler(State state) {
if (isConfigured) {
String returnValue = executeScript(toItemScript, state);
// special handling for UnDefType, it's not available in the TypeParser
if ("UNDEF".equals(returnValue)) {
callback.sendUpdate(UnDefType.UNDEF);
} else if ("NULL".equals(returnValue)) {
callback.sendUpdate(UnDefType.NULL);
} else if (returnValue != null) {
State newState = TypeParser.parseState(acceptedDataTypes, returnValue);
if (newState != null) {
callback.sendUpdate(newState);
}
}
}
}
private @Nullable String executeScript(String script, Type input) {
if (!script.isBlank()) {
try {
return transformationService.transform(scriptLanguage + ":" + script, input.toFullString());
} catch (TransformationException e) {
if (e.getCause() instanceof ScriptException) {
logger.error("Failed to process script '{}': {}", script, e.getCause().getMessage());
} else {
logger.error("Failed to process script '{}': {}", script, e.getMessage());
}
}
}
return null;
}
}

View File

@ -0,0 +1,75 @@
/**
* 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.profile;
import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptTransformationService;
import org.openhab.core.thing.profiles.Profile;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileFactory;
import org.openhab.core.thing.profiles.ProfileType;
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;
/**
* The {@link ScriptProfileFactory} creates {@link ScriptProfile} instances
*
* @author Jan N. Klug - Initial contribution
*/
@Component(service = { ScriptProfileFactory.class, ProfileFactory.class, ProfileTypeProvider.class })
@NonNullByDefault
public class ScriptProfileFactory implements ProfileFactory, ProfileTypeProvider {
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;
}
@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;
}
@Override
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
return Set.of(SCRIPT_PROFILE_UID);
}
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return Set.of(PROFILE_TYPE_SCRIPT);
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
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>To Item Script</label>
<description>The Script for transforming states and commands from handler to item.</description>
</parameter>
<parameter name="toHandlerScript" type="text">
<label>To Handler Script</label>
<description>The Script for transforming states and commands from item to handler.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,6 @@
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 = To Item Script
profile.system.script.toItemScript.description = The Script for transforming states and commands from handler to item.
profile.system.script.toHandlerScript.label = To Handler Script
profile.system.script.toHandlerScript.description = The Script for transforming states and commands from item to handler.

View File

@ -0,0 +1,307 @@
/**
* 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.profile;
import static org.mockito.ArgumentMatchers.any;
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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
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.transform.TransformationException;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link ScriptProfileTest} contains tests for the {@link ScriptProfile}
*
* @author Jan N. Klug - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class ScriptProfileTest extends JavaTest {
private @Mock @NonNullByDefault({}) ProfileCallback profileCallback;
private @Mock @NonNullByDefault({}) TransformationService transformationServiceMock;
@BeforeEach
public void setUp() throws TransformationException {
when(transformationServiceMock.transform(any(), any())).thenReturn("");
}
@Test
public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptDefined() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL").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,
"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();
when(transformationServiceMock.transform(any(), any()))
.thenThrow(new TransformationException("intentional failure"));
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
scriptProfile.onCommandFromItem(OnOffType.ON);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@Test
public void scriptExecutionResultNullForwardsNoValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.withToItemScript("inScript").withToHandlerScript("outScript").build();
when(transformationServiceMock.transform(any(), any())).thenReturn(null);
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
scriptProfile.onCommandFromItem(OnOffType.ON);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@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))
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(profileCallback).handleCommand(OnOffType.OFF);
verify(profileCallback).sendUpdate(OnOffType.OFF);
verify(profileCallback).sendCommand(OnOffType.OFF);
}
@Test
public void onlyToItemScriptDoesNotForwardOutboundCommands() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.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.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(2)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback).sendUpdate(OnOffType.OFF);
verify(profileCallback).sendCommand(OnOffType.OFF);
}
@Test
public void onlyToHandlerScriptDoesNotForwardInboundCommands() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
.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.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
verify(transformationServiceMock).transform(any(), any());
verify(profileCallback).handleCommand(OnOffType.OFF);
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@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))
.withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
private static class ProfileContextBuilder {
private final Map<String, Object> configuration = new HashMap<>();
private List<Class<? extends State>> acceptedDataTypes = List.of();
private List<Class<? extends Command>> acceptedCommandTypes = List.of();
private List<Class<? extends Command>> handlerAcceptedCommandTypes = List.of();
public static ProfileContextBuilder create() {
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;
}
public ProfileContextBuilder withToHandlerScript(String toHandlerScript) {
configuration.put(CONFIG_TO_HANDLER_SCRIPT, toHandlerScript);
return this;
}
public ProfileContextBuilder withAcceptedDataTypes(List<Class<? extends State>> acceptedDataTypes) {
this.acceptedDataTypes = acceptedDataTypes;
return this;
}
public ProfileContextBuilder withAcceptedCommandTypes(List<Class<? extends Command>> acceptedCommandTypes) {
this.acceptedCommandTypes = acceptedCommandTypes;
return this;
}
public ProfileContextBuilder withHandlerAcceptedCommandTypes(
List<Class<? extends Command>> handlerAcceptedCommandTypes) {
this.handlerAcceptedCommandTypes = handlerAcceptedCommandTypes;
return this;
}
public ProfileContext build() {
return new ProfileContext() {
@Override
public Configuration getConfiguration() {
return new Configuration(configuration);
}
@Override
public ScheduledExecutorService getExecutorService() {
throw new IllegalStateException();
}
@Override
public List<Class<? extends State>> getAcceptedDataTypes() {
return acceptedDataTypes;
}
@Override
public List<Class<? extends Command>> getAcceptedCommandTypes() {
return acceptedCommandTypes;
}
@Override
public List<Class<? extends Command>> getHandlerAcceptedCommandTypes() {
return handlerAcceptedCommandTypes;
}
};
}
}
}