2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.automation.jrubyscripting.internal;
16 import java.nio.file.Path;
17 import java.nio.file.Paths;
18 import java.util.Collections;
19 import java.util.List;
21 import java.util.Optional;
22 import java.util.stream.Collectors;
24 import javax.script.ScriptEngine;
25 import javax.script.ScriptEngineFactory;
26 import javax.script.ScriptException;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.OpenHAB;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * Processes JRuby Configuration Parameters.
37 * @author Brian O'Connell - Initial contribution
40 public class JRubyScriptEngineConfiguration {
42 private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class);
44 private static final Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby",
47 private static final Path DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "lib", "ruby");
49 private static final String GEM_HOME = "gem_home";
50 private static final String RUBYLIB = "rubylib";
52 // Map of configuration parameters
53 private static final Map<String, OptionalConfigurationElement> CONFIGURATION_PARAMETERS = Map.ofEntries(
54 Map.entry("local_context",
55 new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
56 .mappedTo("org.jruby.embed.localcontext.scope").defaultValue("singlethread").build()),
58 Map.entry("local_variable",
59 new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
60 .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()),
63 new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
64 .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()),
67 new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
68 .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()),
70 Map.entry("gems", new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()));
72 private static final Map<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS
73 .values().stream().collect(Collectors.groupingBy(v -> v.type));
76 * Update configuration
78 * @param config Configuration parameters to apply to ScriptEngine
79 * @param factory ScriptEngineFactory to configure
81 void update(Map<String, Object> config, ScriptEngineFactory factory) {
82 logger.trace("JRuby Script Engine Configuration: {}", config);
83 config.forEach(this::processConfigValue);
84 configureScriptEngine(factory);
88 * Apply configuration key/value to known configuration parameters
90 * @param key Configuration key
91 * @param value Configuration value
93 private void processConfigValue(String key, Object value) {
94 OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key);
95 if (configurationElement != null) {
96 configurationElement.setValue(value.toString());
98 logger.debug("Ignoring unexpected configuration key: {}", key);
103 * Configure the ScriptEngine
105 * @param factory Script Engine to configure
107 void configureScriptEngine(ScriptEngineFactory factory) {
108 configureSystemProperties(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.SYSTEM_PROPERTY,
109 Collections.<OptionalConfigurationElement> emptyList()));
111 ScriptEngine engine = factory.getScriptEngine();
113 configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
114 Collections.<OptionalConfigurationElement> emptyList()), engine);
116 configureGems(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.GEM,
117 Collections.<OptionalConfigurationElement> emptyList()), engine);
121 * Makes Gem home directory if it does not exist
123 private void ensureGemHomeExists() {
124 OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME);
125 if (gemHomeConfigElement != null) {
126 Optional<String> gemHome = gemHomeConfigElement.getValue();
127 if (gemHome.isPresent()) {
128 File gemHomeDirectory = new File(gemHome.get());
129 if (!gemHomeDirectory.exists()) {
130 logger.debug("gem_home directory does not exist, creating");
131 if (!gemHomeDirectory.mkdirs()) {
132 logger.debug("Error creating gem_home direcotry");
136 logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists");
142 * Install a gems in ScriptEngine
144 * @param gemsDirectives List of gems to install
145 * @param engine Engine to install gems
147 private synchronized void configureGems(List<OptionalConfigurationElement> gemDirectives, ScriptEngine engine) {
148 for (OptionalConfigurationElement gemDirective : gemDirectives) {
149 if (gemDirective.getValue().isPresent()) {
150 ensureGemHomeExists();
152 String[] gems = gemDirective.getValue().get().split(",");
153 for (String gem : gems) {
156 if (gem.contains("=")) {
157 String[] gemParts = gem.split("=");
159 String version = gemParts[1];
160 gemCommand = "Gem.install('" + gem + "',version='" + version + "')\n";
162 gemCommand = "Gem.install('" + gem + "')\n";
166 logger.debug("Installing Gem: {} ", gem);
167 logger.trace("Gem install code:\n{}\n", gemCommand);
168 engine.eval(gemCommand);
169 } catch (Exception e) {
170 logger.error("Error installing Gem", e);
174 logger.debug("Ruby gem property has no value");
180 * Configure the base Ruby Environment
182 * @param engine Engine to configure
184 public ScriptEngine configureRubyEnvironment(ScriptEngine engine) {
185 configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
186 Collections.<OptionalConfigurationElement> emptyList()), engine);
191 * Configure the optional elements of the Ruby Environment
193 * @param optionalConfigurationElements Optional elements to configure in the ruby environment
194 * @param engine Engine in which to configure environment
196 private void configureRubyEnvironment(List<OptionalConfigurationElement> optionalConfigurationElements,
197 ScriptEngine engine) {
198 for (OptionalConfigurationElement configElement : optionalConfigurationElements) {
199 String environmentProperty = configElement.mappedTo().get();
200 if (configElement.getValue().isPresent()) {
201 String environmentSetting = "ENV['" + environmentProperty + "']='" + configElement.getValue().get()
204 logger.trace("Setting Ruby environment with code: {} ", environmentSetting);
205 engine.eval(environmentSetting);
206 } catch (ScriptException e) {
207 logger.error("Error setting ruby environment", e);
210 logger.debug("Ruby environment property ({}) has no value", environmentProperty);
214 configureRubyLib(engine);
218 * Split up and insert ENV['RUBYLIB'] into Ruby's $LOAD_PATH
219 * This needs to be called after ENV['RUBYLIB'] has been set by configureRubyEnvironment
221 * @param engine Engine in which to configure environment
223 private void configureRubyLib(ScriptEngine engine) {
224 OptionalConfigurationElement rubyLibConfigElement = CONFIGURATION_PARAMETERS.get(RUBYLIB);
225 if (rubyLibConfigElement == null) {
229 Optional<String> rubyLib = rubyLibConfigElement.getValue();
230 if (rubyLib.isPresent() && !rubyLib.get().trim().isEmpty()) {
231 final String code = "$LOAD_PATH.unshift *ENV['RUBYLIB']&.split(File::PATH_SEPARATOR)" + //
232 "&.reject(&:empty?)" + //
233 "&.reject { |path| $LOAD_PATH.include?(path) }"; //
236 } catch (ScriptException exception) {
237 logger.warn("Error setting $LOAD_PATH from RUBYLIB='{}': {}", rubyLib.get(), exception.getMessage());
243 * Configure system properties
245 * @param optionalConfigurationElements Optional system properties to configure
247 private void configureSystemProperties(List<OptionalConfigurationElement> optionalConfigurationElements) {
248 for (OptionalConfigurationElement configElement : optionalConfigurationElements) {
249 String systemProperty = configElement.mappedTo().get();
250 if (configElement.getValue().isPresent()) {
251 String propertyValue = configElement.getValue().get();
252 logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue);
253 System.setProperty(systemProperty, propertyValue);
255 logger.warn("System property ({}) has no value", systemProperty);
261 * Inner static companion class for configuration elements
263 private static class OptionalConfigurationElement {
265 private final Optional<String> defaultValue;
266 private final Optional<String> mappedTo;
267 private final Type type;
268 private Optional<String> value;
270 private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) {
272 this.defaultValue = Optional.ofNullable(defaultValue);
273 this.mappedTo = Optional.ofNullable(mappedTo);
274 value = Optional.empty();
277 private Optional<String> getValue() {
278 return value.or(() -> defaultValue);
281 private void setValue(String value) {
282 this.value = Optional.of(value);
285 private Optional<String> mappedTo() {
295 private static class Builder {
296 private final Type type;
297 private @Nullable String defaultValue = null;
298 private @Nullable String mappedTo = null;
300 private Builder(Type type) {
304 private Builder mappedTo(String mappedTo) {
305 this.mappedTo = mappedTo;
309 private Builder defaultValue(String value) {
310 this.defaultValue = value;
314 private OptionalConfigurationElement build() {
315 return new OptionalConfigurationElement(type, mappedTo, defaultValue);