]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jrubyscripting] JRuby Scripting initial contribution (#11538)
authorboc-tothefuture <broconne+github@gmail.com>
Mon, 15 Nov 2021 13:21:29 +0000 (08:21 -0500)
committerGitHub <noreply@github.com>
Mon, 15 Nov 2021 13:21:29 +0000 (14:21 +0100)
Also-by: Jimmy Tanagra <jimmy@tanagra.id.au>
Signed-off-by: Brian O'Connell <broconne@gmail.com>
12 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.automation.jrubyscripting/NOTICE [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/README.md [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/pom.xml [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/package-info.java [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties [new file with mode: 0644]
bundles/pom.xml

index a74a451fe98079805ca7058669cb719e5c44c717..6f004ed451fea3ab75c7850f56bffec91710fabd 100644 (file)
@@ -6,6 +6,7 @@
 
 # Add-on maintainers:
 /bundles/org.openhab.automation.groovyscripting/ @wborn
+/bundles/org.openhab.automation.jrubyscripting/ @boc-tothefuture
 /bundles/org.openhab.automation.jsscripting/ @jpg0
 /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers
 /bundles/org.openhab.automation.pidcontroller/ @fwolter
index 67288274817e9859a673dc0c91d81d3e4a266611..6646e23c03d0c7075e6c9d6846d5f0c6adc08d35 100644 (file)
       <artifactId>org.openhab.automation.groovyscripting</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.automation.jrubyscripting</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.automation.jsscripting</artifactId>
diff --git a/bundles/org.openhab.automation.jrubyscripting/NOTICE b/bundles/org.openhab.automation.jrubyscripting/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.automation.jrubyscripting/README.md b/bundles/org.openhab.automation.jrubyscripting/README.md
new file mode 100644 (file)
index 0000000..f9cf01c
--- /dev/null
@@ -0,0 +1,72 @@
+# JRuby Scripting
+
+This add-on provides [JRuby](https://www.jruby.org/) 9.3.1 that can be used as a scripting language within automation rules.
+
+## JRuby Scripting Configuration
+
+After installing this add-on, you will find configuration options in the openHAB portal under _Settings -> Other Services -> JRuby Scripting_.
+
+Alternatively, JRuby configuration parameters may be set by creating a `jruby.cfg` file in `conf/services/`
+
+| Parameter                                             | Default                                 | Description                                                                                                                                                                                                 |
+| ----------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| org.openhab.automation.jrubyscripting:gem_home        | $OPENHAB_CONF/scripts/lib/ruby/gem_home | Location ruby gems will be installed and loaded, directory will be created if missing and gem installs are specified                                                                                        |
+| 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.                                                                                                                                      |
+
+## Ruby Gems
+
+This automation add-on will install user specified gems and make them available on the library search path.
+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/).
+
+```text
+org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>4.0
+```
+
+## Creating JRuby Scripts
+
+When this add-on is installed, you can select JRuby as a scripting language when creating a script action within the rule editor of the UI.
+
+Alternatively, you can create scripts in the `automation/jsr223/ruby/personal` configuration directory.
+If you create an empty file called `test.rb`, you will see a log line with information similar to:
+
+```text
+    ... [INFO ] [.a.m.s.r.i.l.ScriptFileWatcher:150  ] - Loading script 'test.rb'
+```
+
+To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to
+enable debug logging for the automation functionality:
+
+```text
+log:set DEBUG org.openhab.core.automation
+log:set DEBUG org.openhab.automation.jrubyscripting
+```
+
+## Imports
+
+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.
+
+## Script Examples
+
+JRuby scripts provide access to almost all the functionality in an openHAB runtime environment.
+As a simple example, the following script logs "Hello, World!".
+Note that `puts` will usually not work since the output has no terminal to display the text.
+The openHAB server uses the [SLF4J](https://www.slf4j.org/) library for logging.
+
+```ruby
+require 'java'
+java_import org.slf4j.LoggerFactory
+
+LoggerFactory.getLogger("org.openhab.core.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).
diff --git a/bundles/org.openhab.automation.jrubyscripting/pom.xml b/bundles/org.openhab.automation.jrubyscripting/pom.xml
new file mode 100644 (file)
index 0000000..b5cd89e
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.automation.jrubyscripting</artifactId>
+
+  <name>openHAB Add-ons :: Automation :: JRuby Scripting</name>
+
+  <properties>
+    <bnd.importpackage>com.sun.nio.*;resolution:=optional,com.sun.security.*;resolution:=optional,org.apache.tools.ant.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.joda.*;resolution:=optional,sun.management.*;resolution:=optional,sun.nio.*;resolution:=optional</bnd.importpackage>
+    <jruby.version>9.3.1.0</jruby.version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.jruby</groupId>
+      <artifactId>jruby-complete</artifactId>
+      <version>${jruby.version}</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml b/bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..2ef4f18
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.automation.jrubyscripting-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-automation-jrubyscripting" description="JRuby Scripting" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.jrubyscripting/${project.version}</bundle>
+       </feature>
+</features>
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
new file mode 100644 (file)
index 0000000..df0db8c
--- /dev/null
@@ -0,0 +1,291 @@
+/**
+ * Copyright (c) 2010-2021 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.jrubyscripting.internal;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineFactory;
+import javax.script.ScriptException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.OpenHAB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Processes JRuby Configuration Parameters.
+ *
+ * @author Brian O'Connell - Initial contribution
+ */
+@NonNullByDefault
+public class JRubyScriptEngineConfiguration {
+
+    private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class);
+
+    private static final Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby",
+            "gem_home");
+
+    private static final Path DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "lib", "ruby");
+
+    private static final String GEM_HOME = "gem_home";
+
+    // Map of configuration parameters
+    private static final Map<String, OptionalConfigurationElement> CONFIGURATION_PARAMETERS = Map.ofEntries(
+            Map.entry("local_context",
+                    new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
+                            .mappedTo("org.jruby.embed.localcontext.scope").defaultValue("singlethread").build()),
+
+            Map.entry("local_variable",
+                    new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
+                            .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()),
+
+            Map.entry(GEM_HOME,
+                    new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
+                            .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()),
+
+            Map.entry("rubylib",
+                    new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
+                            .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()),
+
+            Map.entry("gems", new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()));
+
+    private static final Map<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS
+            .values().stream().collect(Collectors.groupingBy(v -> v.type));
+
+    /**
+     * Update configuration
+     * 
+     * @param config Configuration parameters to apply to ScriptEngine
+     * @param factory ScriptEngineFactory to configure
+     */
+    void update(Map<String, Object> config, ScriptEngineFactory factory) {
+        logger.trace("JRuby Script Engine Configuration: {}", config);
+        config.forEach(this::processConfigValue);
+        configureScriptEngine(factory);
+    }
+
+    /**
+     * Apply configuration key/value to known configuration parameters
+     * 
+     * @param key Configuration key
+     * @param value Configuration value
+     */
+    private void processConfigValue(String key, Object value) {
+        OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key);
+        if (configurationElement != null) {
+            configurationElement.setValue(value.toString());
+        } else {
+            logger.debug("Ignoring unexpected configuration key: {}", key);
+        }
+    }
+
+    /**
+     * Configure the ScriptEngine
+     * 
+     * @param factory Script Engine to configure
+     */
+    void configureScriptEngine(ScriptEngineFactory factory) {
+        configureSystemProperties(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.SYSTEM_PROPERTY,
+                Collections.<OptionalConfigurationElement> emptyList()));
+
+        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);
+    }
+
+    /**
+     * Makes Gem home directory if it does not exist
+     */
+    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");
+                    }
+                }
+            } 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();
+
+                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";
+                    }
+
+                    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);
+                    }
+                }
+            } else {
+                logger.debug("Ruby gem property has no value");
+            }
+        }
+    }
+
+    /**
+     * Configure the base Ruby Environment
+     * 
+     * @param engine Engine to configure
+     */
+    public ScriptEngine configureRubyEnvironment(ScriptEngine engine) {
+        configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
+                Collections.<OptionalConfigurationElement> emptyList()), engine);
+        return engine;
+    }
+
+    /**
+     * 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);
+            }
+        }
+    }
+
+    /**
+     * Configure system properties
+     * 
+     * @param optionalConfigurationElements Optional system properties to configure
+     */
+    private void configureSystemProperties(List<OptionalConfigurationElement> optionalConfigurationElements) {
+        for (OptionalConfigurationElement configElement : optionalConfigurationElements) {
+            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);
+            }
+        }
+    }
+
+    /**
+     * Inner static companion class for configuration elements
+     */
+    private static class OptionalConfigurationElement {
+
+        private final Optional<String> defaultValue;
+        private final Optional<String> mappedTo;
+        private final Type type;
+        private Optional<String> value;
+
+        private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) {
+            this.type = type;
+            this.defaultValue = Optional.ofNullable(defaultValue);
+            this.mappedTo = Optional.ofNullable(mappedTo);
+            value = Optional.empty();
+        }
+
+        private Optional<String> getValue() {
+            return value.or(() -> defaultValue);
+        }
+
+        private void setValue(String value) {
+            this.value = Optional.of(value);
+        }
+
+        private Optional<String> mappedTo() {
+            return mappedTo;
+        }
+
+        private enum Type {
+            SYSTEM_PROPERTY,
+            RUBY_ENVIRONMENT,
+            GEM
+        }
+
+        private static class Builder {
+            private final Type type;
+            private @Nullable String defaultValue = null;
+            private @Nullable String mappedTo = null;
+
+            private Builder(Type type) {
+                this.type = type;
+            }
+
+            private Builder mappedTo(String mappedTo) {
+                this.mappedTo = mappedTo;
+                return this;
+            }
+
+            private Builder defaultValue(String value) {
+                this.defaultValue = value;
+                return this;
+            }
+
+            private OptionalConfigurationElement build() {
+                return new OptionalConfigurationElement(type, mappedTo, defaultValue);
+            }
+        }
+    }
+}
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
new file mode 100644 (file)
index 0000000..cefa9b3
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2021 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.jrubyscripting.internal;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.script.ScriptEngine;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.automation.module.script.AbstractScriptEngineFactory;
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.openhab.core.config.core.ConfigurableService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+
+/**
+ * This is an implementation of a {@link ScriptEngineFactory} for Ruby.
+ *
+ * @author Brian O'Connell - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.automation.jrubyscripting")
+@ConfigurableService(category = "automation", label = "JRuby Scripting", description_uri = "automation:jruby")
+public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
+
+    private final JRubyScriptEngineConfiguration configuration = new JRubyScriptEngineConfiguration();
+
+    // 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> 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",
+            "lifecycleTracker");
+
+    private final javax.script.ScriptEngineFactory factory = new org.jruby.embed.jsr223.JRubyEngineFactory();
+
+    private final List<String> scriptTypes = Stream
+            .concat(factory.getExtensions().stream(), factory.getMimeTypes().stream())
+            .collect(Collectors.toUnmodifiableList());
+
+    // Adds @ in front of a set of variables so that Ruby recogonizes them as instance variables
+    private static Map.Entry<String, Object> mapInstancePresets(Map.Entry<String, Object> entry) {
+        if (INSTANCE_PRESETS.contains(entry.getKey())) {
+            return Map.entry("@" + entry.getKey(), entry.getValue());
+        } else {
+            return entry;
+        }
+    }
+
+    // Adds $ in front of a set of variables so that Ruby recogonizes them as global variables
+    private static Map.Entry<String, Object> mapGlobalPresets(Map.Entry<String, Object> entry) {
+        if (GLOBAL_PRESETS.contains(entry.getKey())) {
+            return Map.entry("$" + entry.getKey(), entry.getValue());
+        } else {
+            return entry;
+        }
+    }
+
+    // The activate call activates the automation and sets its configuration
+    @Activate
+    protected void activate(Map<String, Object> config) {
+        configuration.update(config, factory);
+    }
+
+    // The modified call updates configuration for the automation
+    @Modified
+    protected void modified(Map<String, Object> config) {
+        configuration.update(config, factory);
+    }
+
+    @Override
+    public List<String> getScriptTypes() {
+        return scriptTypes;
+    }
+
+    @Override
+    public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValues) {
+        // Empty comments prevent the formatter from breaking up the correct streams chaining
+        Map<String, Object> filteredScopeValues = //
+                scopeValues //
+                        .entrySet() //
+                        .stream() //
+                        .filter(map -> !FILTERED_PRESETS.contains(map.getKey())) //
+                        .map(JRubyScriptEngineFactory::mapInstancePresets) //
+                        .map(JRubyScriptEngineFactory::mapGlobalPresets) //
+                        .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); //
+
+        super.scopeValues(scriptEngine, filteredScopeValues);
+    }
+
+    @Override
+    public @Nullable ScriptEngine createScriptEngine(String scriptType) {
+        return scriptTypes.contains(scriptType) ? configuration.configureRubyEnvironment(factory.getScriptEngine())
+                : null;
+    }
+}
diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/package-info.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/package-info.java
new file mode 100644 (file)
index 0000000..0bd16bf
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2010-2021 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
+ */
+
+@org.osgi.annotation.bundle.Header(name = org.osgi.framework.Constants.DYNAMICIMPORT_PACKAGE, value = "*")
+package org.openhab.automation.jrubyscripting.internal;
+
+/**
+ * Additional information for the JRuby Scripting package
+ *
+ * @author Brian O'Connell - Initial contribution
+ */
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
new file mode 100644 (file)
index 0000000..b0605b4
--- /dev/null
@@ -0,0 +1,70 @@
+<?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="automation:jruby">
+
+               <parameter-group name="system">
+                       <label>System Properties</label>
+                       <description>This group defines JRuby system properties.</description>
+                       <advanced>true</advanced>
+               </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>
+
+               <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
+                               https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type for options and details.</description>
+                       <default>singlethread</default>
+                       <options>
+                               <option value="singleton">Singleton</option>
+                               <option value="threadsafe">ThreadSafe</option>
+                               <option value="singlethread">SingleThread</option>
+                               <option value="concurrent">Concurrent</option>
+                       </options>
+               </parameter>
+
+               <parameter name="local_variable" type="text" required="false" groupName="system">
+                       <label>Local Variable Behavior</label>
+                       <description>Defines how variables are shared between Ruby and Java. See
+                               https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options for options and details.</description>
+                       <default>transient</default>
+                       <options>
+                               <option value="transient">Transient</option>
+                               <option value="persistent">Persistent</option>
+                               <option value="global">Global</option>
+                       </options>
+               </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>
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
new file mode 100644 (file)
index 0000000..97e1408
--- /dev/null
@@ -0,0 +1,30 @@
+
+# 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.gems.label = Ruby Gems
+automation.config.jruby.gems.description = 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
+automation.config.jruby.group.gems.description = This group defines the list of Ruby Gems to install.
+automation.config.jruby.group.system.label = System Properties
+automation.config.jruby.group.system.description = This group defines JRuby system properties.
+automation.config.jruby.local_context.label = Context Instance Type
+automation.config.jruby.local_context.description = The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type for options and details.
+automation.config.jruby.local_context.option.singleton = Singleton
+automation.config.jruby.local_context.option.threadsafe = ThreadSafe
+automation.config.jruby.local_context.option.singlethread = SingleThread
+automation.config.jruby.local_context.option.concurrent = Concurrent
+automation.config.jruby.local_variable.label = Local Variable Behavior
+automation.config.jruby.local_variable.description = Defines how variables are shared between Ruby and Java. See https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options for options and details.
+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.rubylib.label = RUBYLIB
+automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows).
index 345fc9daecd35afc5b28e1c6adfdc4991756a331..9cc1c10accae58bdadc4aab4d7baca57334808e7 100644 (file)
@@ -19,6 +19,7 @@
   <modules>
     <!-- automation -->
     <module>org.openhab.automation.groovyscripting</module>
+    <module>org.openhab.automation.jrubyscripting</module>
     <module>org.openhab.automation.jsscripting</module>
     <module>org.openhab.automation.jythonscripting</module>
     <module>org.openhab.automation.pidcontroller</module>