]> git.basschouten.com Git - openhab-addons.git/blob
d9f0e29450b1ae6f64312c976bb67ca63fabcd13
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.automation.jrubyscripting.internal;
14
15 import java.io.File;
16 import java.nio.file.Path;
17 import java.nio.file.Paths;
18 import java.util.Collections;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
24
25 import javax.script.ScriptEngine;
26 import javax.script.ScriptEngineFactory;
27 import javax.script.ScriptException;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.core.OpenHAB;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Processes JRuby Configuration Parameters.
37  *
38  * @author Brian O'Connell - Initial contribution
39  * @author Jimmy Tanagra - Add $LOAD_PATH, require injection
40  */
41 @NonNullByDefault
42 public class JRubyScriptEngineConfiguration {
43
44     private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class);
45
46     private static final Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby",
47             "gem_home");
48
49     private static final Path DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "lib", "ruby");
50
51     private static final String GEM_HOME = "gem_home";
52     private static final String RUBYLIB = "rubylib";
53     private static final String GEMS = "gems";
54     private static final String REQUIRE = "require";
55
56     // Map of configuration parameters
57     private static final Map<String, OptionalConfigurationElement> CONFIGURATION_PARAMETERS = Map.ofEntries(
58             Map.entry("local_context",
59                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
60                             .mappedTo("org.jruby.embed.localcontext.scope").defaultValue("singlethread").build()),
61
62             Map.entry("local_variable",
63                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
64                             .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()),
65
66             Map.entry(GEM_HOME,
67                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
68                             .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()),
69
70             Map.entry(RUBYLIB,
71                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
72                             .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()),
73
74             Map.entry(GEMS, new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()),
75
76             Map.entry(REQUIRE,
77                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.REQUIRE).build()));
78
79     private static final Map<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS
80             .values().stream().collect(Collectors.groupingBy(v -> v.type));
81
82     /**
83      * Update configuration
84      * 
85      * @param config Configuration parameters to apply to ScriptEngine
86      * @param factory ScriptEngineFactory to configure
87      */
88     void update(Map<String, Object> config, ScriptEngineFactory factory) {
89         logger.trace("JRuby Script Engine Configuration: {}", config);
90         config.forEach(this::processConfigValue);
91         configureScriptEngine(factory);
92     }
93
94     /**
95      * Apply configuration key/value to known configuration parameters
96      * 
97      * @param key Configuration key
98      * @param value Configuration value
99      */
100     private void processConfigValue(String key, Object value) {
101         OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key);
102         if (configurationElement != null) {
103             configurationElement.setValue(value.toString());
104         } else {
105             logger.debug("Ignoring unexpected configuration key: {}", key);
106         }
107     }
108
109     /**
110      * Configure the ScriptEngine
111      * 
112      * @param factory Script Engine to configure
113      */
114     void configureScriptEngine(ScriptEngineFactory factory) {
115         configureSystemProperties();
116
117         ScriptEngine engine = factory.getScriptEngine();
118         configureRubyEnvironment(engine);
119         configureGems(engine);
120     }
121
122     /**
123      * Makes Gem home directory if it does not exist
124      */
125     private void ensureGemHomeExists() {
126         OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME);
127         if (gemHomeConfigElement == null) {
128             return;
129         }
130         Optional<String> gemHome = gemHomeConfigElement.getValue();
131         if (gemHome.isPresent()) {
132             File gemHomeDirectory = new File(gemHome.get());
133             if (!gemHomeDirectory.exists()) {
134                 logger.debug("gem_home directory does not exist, creating");
135                 if (!gemHomeDirectory.mkdirs()) {
136                     logger.warn("Error creating gem_home directory");
137                 }
138             }
139         } else {
140             logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists");
141         }
142     }
143
144     /**
145      * Install a gems in ScriptEngine
146      * 
147      * @param engine Engine to install gems
148      */
149     private synchronized void configureGems(ScriptEngine engine) {
150         ensureGemHomeExists();
151
152         OptionalConfigurationElement gemsConfigElement = CONFIGURATION_PARAMETERS.get(GEMS);
153         if (gemsConfigElement == null || !gemsConfigElement.getValue().isPresent()) {
154             return;
155         }
156
157         String[] gems = gemsConfigElement.getValue().get().split(",");
158         for (String gem : gems) {
159             gem = gem.trim();
160             String version = "";
161             String gemCommand;
162             if (gem.contains("=")) {
163                 String[] gemParts = gem.split("=");
164                 gem = gemParts[0].trim();
165                 version = gemParts[1].trim();
166             }
167
168             if (gem.isEmpty()) {
169                 continue;
170             } else if (version.isEmpty()) {
171                 gemCommand = "Gem.install('" + gem + "')\n";
172             } else {
173                 gemCommand = "Gem.install('" + gem + "', '" + version + "')\n";
174             }
175
176             try {
177                 logger.debug("Installing Gem: {}", gem);
178                 logger.trace("Gem install code:\n{}\n", gemCommand);
179                 engine.eval(gemCommand);
180             } catch (ScriptException e) {
181                 logger.warn("Error installing Gem: {}", e.getMessage());
182             } catch (BootstrapMethodError e) {
183                 logger.warn("Error while checking/installing gems: {}. You may need to restart OpenHAB",
184                         e.getMessage());
185                 logger.debug("Error in configureGems", e);
186             }
187         }
188     }
189
190     /**
191      * Execute ruby require statement in the ScriptEngine
192      * 
193      * @param engine Engine to insert the require statements
194      */
195     public void injectRequire(ScriptEngine engine) {
196         OptionalConfigurationElement requireConfigElement = CONFIGURATION_PARAMETERS.get(REQUIRE);
197         if (requireConfigElement == null || !requireConfigElement.getValue().isPresent()) {
198             return;
199         }
200
201         Stream.of(requireConfigElement.getValue().get().split(",")).map(s -> s.trim()).filter(s -> !s.isEmpty())
202                 .forEach(script -> {
203                     final String requireStatement = String.format("require '%s'", script);
204                     try {
205                         logger.trace("Injecting require statement: {}", requireStatement);
206                         engine.eval(requireStatement);
207                     } catch (ScriptException e) {
208                         logger.warn("Error evaluating statement {}: {}", requireStatement, e.getMessage());
209                     }
210                 });
211     }
212
213     /**
214      * Configure the optional elements of the Ruby Environment
215      * 
216      * @param engine Engine in which to configure environment
217      */
218     public ScriptEngine configureRubyEnvironment(ScriptEngine engine) {
219         getConfigurationElements(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT).forEach(configElement -> {
220             final String environmentSetting = String.format("ENV['%s']='%s'", configElement.mappedTo().get(),
221                     configElement.getValue().get());
222             try {
223                 logger.trace("Setting Ruby environment with code: {} ", environmentSetting);
224                 engine.eval(environmentSetting);
225             } catch (ScriptException e) {
226                 logger.warn("Error setting ruby environment", e);
227             }
228         });
229
230         configureRubyLib(engine);
231         return engine;
232     }
233
234     /**
235      * Split up and insert ENV['RUBYLIB'] into Ruby's $LOAD_PATH
236      * This needs to be called after ENV['RUBYLIB'] has been set by configureRubyEnvironment
237      * 
238      * @param engine Engine in which to configure environment
239      */
240     private void configureRubyLib(ScriptEngine engine) {
241         OptionalConfigurationElement rubyLibConfigElement = CONFIGURATION_PARAMETERS.get(RUBYLIB);
242         if (rubyLibConfigElement == null) {
243             return;
244         }
245
246         Optional<String> rubyLib = rubyLibConfigElement.getValue();
247         if (rubyLib.isPresent() && !rubyLib.get().trim().isEmpty()) {
248             final String code = "$LOAD_PATH.unshift *ENV['RUBYLIB']&.split(File::PATH_SEPARATOR)" + //
249                     "&.reject(&:empty?)" + //
250                     "&.reject { |path| $LOAD_PATH.include?(path) }"; //
251             try {
252                 engine.eval(code);
253             } catch (ScriptException exception) {
254                 logger.warn("Error setting $LOAD_PATH from RUBYLIB='{}': {}", rubyLib.get(), exception.getMessage());
255             }
256         }
257     }
258
259     /**
260      * Configure system properties
261      * 
262      * @param optionalConfigurationElements Optional system properties to configure
263      */
264     private void configureSystemProperties() {
265         getConfigurationElements(OptionalConfigurationElement.Type.SYSTEM_PROPERTY).forEach(configElement -> {
266             String systemProperty = configElement.mappedTo().get();
267             String propertyValue = configElement.getValue().get();
268             logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue);
269             System.setProperty(systemProperty, propertyValue);
270         });
271     }
272
273     private Stream<OptionalConfigurationElement> getConfigurationElements(
274             OptionalConfigurationElement.Type configurationType) {
275         return CONFIGURATION_TYPE_MAP
276                 .getOrDefault(configurationType, Collections.<OptionalConfigurationElement> emptyList()).stream()
277                 .filter(element -> element.getValue().isPresent());
278     }
279
280     /**
281      * Inner static companion class for configuration elements
282      */
283     private static class OptionalConfigurationElement {
284
285         private final Optional<String> defaultValue;
286         private final Optional<String> mappedTo;
287         private final Type type;
288         private Optional<String> value;
289
290         private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) {
291             this.type = type;
292             this.defaultValue = Optional.ofNullable(defaultValue);
293             this.mappedTo = Optional.ofNullable(mappedTo);
294             value = Optional.empty();
295         }
296
297         private Optional<String> getValue() {
298             return value.or(() -> defaultValue);
299         }
300
301         private void setValue(String value) {
302             this.value = Optional.of(value);
303         }
304
305         private Optional<String> mappedTo() {
306             return mappedTo;
307         }
308
309         private enum Type {
310             SYSTEM_PROPERTY,
311             RUBY_ENVIRONMENT,
312             GEM,
313             REQUIRE
314         }
315
316         private static class Builder {
317             private final Type type;
318             private @Nullable String defaultValue = null;
319             private @Nullable String mappedTo = null;
320
321             private Builder(Type type) {
322                 this.type = type;
323             }
324
325             private Builder mappedTo(String mappedTo) {
326                 this.mappedTo = mappedTo;
327                 return this;
328             }
329
330             private Builder defaultValue(String value) {
331                 this.defaultValue = value;
332                 return this;
333             }
334
335             private OptionalConfigurationElement build() {
336                 return new OptionalConfigurationElement(type, mappedTo, defaultValue);
337             }
338         }
339     }
340 }