[jrubyscripting] Add auto-require scripts option (#12381)

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng 2022-02-28 18:04:52 +10:00 committed by GitHub
parent d56b2e4dbe
commit 6c25e8b528
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 160 additions and 123 deletions

View File

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

View File

@ -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<String, OptionalConfigurationElement> 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<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> 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.<OptionalConfigurationElement> emptyList()));
configureSystemProperties();
ScriptEngine engine = factory.getScriptEngine();
configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
Collections.<OptionalConfigurationElement> emptyList()), engine);
configureGems(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.GEM,
Collections.<OptionalConfigurationElement> 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<String> 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<String> 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<OptionalConfigurationElement> 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.<OptionalConfigurationElement> 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<OptionalConfigurationElement> 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<OptionalConfigurationElement> 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<OptionalConfigurationElement> getConfigurationElements(
OptionalConfigurationElement.Type configurationType) {
return CONFIGURATION_TYPE_MAP
.getOrDefault(configurationType, Collections.<OptionalConfigurationElement> 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 {

View File

@ -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<String> FILTERED_PRESETS = Set.of("File");
private static final Set<String> FILTERED_PRESETS = Set.of("File", "Files", "Path", "Paths");
private static final Set<String> INSTANCE_PRESETS = Set.of();
private static final Set<String> 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<String, Object> objects) {

View File

@ -6,24 +6,44 @@
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="automation:jruby">
<parameter-group name="system">
<label>System Properties</label>
<description>This group defines JRuby system properties.</description>
<advanced>true</advanced>
<parameter-group name="gems">
<label>Ruby Gems</label>
<description>This group defines the list of Ruby Gems to install.</description>
</parameter-group>
<parameter-group name="environment">
<label>Ruby Environment</label>
<description>This group defines Ruby's environment.</description>
<advanced>false</advanced>
</parameter-group>
<parameter-group name="gems">
<label>Ruby Gems</label>
<description>This group defines the list of Ruby Gems to install.</description>
<advanced>false</advanced>
<parameter-group name="system">
<label>System Properties</label>
<description>This group defines JRuby system properties.</description>
</parameter-group>
<parameter name="gems" type="text" required="false" groupName="gems">
<label>Ruby Gems</label>
<description>A comma separated list of Ruby Gems to install.</description>
</parameter>
<parameter name="require" type="text" required="false" groupName="gems">
<label>Require Scripts</label>
<description>A comma separated list of script names to be required by the JRuby Scripting Engine before running user
scripts.</description>
</parameter>
<parameter name="gem_home" type="text" required="false" groupName="environment">
<label>GEM_HOME</label>
<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.</description>
</parameter>
<parameter name="rubylib" type="text" required="false" groupName="environment">
<label>RUBYLIB</label>
<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.</description>
</parameter>
<parameter name="local_context" type="text" required="false" groupName="system">
<label>Context Instance Type</label>
<description>The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See
@ -35,6 +55,7 @@
<option value="singlethread">SingleThread</option>
<option value="concurrent">Concurrent</option>
</options>
<advanced>true</advanced>
</parameter>
<parameter name="local_variable" type="text" required="false" groupName="system">
@ -47,24 +68,9 @@
<option value="persistent">Persistent</option>
<option value="global">Global</option>
</options>
<advanced>true</advanced>
</parameter>
<parameter name="gem_home" type="text" required="false" groupName="environment">
<label>GEM_HOME</label>
<description>Location Ruby Gems will be installed and loaded, directory will be created if missing and gem installs
are specified</description>
<default></default>
</parameter>
<parameter name="rubylib" type="text" required="false" groupName="environment">
<label>RUBYLIB</label>
<description>Search path for user libraries. Separate each path with a colon (semicolon in Windows).</description>
</parameter>
<parameter name="gems" type="text" required="false" groupName="gems">
<label>Ruby Gems</label>
<description>Comma separated list of Ruby Gems to install.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

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