diff --git a/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/CustomizableGroovyClassLoader.java b/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/CustomizableGroovyClassLoader.java new file mode 100644 index 0000000000000..0314f33d15b53 --- /dev/null +++ b/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/CustomizableGroovyClassLoader.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.automation.groovyscripting.internal; + +import java.io.File; + +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.openhab.core.OpenHAB; + +import groovy.lang.GroovyClassLoader; + +/** + * Customizes the {@link GroovyClassLoader} so that {@link CompilationCustomizer}s can be added which allows for + * importing additional classes via scopes. + * + * @author Wouter Born - Initial contribution + */ +public class CustomizableGroovyClassLoader extends GroovyClassLoader { + + private static final String FILE_DIRECTORY = "automation" + File.separator + "groovy"; + + private CompilerConfiguration config; + + public CustomizableGroovyClassLoader() { + this(CustomizableGroovyClassLoader.class.getClassLoader(), new CompilerConfiguration(), true); + } + + public CustomizableGroovyClassLoader(ClassLoader parent, CompilerConfiguration config, + boolean useConfigurationClasspath) { + super(parent, config, useConfigurationClasspath); + this.config = config; + addClasspath(OpenHAB.getConfigFolder() + File.separator + FILE_DIRECTORY); + } + + public void addCompilationCustomizers(CompilationCustomizer... customizers) { + config.addCompilationCustomizers(customizers); + } +} diff --git a/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java b/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java index f192ed0106c21..d2dac88609dd4 100644 --- a/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java +++ b/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java @@ -12,22 +12,20 @@ */ package org.openhab.automation.groovyscripting.internal; -import java.io.File; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; import java.util.stream.Stream; import javax.script.ScriptEngine; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.OpenHAB; import org.openhab.core.automation.module.script.AbstractScriptEngineFactory; import org.openhab.core.automation.module.script.ScriptEngineFactory; import org.osgi.service.component.annotations.Component; -import groovy.lang.GroovyClassLoader; - /** * This is an implementation of a {@link ScriptEngineFactory} for Groovy. * @@ -37,20 +35,11 @@ @NonNullByDefault public class GroovyScriptEngineFactory extends AbstractScriptEngineFactory { - private static final String FILE_DIRECTORY = "automation" + File.separator + "groovy"; private final org.codehaus.groovy.jsr223.GroovyScriptEngineFactory factory = new org.codehaus.groovy.jsr223.GroovyScriptEngineFactory(); - private final List scriptTypes = (List) Stream.of(factory.getExtensions(), factory.getMimeTypes()) + private final List scriptTypes = Stream.of(factory.getExtensions(), factory.getMimeTypes()) .flatMap(List::stream) // - .collect(Collectors.toUnmodifiableList()); - - private final GroovyClassLoader gcl = new GroovyClassLoader(GroovyScriptEngineFactory.class.getClassLoader()); - - public GroovyScriptEngineFactory() { - String scriptDir = OpenHAB.getConfigFolder() + File.separator + FILE_DIRECTORY; - logger.debug("Adding script directory {} to the GroovyScriptEngine class path.", scriptDir); - gcl.addClasspath(scriptDir); - } + .toList(); @Override public List getScriptTypes() { @@ -58,10 +47,24 @@ public List getScriptTypes() { } @Override - public @Nullable ScriptEngine createScriptEngine(String scriptType) { - if (scriptTypes.contains(scriptType)) { - return new org.codehaus.groovy.jsr223.GroovyScriptEngineImpl(gcl); + public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) { + ImportCustomizer importCustomizer = new ImportCustomizer(); + for (Map.Entry entry : scopeValues.entrySet()) { + if (entry.getValue() instanceof Class clazz) { + importCustomizer.addImport(entry.getKey(), clazz.getCanonicalName()); + } else { + scriptEngine.put(entry.getKey(), entry.getValue()); + } } - return null; + + GroovyScriptEngineImpl gse = (GroovyScriptEngineImpl) scriptEngine; + CustomizableGroovyClassLoader cl = (CustomizableGroovyClassLoader) gse.getClassLoader(); + cl.addCompilationCustomizers(importCustomizer); + } + + @Override + public @Nullable ScriptEngine createScriptEngine(String scriptType) { + return scriptTypes.contains(scriptType) ? new GroovyScriptEngineImpl(new CustomizableGroovyClassLoader()) + : null; } } diff --git a/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/AbstractGroovyScriptingOSGiTest.java b/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/AbstractGroovyScriptingOSGiTest.java new file mode 100644 index 0000000000000..e9a0d6401af21 --- /dev/null +++ b/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/AbstractGroovyScriptingOSGiTest.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 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.automation.groovyscripting; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Objects; + +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.openhab.core.automation.module.script.ScriptEngineContainer; +import org.openhab.core.automation.module.script.ScriptEngineManager; +import org.openhab.core.test.java.JavaOSGiTest; + +/** + * Provides helper methods that can be reused for testing Groovy scripts. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractGroovyScriptingOSGiTest extends JavaOSGiTest { + + protected @NonNullByDefault({}) ScriptEngine engine; + + private final String path = "OH-INF/automation/jsr223/"; + + @BeforeEach + public void init() { + ScriptEngineManager scriptManager = Objects.requireNonNull(getService(ScriptEngineManager.class), + "Could not get ScriptEngineManager"); + ScriptEngineContainer container = Objects.requireNonNull( + scriptManager.createScriptEngine("groovy", "testGroovyEngine"), "Could not create Groovy ScriptEngine"); + engine = container.getScriptEngine(); + } + + protected void evalScript(String fileName) throws ScriptException, IOException { + URL url = bundleContext.getBundle().getResource(path + fileName); + engine.eval(new InputStreamReader(url.openStream())); + } +} diff --git a/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/ScriptScopeOSGiTest.java b/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/ScriptScopeOSGiTest.java new file mode 100644 index 0000000000000..6b2da61a94c15 --- /dev/null +++ b/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/ScriptScopeOSGiTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 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.automation.groovyscripting; + +import java.io.IOException; + +import javax.script.ScriptException; + +import org.junit.jupiter.api.Test; + +/** + * This tests the script modules using the Groovy scripting engine. + * + * @author Wouter Born - Initial contribution + */ +public class ScriptScopeOSGiTest extends AbstractGroovyScriptingOSGiTest { + + @Test + public void scopeWorking() throws ScriptException, IOException { + evalScript("scope-working.groovy"); + } +} diff --git a/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/SlurperOSGiTest.java b/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/SlurperOSGiTest.java index c12e6db610a54..430268d94e3c1 100644 --- a/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/SlurperOSGiTest.java +++ b/itests/org.openhab.automation.groovyscripting.tests/src/main/java/org/openhab/automation/groovyscripting/SlurperOSGiTest.java @@ -13,18 +13,11 @@ package org.openhab.automation.groovyscripting; import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -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.openhab.core.automation.module.script.ScriptEngineContainer; -import org.openhab.core.automation.module.script.ScriptEngineManager; -import org.openhab.core.test.java.JavaOSGiTest; /** * This tests the JSON, XML and YAML slurpers using the Groovy scripting engine. @@ -32,23 +25,7 @@ * @author Wouter Born - Initial contribution */ @NonNullByDefault -public class SlurperOSGiTest extends JavaOSGiTest { - - private @NonNullByDefault({}) ScriptEngine engine; - - private final String path = "OH-INF/automation/jsr223/"; - - @BeforeEach - public void init() { - ScriptEngineManager scriptManager = getService(ScriptEngineManager.class); - ScriptEngineContainer container = scriptManager.createScriptEngine("groovy", "myGroovyEngine"); - engine = container.getScriptEngine(); - } - - private void evalScript(String fileName) throws ScriptException, IOException { - URL url = bundleContext.getBundle().getResource(path + fileName); - engine.eval(new InputStreamReader(url.openStream())); - } +public class SlurperOSGiTest extends AbstractGroovyScriptingOSGiTest { @Test public void jsonSlurper() throws ScriptException, IOException { diff --git a/itests/org.openhab.automation.groovyscripting.tests/src/main/resources/OH-INF/automation/jsr223/scope-working.groovy b/itests/org.openhab.automation.groovyscripting.tests/src/main/resources/OH-INF/automation/jsr223/scope-working.groovy new file mode 100644 index 0000000000000..508cb06a512b0 --- /dev/null +++ b/itests/org.openhab.automation.groovyscripting.tests/src/main/resources/OH-INF/automation/jsr223/scope-working.groovy @@ -0,0 +1,163 @@ +import static org.hamcrest.CoreMatchers.* +import static org.hamcrest.MatcherAssert.assertThat + +assertThat(actions, instanceOf(Object)) +assertThat(events, instanceOf(Object)) +assertThat(ir, instanceOf(Object)) +assertThat(itemRegistry, instanceOf(Object)) +assertThat(ir, is(itemRegistry)) +assertThat(items, instanceOf(Object)) +assertThat(rules, instanceOf(Object)) +assertThat(se, instanceOf(Object)) +assertThat(scriptExtension, instanceOf(Object)) +assertThat(se, is(scriptExtension)) +assertThat(things, instanceOf(Object)) + +assertThat(State, instanceOf(Class)) +assertThat(State.getCanonicalName(), is("org.openhab.core.types.State")) + +assertThat(Command, instanceOf(Class)) +assertThat(Command.getCanonicalName(), is("org.openhab.core.types.Command")) + +assertThat(URLEncoder, instanceOf(Class)) +assertThat(URLEncoder.getCanonicalName(), is("java.net.URLEncoder")) + +assertThat(File, instanceOf(Class)) +assertThat(File.getCanonicalName(), is("java.io.File")) + +assertThat(Files, instanceOf(Class)) +assertThat(Files.getCanonicalName(), is("java.nio.file.Files")) + +assertThat(Path, instanceOf(Class)) +assertThat(Path.getCanonicalName(), is("java.nio.file.Path")) + +assertThat(Paths, instanceOf(Class)) +assertThat(Paths.getCanonicalName(), is("java.nio.file.Paths")) + +assertThat(IncreaseDecreaseType, instanceOf(Class)) +assertThat(IncreaseDecreaseType.getCanonicalName(), is("org.openhab.core.library.types.IncreaseDecreaseType")) +assertThat(DECREASE, instanceOf(IncreaseDecreaseType)) +assertThat(DECREASE, is(IncreaseDecreaseType.DECREASE)) +assertThat(INCREASE, instanceOf(IncreaseDecreaseType)) +assertThat(INCREASE, is(IncreaseDecreaseType.INCREASE)) + +assertThat(OnOffType, instanceOf(Class)) +assertThat(OnOffType.getCanonicalName(), is("org.openhab.core.library.types.OnOffType")) +assertThat(ON, instanceOf(OnOffType)) +assertThat(ON, is(OnOffType.ON)) +assertThat(OFF, instanceOf(OnOffType)) +assertThat(OFF, is(OnOffType.OFF)) + +assertThat(OpenClosedType, instanceOf(Class)) +assertThat(OpenClosedType.getCanonicalName(), is("org.openhab.core.library.types.OpenClosedType")) +assertThat(CLOSED, instanceOf(OpenClosedType)) +assertThat(CLOSED, is(OpenClosedType.CLOSED)) +assertThat(OPEN, instanceOf(OpenClosedType)) +assertThat(OPEN, is(OpenClosedType.OPEN)) + +assertThat(StopMoveType, instanceOf(Class)) +assertThat(StopMoveType.getCanonicalName(), is("org.openhab.core.library.types.StopMoveType")) +assertThat(MOVE, instanceOf(StopMoveType)) +assertThat(MOVE, is(StopMoveType.MOVE)) +assertThat(STOP, instanceOf(StopMoveType)) +assertThat(STOP, is(StopMoveType.STOP)) + +assertThat(UpDownType, instanceOf(Class)) +assertThat(UpDownType.getCanonicalName(), is("org.openhab.core.library.types.UpDownType")) +assertThat(DOWN, instanceOf(UpDownType)) +assertThat(DOWN, is(UpDownType.DOWN)) +assertThat(UP, instanceOf(UpDownType)) +assertThat(UP, is(UpDownType.UP)) + +assertThat(UnDefType, instanceOf(Class)) +assertThat(UnDefType.getCanonicalName(), is("org.openhab.core.types.UnDefType")) +assertThat(NULL, instanceOf(UnDefType)) +assertThat(NULL, is(UnDefType.NULL)) +assertThat(UNDEF, instanceOf(UnDefType)) +assertThat(UNDEF, is(UnDefType.UNDEF)) + +assertThat(RefreshType, instanceOf(Class)) +assertThat(RefreshType.getCanonicalName(), is("org.openhab.core.types.RefreshType")) +assertThat(REFRESH, instanceOf(RefreshType)) +assertThat(REFRESH, is(RefreshType.REFRESH)) + +assertThat(NextPreviousType, instanceOf(Class)) +assertThat(NextPreviousType.getCanonicalName(), is("org.openhab.core.library.types.NextPreviousType")) +assertThat(NEXT, instanceOf(NextPreviousType)) +assertThat(NEXT, is(NextPreviousType.NEXT)) +assertThat(PREVIOUS, instanceOf(NextPreviousType)) +assertThat(PREVIOUS, is(NextPreviousType.PREVIOUS)) + +assertThat(PlayPauseType, instanceOf(Class)) +assertThat(PlayPauseType.getCanonicalName(), is("org.openhab.core.library.types.PlayPauseType")) +assertThat(PLAY, instanceOf(PlayPauseType)) +assertThat(PLAY, is(PlayPauseType.PLAY)) +assertThat(PAUSE, instanceOf(PlayPauseType)) +assertThat(PAUSE, is(PlayPauseType.PAUSE)) + +assertThat(RewindFastforwardType, instanceOf(Class)) +assertThat(RewindFastforwardType.getCanonicalName(), is("org.openhab.core.library.types.RewindFastforwardType")) +assertThat(REWIND, instanceOf(RewindFastforwardType)) +assertThat(REWIND, is(RewindFastforwardType.REWIND)) +assertThat(FASTFORWARD, instanceOf(RewindFastforwardType)) +assertThat(FASTFORWARD, is(RewindFastforwardType.FASTFORWARD)) + +assertThat(QuantityType, instanceOf(Class)) +assertThat(QuantityType.getCanonicalName(), is("org.openhab.core.library.types.QuantityType")) + +assertThat(StringListType, instanceOf(Class)) +assertThat(StringListType.getCanonicalName(), is("org.openhab.core.library.types.StringListType")) + +assertThat(RawType, instanceOf(Class)) +assertThat(RawType.getCanonicalName(), is("org.openhab.core.library.types.RawType")) + +assertThat(DateTimeType, instanceOf(Class)) +assertThat(DateTimeType.getCanonicalName(), is("org.openhab.core.library.types.DateTimeType")) + +assertThat(DecimalType, instanceOf(Class)) +assertThat(DecimalType.getCanonicalName(), is("org.openhab.core.library.types.DecimalType")) + +assertThat(HSBType, instanceOf(Class)) +assertThat(HSBType.getCanonicalName(), is("org.openhab.core.library.types.HSBType")) + +assertThat(PercentType, instanceOf(Class)) +assertThat(PercentType.getCanonicalName(), is("org.openhab.core.library.types.PercentType")) + +assertThat(PointType, instanceOf(Class)) +assertThat(PointType.getCanonicalName(), is("org.openhab.core.library.types.PointType")) + +assertThat(StringType, instanceOf(Class)) +assertThat(StringType.getCanonicalName(), is("org.openhab.core.library.types.StringType")) + +assertThat(ImperialUnits, instanceOf(Class)) +assertThat(ImperialUnits.getCanonicalName(), is("org.openhab.core.library.unit.ImperialUnits")) + +assertThat(MetricPrefix, instanceOf(Class)) +assertThat(MetricPrefix.getCanonicalName(), is("org.openhab.core.library.unit.MetricPrefix")) + +assertThat(SIUnits, instanceOf(Class)) +assertThat(SIUnits.getCanonicalName(), is("org.openhab.core.library.unit.SIUnits")) + +assertThat(Units, instanceOf(Class)) +assertThat(Units.getCanonicalName(), is("org.openhab.core.library.unit.Units")) + +assertThat(BinaryPrefix, instanceOf(Class)) +assertThat(BinaryPrefix.getCanonicalName(), is("org.openhab.core.library.unit.BinaryPrefix")) + +assertThat(ChronoUnit, instanceOf(Class)) +assertThat(ChronoUnit.getCanonicalName(), is("java.time.temporal.ChronoUnit")) + +assertThat(DayOfWeek, instanceOf(Class)) +assertThat(DayOfWeek.getCanonicalName(), is("java.time.DayOfWeek")) + +assertThat(Duration, instanceOf(Class)) +assertThat(Duration.getCanonicalName(), is("java.time.Duration")) + +assertThat(Month, instanceOf(Class)) +assertThat(Month.getCanonicalName(), is("java.time.Month")) + +assertThat(ZoneId, instanceOf(Class)) +assertThat(ZoneId.getCanonicalName(), is("java.time.ZoneId")) + +assertThat(ZonedDateTime, instanceOf(Class)) +assertThat(ZonedDateTime.getCanonicalName(), is("java.time.ZonedDateTime"))