-
-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Jan N. Klug <[email protected]>
- Loading branch information
Showing
3 changed files
with
293 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
.../src/main/java/org/openhab/core/automation/module/script/ScriptTransformationService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
/** | ||
* Copyright (c) 2010-2022 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.HashMap; | ||
import java.util.Map; | ||
|
||
import javax.script.Compilable; | ||
import javax.script.CompiledScript; | ||
import javax.script.ScriptContext; | ||
import javax.script.ScriptEngine; | ||
import javax.script.ScriptException; | ||
|
||
import org.eclipse.jdt.annotation.NonNullByDefault; | ||
import org.eclipse.jdt.annotation.Nullable; | ||
import org.openhab.core.common.registry.RegistryChangeListener; | ||
import org.openhab.core.transform.TransformationConfiguration; | ||
import org.openhab.core.transform.TransformationConfigurationRegistry; | ||
import org.openhab.core.transform.TransformationException; | ||
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 ScriptTransformationService} implements a {@link TransformationService} using any available script | ||
* language | ||
* | ||
* @author Jan N. Klug - Initial contribution | ||
*/ | ||
@Component(service = TransformationService.class, property = { "openhab.transform=SCRIPT" }) | ||
@NonNullByDefault | ||
public class ScriptTransformationService | ||
implements TransformationService, RegistryChangeListener<TransformationConfiguration> { | ||
public static final String OPENHAB_TRANSFORMATION_SCRIPT = "openhab-transformation-script-"; | ||
|
||
private final Map<String, ScriptEngineContainer> scriptEngineContainers = new HashMap<>(); | ||
private final Map<String, CompiledScript> compiledScripts = new HashMap<>(); | ||
private final Map<String, String> scriptCache = new HashMap<>(); | ||
|
||
private final TransformationConfigurationRegistry transformationConfigurationRegistry; | ||
private final ScriptEngineManager scriptEngineManager; | ||
|
||
@Activate | ||
public ScriptTransformationService( | ||
@Reference TransformationConfigurationRegistry transformationConfigurationRegistry, | ||
@Reference ScriptEngineManager scriptEngineManager) { | ||
this.transformationConfigurationRegistry = transformationConfigurationRegistry; | ||
this.scriptEngineManager = scriptEngineManager; | ||
} | ||
|
||
@Override | ||
public @Nullable String transform(String function, String source) throws TransformationException { | ||
int splitPoint = function.indexOf(":"); | ||
if (splitPoint < 1) { | ||
throw new TransformationException("Script Type must be prepended to transformation UID."); | ||
} | ||
String scriptType = function.substring(0, splitPoint); | ||
String scriptUid = function.substring(splitPoint + 1); | ||
|
||
String script = scriptCache.get(scriptUid); | ||
if (script == null) { | ||
TransformationConfiguration transformationConfiguration = transformationConfigurationRegistry | ||
.get(scriptUid); | ||
if (transformationConfiguration != null) { | ||
script = transformationConfiguration.getContent(); | ||
} | ||
if (script == null) { | ||
throw new TransformationException("Could not get script for UID '" + scriptUid + "'."); | ||
} | ||
|
||
scriptCache.put(scriptUid, script); | ||
} | ||
|
||
if (!scriptEngineManager.isSupported(scriptType)) { | ||
// language has been removed, clear container and compiled scripts if found | ||
if (scriptEngineContainers.containsKey(scriptUid)) { | ||
scriptEngineManager.removeEngine(OPENHAB_TRANSFORMATION_SCRIPT + scriptUid); | ||
} | ||
clearCache(scriptUid); | ||
throw new TransformationException( | ||
"Script type '" + scriptType + "' is not supported by any available script engine."); | ||
} | ||
|
||
ScriptEngineContainer scriptEngineContainer = scriptEngineContainers.computeIfAbsent(scriptUid, | ||
k -> scriptEngineManager.createScriptEngine(scriptType, OPENHAB_TRANSFORMATION_SCRIPT + k)); | ||
|
||
if (scriptEngineContainer == null) { | ||
throw new TransformationException("Failed to create script engine container for '" + function + "'."); | ||
} | ||
if (scriptEngineContainer != null) { | ||
try { | ||
CompiledScript compiledScript = this.compiledScripts.get(scriptUid); | ||
|
||
if (compiledScript == null && scriptEngineContainer.getScriptEngine() instanceof Compilable) { | ||
// no compiled script available but compiling is supported | ||
compiledScript = ((Compilable) scriptEngineContainer.getScriptEngine()).compile(script); | ||
this.compiledScripts.put(scriptUid, compiledScript); | ||
} | ||
|
||
ScriptEngine engine = compiledScript != null ? compiledScript.getEngine() | ||
: scriptEngineContainer.getScriptEngine(); | ||
ScriptContext executionContext = engine.getContext(); | ||
executionContext.setAttribute("inputString", source, ScriptContext.ENGINE_SCOPE); | ||
|
||
Object result = compiledScript != null ? compiledScript.eval() : engine.eval(script); | ||
return result == null ? null : result.toString(); | ||
} catch (ScriptException e) { | ||
throw new TransformationException("Failed to execute script.", e); | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
@Override | ||
public void added(TransformationConfiguration element) { | ||
clearCache(element.getUID()); | ||
} | ||
|
||
@Override | ||
public void removed(TransformationConfiguration element) { | ||
|
||
clearCache(element.getUID()); | ||
} | ||
|
||
@Override | ||
public void updated(TransformationConfiguration oldElement, TransformationConfiguration element) { | ||
clearCache(element.getUID()); | ||
} | ||
|
||
private void clearCache(String uid) { | ||
compiledScripts.remove(uid); | ||
scriptEngineContainers.remove(uid); | ||
scriptCache.remove(uid); | ||
} | ||
} |
141 changes: 141 additions & 0 deletions
141
.../test/java/org/openhab/core/automation/module/script/ScriptTransformationServiceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/** | ||
* Copyright (c) 2010-2022 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 static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.instanceOf; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.ArgumentMatchers.anyString; | ||
import static org.mockito.ArgumentMatchers.eq; | ||
import static org.mockito.Mockito.times; | ||
import static org.mockito.Mockito.verify; | ||
import static org.mockito.Mockito.when; | ||
|
||
import javax.script.ScriptContext; | ||
import javax.script.ScriptEngine; | ||
import javax.script.ScriptException; | ||
|
||
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.transform.TransformationConfiguration; | ||
import org.openhab.core.transform.TransformationConfigurationRegistry; | ||
import org.openhab.core.transform.TransformationException; | ||
|
||
/** | ||
* The {@link ScriptTransformationServiceTest} holds tests for the {@link ScriptTransformationService} | ||
* | ||
* @author Jan N. Klug - Initial contribution | ||
*/ | ||
@NonNullByDefault | ||
@ExtendWith(MockitoExtension.class) | ||
@MockitoSettings(strictness = Strictness.WARN) | ||
public class ScriptTransformationServiceTest { | ||
private static final String COMPILABLE_SCRIPT_TYPE = "compilableScript"; | ||
private static final String SCRIPT_TYPE = "script"; | ||
private static final String SCRIPT_UID = "scriptUid"; | ||
private static final String SCRIPT = "script"; | ||
private static final String SCRIPT_OUTPUT = "output"; | ||
|
||
private static final TransformationConfiguration TRANSFORMATION_CONFIGURATION = new TransformationConfiguration( | ||
SCRIPT_UID, "label", "script", null, SCRIPT); | ||
private @Mock @NonNullByDefault({}) TransformationConfigurationRegistry transformationConfigurationRegistry; | ||
private @Mock @NonNullByDefault({}) ScriptEngineManager scriptEngineManager; | ||
private @Mock @NonNullByDefault({}) ScriptEngineContainer scriptEngineContainer; | ||
private @Mock @NonNullByDefault({}) ScriptEngine scriptEngine; | ||
private @Mock @NonNullByDefault({}) ScriptContext scriptContext; | ||
|
||
private @NonNullByDefault({}) ScriptTransformationService service; | ||
|
||
@BeforeEach | ||
public void setUp() throws ScriptException { | ||
service = new ScriptTransformationService(transformationConfigurationRegistry, scriptEngineManager); | ||
|
||
when(scriptEngineManager.createScriptEngine(eq(SCRIPT_TYPE), any())).thenReturn(scriptEngineContainer); | ||
when(scriptEngineManager.isSupported(anyString())) | ||
.thenAnswer(scriptType -> SCRIPT_TYPE.equals(scriptType.getArgument(0))); | ||
when(scriptEngineContainer.getScriptEngine()).thenReturn(scriptEngine); | ||
when(scriptEngine.eval(SCRIPT)).thenReturn("output"); | ||
when(scriptEngine.getContext()).thenReturn(scriptContext); | ||
|
||
when(transformationConfigurationRegistry.get(anyString())).thenAnswer( | ||
scriptUid -> SCRIPT_UID.equals(scriptUid.getArgument(0)) ? TRANSFORMATION_CONFIGURATION : null); | ||
} | ||
|
||
@Test | ||
public void success() throws TransformationException { | ||
String returnValue = service.transform(SCRIPT_TYPE + ":" + SCRIPT_UID, "input"); | ||
|
||
assertThat(returnValue, is(SCRIPT_OUTPUT)); | ||
} | ||
|
||
@Test | ||
public void scriptsAreCached() throws TransformationException { | ||
service.transform(SCRIPT_TYPE + ":" + SCRIPT_UID, "input"); | ||
service.transform(SCRIPT_TYPE + ":" + SCRIPT_UID, "input"); | ||
|
||
verify(transformationConfigurationRegistry).get(SCRIPT_UID); | ||
} | ||
|
||
@Test | ||
public void scriptCacheInvalidatedAfterChange() throws TransformationException { | ||
service.transform(SCRIPT_TYPE + ":" + SCRIPT_UID, "input"); | ||
service.updated(TRANSFORMATION_CONFIGURATION, TRANSFORMATION_CONFIGURATION); | ||
service.transform(SCRIPT_TYPE + ":" + SCRIPT_UID, "input"); | ||
|
||
verify(transformationConfigurationRegistry, 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_TYPE + ":" + "foo", "input")); | ||
|
||
assertThat(e.getMessage(), is("Could not get script for UID 'foo'.")); | ||
} | ||
|
||
@Test | ||
public void scriptExceptionResultsInTransformationException() throws ScriptException { | ||
when(scriptEngine.eval(SCRIPT)).thenThrow(new ScriptException("exception")); | ||
|
||
TransformationException e = assertThrows(TransformationException.class, | ||
() -> service.transform(SCRIPT_TYPE + ":" + SCRIPT_UID, "input")); | ||
|
||
assertThat(e.getMessage(), is("Failed to execute script.")); | ||
assertThat(e.getCause(), instanceOf(ScriptException.class)); | ||
assertThat(e.getCause().getMessage(), is("exception")); | ||
} | ||
} |