From 6c25e8b528cf5e56c8be8ffe320f53f22bfe00f7 Mon Sep 17 00:00:00 2001 From: jimtng <2554958+jimtng@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:04:52 +1000 Subject: [PATCH] [jrubyscripting] Add auto-require scripts option (#12381) Signed-off-by: Jimmy Tanagra --- .../README.md | 18 +- .../JRubyScriptEngineConfiguration.java | 181 ++++++++++-------- .../internal/JRubyScriptEngineFactory.java | 9 +- .../main/resources/OH-INF/config/config.xml | 56 +++--- .../resources/OH-INF/i18n/jruby.properties | 19 +- 5 files changed, 160 insertions(+), 123 deletions(-) diff --git a/bundles/org.openhab.automation.jrubyscripting/README.md b/bundles/org.openhab.automation.jrubyscripting/README.md index 6a459af28..13bab6397 100644 --- a/bundles/org.openhab.automation.jrubyscripting/README.md +++ b/bundles/org.openhab.automation.jrubyscripting/README.md @@ -14,7 +14,8 @@ Alternatively, JRuby configuration parameters may be set by creating a `jruby.cf | org.openhab.automation.jrubyscripting:rubylib | $OPENHAB_CONF/automation/lib/ruby/ | Search path for user libraries. Separate each path with a colon (semicolon in Windows). | | org.openhab.automation.jrubyscripting:local_context | singlethread | The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See [this](https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type) for options and details | | org.openhab.automation.jrubyscripting:local_variables | transient | Defines how variables are shared between Ruby and Java. See [this](https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options) for options and details | -| org.openhab.automation.jrubyscripting:gems | | Comma separated list of [Ruby Gems](https://rubygems.org/) to install. | +| org.openhab.automation.jrubyscripting:gems | | A comma separated list of [Ruby Gems](https://rubygems.org/) to install. | +| org.openhab.automation.jrubyscripting:require | | A comma separated list of script names to be required by the JRuby Scripting Engine at the beginning of user scripts. | ## Ruby Gems @@ -22,10 +23,11 @@ This automation add-on will install user specified gems and make them available Gem versions may be specified using the standard ruby gem_name=version format. The version number follows the [pessimistic version constraint](https://guides.rubygems.org/patterns/#pessimistic-version-constraint) syntax. -For example this configuration will install version 4 or higher of the [openHAB JRuby Scripting Library](https://boc-tothefuture.github.io/openhab-jruby/). +For example this configuration will install the latest version of the [openHAB JRuby Scripting Library](https://boc-tothefuture.github.io/openhab-jruby/), and instruct the scripting engine to automatically insert `require 'openhab'` at the start of the script. ```text -org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>4.0 +org.openhab.automation.jrubyscripting:gems=openhab-scripting +org.openhab.automation.jrubyscripting:require=openhab ``` ## Creating JRuby Scripts @@ -51,8 +53,8 @@ log:set DEBUG org.openhab.automation.jrubyscripting All [ScriptExtensions]({{base}}/configuration/jsr223.html#scriptextension-objects-all-jsr223-languages) are available in JRuby with the following exceptions/modifications: -- The File variable, referencing java.io.File is not available as it conflicts with Ruby's File class preventing Ruby from initializing -- Globals scriptExtension, automationManager, ruleRegistry, items, voice, rules, things, events, itemRegistry, ir, actions, se, audio, lifecycleTracker are prepended with a $ (e.g. $automationManager) making them available as a global objects in Ruby. +- The `File` variable, referencing `java.io.File` is not available as it conflicts with Ruby's File class preventing Ruby from initializing +- Globals `scriptExtension`, `automationManager`, `ruleRegistry`, `items`, `voice`, `rules`, `things`, `events`, `itemRegistry`, `ir`, `actions`, `se`, `audio`, `lifecycleTracker` are prepended with a `$` (e.g. `$automationManager`) making them available as global objects in Ruby. ## Script Examples @@ -65,8 +67,10 @@ The openHAB server uses the [SLF4J](https://www.slf4j.org/) library for logging. require 'java' java_import org.slf4j.LoggerFactory -LoggerFactory.getLogger("org.openhab.core.automation.examples").info("Hello world!") +LoggerFactory.getLogger("org.openhab.automation.examples").info("Hello world!") ``` JRuby can [import Java classes](https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby). -Depending on the openHAB logging configuration, you may need to prefix logger names with `org.openhab.core.automation` for them to show up in the log file (or you modify the logging configuration). +Depending on the openHAB logging configuration, you may need to prefix logger names with `org.openhab.automation` for them to show up in the log file (or you modify the logging configuration). + +**Note**: Installing the [JRuby Scripting Library](https://boc-tothefuture.github.io/openhab-jruby/installation/) will provide enhanced capabilities with simpler rule syntax. diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java index 5a38184e3..9e8b8ea1f 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; @@ -35,6 +36,7 @@ import org.slf4j.LoggerFactory; * Processes JRuby Configuration Parameters. * * @author Brian O'Connell - Initial contribution + * @author Jimmy Tanagra - Add $LOAD_PATH, require injection */ @NonNullByDefault public class JRubyScriptEngineConfiguration { @@ -48,6 +50,8 @@ public class JRubyScriptEngineConfiguration { private static final String GEM_HOME = "gem_home"; private static final String RUBYLIB = "rubylib"; + private static final String GEMS = "gems"; + private static final String REQUIRE = "require"; // Map of configuration parameters private static final Map CONFIGURATION_PARAMETERS = Map.ofEntries( @@ -67,7 +71,10 @@ public class JRubyScriptEngineConfiguration { new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()), - Map.entry("gems", new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build())); + Map.entry(GEMS, new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()), + + Map.entry(REQUIRE, + new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.REQUIRE).build())); private static final Map> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS .values().stream().collect(Collectors.groupingBy(v -> v.type)); @@ -105,16 +112,11 @@ public class JRubyScriptEngineConfiguration { * @param factory Script Engine to configure */ void configureScriptEngine(ScriptEngineFactory factory) { - configureSystemProperties(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.SYSTEM_PROPERTY, - Collections. emptyList())); + configureSystemProperties(); ScriptEngine engine = factory.getScriptEngine(); - - configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT, - Collections. emptyList()), engine); - - configureGems(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.GEM, - Collections. emptyList()), engine); + configureRubyEnvironment(engine); + configureGems(engine); } /** @@ -122,96 +124,111 @@ public class JRubyScriptEngineConfiguration { */ private void ensureGemHomeExists() { OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME); - if (gemHomeConfigElement != null) { - Optional gemHome = gemHomeConfigElement.getValue(); - if (gemHome.isPresent()) { - File gemHomeDirectory = new File(gemHome.get()); - if (!gemHomeDirectory.exists()) { - logger.debug("gem_home directory does not exist, creating"); - if (!gemHomeDirectory.mkdirs()) { - logger.debug("Error creating gem_home direcotry"); - } + if (gemHomeConfigElement == null) { + return; + } + Optional gemHome = gemHomeConfigElement.getValue(); + if (gemHome.isPresent()) { + File gemHomeDirectory = new File(gemHome.get()); + if (!gemHomeDirectory.exists()) { + logger.debug("gem_home directory does not exist, creating"); + if (!gemHomeDirectory.mkdirs()) { + logger.warn("Error creating gem_home directory"); } - } else { - logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists"); } + } else { + logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists"); } } /** * Install a gems in ScriptEngine * - * @param gemsDirectives List of gems to install * @param engine Engine to install gems */ - private synchronized void configureGems(List gemDirectives, ScriptEngine engine) { - for (OptionalConfigurationElement gemDirective : gemDirectives) { - if (gemDirective.getValue().isPresent()) { - ensureGemHomeExists(); + private synchronized void configureGems(ScriptEngine engine) { + ensureGemHomeExists(); - String[] gems = gemDirective.getValue().get().split(","); - for (String gem : gems) { - gem = gem.trim(); - String gemCommand; - if (gem.contains("=")) { - String[] gemParts = gem.split("="); - gem = gemParts[0]; - String version = gemParts[1]; - gemCommand = "Gem.install('" + gem + "',version='" + version + "')\n"; - } else { - gemCommand = "Gem.install('" + gem + "')\n"; - } + OptionalConfigurationElement gemsConfigElement = CONFIGURATION_PARAMETERS.get(GEMS); + if (gemsConfigElement == null || !gemsConfigElement.getValue().isPresent()) { + return; + } - try { - logger.debug("Installing Gem: {} ", gem); - logger.trace("Gem install code:\n{}\n", gemCommand); - engine.eval(gemCommand); - } catch (Exception e) { - logger.error("Error installing Gem", e); - } - } + String[] gems = gemsConfigElement.getValue().get().split(","); + for (String gem : gems) { + gem = gem.trim(); + String version = ""; + String gemCommand; + if (gem.contains("=")) { + String[] gemParts = gem.split("="); + gem = gemParts[0].trim(); + version = gemParts[1].trim(); + } + + if (gem.isEmpty()) { + continue; + } else if (version.isEmpty()) { + gemCommand = "Gem.install('" + gem + "')\n"; } else { - logger.debug("Ruby gem property has no value"); + gemCommand = "Gem.install('" + gem + "', '" + version + "')\n"; + } + + try { + logger.debug("Installing Gem: {}", gem); + logger.trace("Gem install code:\n{}\n", gemCommand); + engine.eval(gemCommand); + } catch (ScriptException e) { + logger.warn("Error installing Gem: {}", e.getMessage()); + } catch (BootstrapMethodError e) { + logger.warn("Error while checking/installing gems: {}. You may need to restart OpenHAB", + e.getMessage()); + logger.debug("Error in configureGems", e); } } } /** - * Configure the base Ruby Environment + * Execute ruby require statement in the ScriptEngine * - * @param engine Engine to configure + * @param engine Engine to insert the require statements */ - public ScriptEngine configureRubyEnvironment(ScriptEngine engine) { - configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT, - Collections. emptyList()), engine); - return engine; + public void injectRequire(ScriptEngine engine) { + OptionalConfigurationElement requireConfigElement = CONFIGURATION_PARAMETERS.get(REQUIRE); + if (requireConfigElement == null || !requireConfigElement.getValue().isPresent()) { + return; + } + + String[] scripts = requireConfigElement.getValue().get().split(","); + for (String script : scripts) { + final String requireStatement = String.format("require '%s'", script.trim()); + try { + logger.trace("Injecting require statement: {}", requireStatement); + engine.eval(requireStatement); + } catch (ScriptException e) { + logger.warn("Error evaluating statement {}: {}", requireStatement, e.getMessage()); + } + } } /** * Configure the optional elements of the Ruby Environment * - * @param optionalConfigurationElements Optional elements to configure in the ruby environment * @param engine Engine in which to configure environment */ - private void configureRubyEnvironment(List optionalConfigurationElements, - ScriptEngine engine) { - for (OptionalConfigurationElement configElement : optionalConfigurationElements) { - String environmentProperty = configElement.mappedTo().get(); - if (configElement.getValue().isPresent()) { - String environmentSetting = "ENV['" + environmentProperty + "']='" + configElement.getValue().get() - + "'"; - try { - logger.trace("Setting Ruby environment with code: {} ", environmentSetting); - engine.eval(environmentSetting); - } catch (ScriptException e) { - logger.error("Error setting ruby environment", e); - } - } else { - logger.debug("Ruby environment property ({}) has no value", environmentProperty); + public ScriptEngine configureRubyEnvironment(ScriptEngine engine) { + getConfigurationElements(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT).forEach(configElement -> { + final String environmentSetting = String.format("ENV['%s']='%s'", configElement.mappedTo().get(), + configElement.getValue().get()); + try { + logger.trace("Setting Ruby environment with code: {} ", environmentSetting); + engine.eval(environmentSetting); + } catch (ScriptException e) { + logger.warn("Error setting ruby environment", e); } - } + }); configureRubyLib(engine); + return engine; } /** @@ -244,17 +261,20 @@ public class JRubyScriptEngineConfiguration { * * @param optionalConfigurationElements Optional system properties to configure */ - private void configureSystemProperties(List optionalConfigurationElements) { - for (OptionalConfigurationElement configElement : optionalConfigurationElements) { + private void configureSystemProperties() { + getConfigurationElements(OptionalConfigurationElement.Type.SYSTEM_PROPERTY).forEach(configElement -> { String systemProperty = configElement.mappedTo().get(); - if (configElement.getValue().isPresent()) { - String propertyValue = configElement.getValue().get(); - logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue); - System.setProperty(systemProperty, propertyValue); - } else { - logger.warn("System property ({}) has no value", systemProperty); - } - } + String propertyValue = configElement.getValue().get(); + logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue); + System.setProperty(systemProperty, propertyValue); + }); + } + + private Stream getConfigurationElements( + OptionalConfigurationElement.Type configurationType) { + return CONFIGURATION_TYPE_MAP + .getOrDefault(configurationType, Collections. emptyList()).stream() + .filter(element -> element.getValue().isPresent()); } /** @@ -289,7 +309,8 @@ public class JRubyScriptEngineConfiguration { private enum Type { SYSTEM_PROPERTY, RUBY_ENVIRONMENT, - GEM + GEM, + REQUIRE } private static class Builder { diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java index 51ad71983..c903ab78e 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java @@ -35,6 +35,7 @@ import org.osgi.service.component.annotations.Modified; * This is an implementation of a {@link ScriptEngineFactory} for Ruby. * * @author Brian O'Connell - Initial contribution + * @author Jimmy Tanagra - Add require injection */ @NonNullByDefault @Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.automation.jrubyscripting") @@ -45,7 +46,7 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory { // Filter out the File entry to prevent shadowing the Ruby File class which breaks Ruby in spectacularly // difficult ways to debug. - private static final Set FILTERED_PRESETS = Set.of("File"); + private static final Set FILTERED_PRESETS = Set.of("File", "Files", "Path", "Paths"); private static final Set INSTANCE_PRESETS = Set.of(); private static final Set GLOBAL_PRESETS = Set.of("scriptExtension", "automationManager", "ruleRegistry", "items", "voice", "rules", "things", "events", "itemRegistry", "ir", "actions", "se", "audio", @@ -113,6 +114,12 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory { importClassesToRuby(scriptEngine, partitionedMap.getOrDefault(true, new HashMap<>())); super.scopeValues(scriptEngine, partitionedMap.getOrDefault(false, new HashMap<>())); + + // scopeValues is called twice. The first call only passed 'se'. The second call passed the rest of the + // presets, including 'ir'. We wait for the second call before running the require statements. + if (scopeValues.containsKey("ir")) { + configuration.injectRequire(scriptEngine); + } } private void importClassesToRuby(ScriptEngine scriptEngine, Map objects) { diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml index b0605b4d2..4d9427e17 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml @@ -6,24 +6,44 @@ https://openhab.org/schemas/config-description-1.0.0.xsd"> - - - This group defines JRuby system properties. - true + + + This group defines the list of Ruby Gems to install. This group defines Ruby's environment. - false - - - This group defines the list of Ruby Gems to install. - false + + + This group defines JRuby system properties. + + + A comma separated list of Ruby Gems to install. + + + + + A comma separated list of script names to be required by the JRuby Scripting Engine before running user + scripts. + + + + + Location Ruby Gems will be installed and loaded, directory will be created if missing and gem installs + are specified. Defaults to "OPENHAB_CONF/scripts/lib/ruby/gem_home" when not specified. + + + + + Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to + "OPENHAB_CONF/automation/lib/ruby" when not specified. + + The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See @@ -35,6 +55,7 @@ + true @@ -47,24 +68,9 @@ + true - - - Location Ruby Gems will be installed and loaded, directory will be created if missing and gem installs - are specified - - - - - - Search path for user libraries. Separate each path with a colon (semicolon in Windows). - - - - - Comma separated list of Ruby Gems to install. - diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties index 97e140816..989c8e5e9 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties @@ -1,14 +1,7 @@ - -# service - -service.automation.jrubyscripting.label = JRuby Scripting - -# bundle config - automation.config.jruby.gem_home.label = GEM_HOME -automation.config.jruby.gem_home.description = Location Ruby Gems will be installed and loaded, directory will be created if missing and gem installs are specified +automation.config.jruby.gem_home.description = Location Ruby Gems will be installed and loaded, directory will be created if missing and gem installs are specified. Defaults to "OPENHAB_CONF/scripts/lib/ruby/gem_home" when not specified. automation.config.jruby.gems.label = Ruby Gems -automation.config.jruby.gems.description = Comma separated list of Ruby Gems to install. +automation.config.jruby.gems.description = A comma separated list of Ruby Gems to install. automation.config.jruby.group.environment.label = Ruby Environment automation.config.jruby.group.environment.description = This group defines Ruby's environment. automation.config.jruby.group.gems.label = Ruby Gems @@ -26,5 +19,11 @@ automation.config.jruby.local_variable.description = Defines how variables are s automation.config.jruby.local_variable.option.transient = Transient automation.config.jruby.local_variable.option.persistent = Persistent automation.config.jruby.local_variable.option.global = Global +automation.config.jruby.require.label = Require Scripts +automation.config.jruby.require.description = A comma separated list of script names to be required by the JRuby Scripting Engine before running user scripts. automation.config.jruby.rubylib.label = RUBYLIB -automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). +automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "OPENHAB_CONF/automation/lib/ruby" when not specified. + +# service + +service.automation.jrubyscripting.label = JRuby Scripting