]> git.basschouten.com Git - openhab-addons.git/blob
5a38184e3d1c88e6c51fabaff6c16f8101825828
[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
24 import javax.script.ScriptEngine;
25 import javax.script.ScriptEngineFactory;
26 import javax.script.ScriptException;
27
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;
33
34 /**
35  * Processes JRuby Configuration Parameters.
36  *
37  * @author Brian O'Connell - Initial contribution
38  */
39 @NonNullByDefault
40 public class JRubyScriptEngineConfiguration {
41
42     private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class);
43
44     private static final Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby",
45             "gem_home");
46
47     private static final Path DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "lib", "ruby");
48
49     private static final String GEM_HOME = "gem_home";
50     private static final String RUBYLIB = "rubylib";
51
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()),
57
58             Map.entry("local_variable",
59                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
60                             .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()),
61
62             Map.entry(GEM_HOME,
63                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
64                             .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()),
65
66             Map.entry(RUBYLIB,
67                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
68                             .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()),
69
70             Map.entry("gems", new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()));
71
72     private static final Map<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS
73             .values().stream().collect(Collectors.groupingBy(v -> v.type));
74
75     /**
76      * Update configuration
77      * 
78      * @param config Configuration parameters to apply to ScriptEngine
79      * @param factory ScriptEngineFactory to configure
80      */
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);
85     }
86
87     /**
88      * Apply configuration key/value to known configuration parameters
89      * 
90      * @param key Configuration key
91      * @param value Configuration value
92      */
93     private void processConfigValue(String key, Object value) {
94         OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key);
95         if (configurationElement != null) {
96             configurationElement.setValue(value.toString());
97         } else {
98             logger.debug("Ignoring unexpected configuration key: {}", key);
99         }
100     }
101
102     /**
103      * Configure the ScriptEngine
104      * 
105      * @param factory Script Engine to configure
106      */
107     void configureScriptEngine(ScriptEngineFactory factory) {
108         configureSystemProperties(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.SYSTEM_PROPERTY,
109                 Collections.<OptionalConfigurationElement> emptyList()));
110
111         ScriptEngine engine = factory.getScriptEngine();
112
113         configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
114                 Collections.<OptionalConfigurationElement> emptyList()), engine);
115
116         configureGems(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.GEM,
117                 Collections.<OptionalConfigurationElement> emptyList()), engine);
118     }
119
120     /**
121      * Makes Gem home directory if it does not exist
122      */
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");
133                     }
134                 }
135             } else {
136                 logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists");
137             }
138         }
139     }
140
141     /**
142      * Install a gems in ScriptEngine
143      * 
144      * @param gemsDirectives List of gems to install
145      * @param engine Engine to install gems
146      */
147     private synchronized void configureGems(List<OptionalConfigurationElement> gemDirectives, ScriptEngine engine) {
148         for (OptionalConfigurationElement gemDirective : gemDirectives) {
149             if (gemDirective.getValue().isPresent()) {
150                 ensureGemHomeExists();
151
152                 String[] gems = gemDirective.getValue().get().split(",");
153                 for (String gem : gems) {
154                     gem = gem.trim();
155                     String gemCommand;
156                     if (gem.contains("=")) {
157                         String[] gemParts = gem.split("=");
158                         gem = gemParts[0];
159                         String version = gemParts[1];
160                         gemCommand = "Gem.install('" + gem + "',version='" + version + "')\n";
161                     } else {
162                         gemCommand = "Gem.install('" + gem + "')\n";
163                     }
164
165                     try {
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);
171                     }
172                 }
173             } else {
174                 logger.debug("Ruby gem property has no value");
175             }
176         }
177     }
178
179     /**
180      * Configure the base Ruby Environment
181      * 
182      * @param engine Engine to configure
183      */
184     public ScriptEngine configureRubyEnvironment(ScriptEngine engine) {
185         configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
186                 Collections.<OptionalConfigurationElement> emptyList()), engine);
187         return engine;
188     }
189
190     /**
191      * Configure the optional elements of the Ruby Environment
192      * 
193      * @param optionalConfigurationElements Optional elements to configure in the ruby environment
194      * @param engine Engine in which to configure environment
195      */
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()
202                         + "'";
203                 try {
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);
208                 }
209             } else {
210                 logger.debug("Ruby environment property ({}) has no value", environmentProperty);
211             }
212         }
213
214         configureRubyLib(engine);
215     }
216
217     /**
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
220      * 
221      * @param engine Engine in which to configure environment
222      */
223     private void configureRubyLib(ScriptEngine engine) {
224         OptionalConfigurationElement rubyLibConfigElement = CONFIGURATION_PARAMETERS.get(RUBYLIB);
225         if (rubyLibConfigElement == null) {
226             return;
227         }
228
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) }"; //
234             try {
235                 engine.eval(code);
236             } catch (ScriptException exception) {
237                 logger.warn("Error setting $LOAD_PATH from RUBYLIB='{}': {}", rubyLib.get(), exception.getMessage());
238             }
239         }
240     }
241
242     /**
243      * Configure system properties
244      * 
245      * @param optionalConfigurationElements Optional system properties to configure
246      */
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);
254             } else {
255                 logger.warn("System property ({}) has no value", systemProperty);
256             }
257         }
258     }
259
260     /**
261      * Inner static companion class for configuration elements
262      */
263     private static class OptionalConfigurationElement {
264
265         private final Optional<String> defaultValue;
266         private final Optional<String> mappedTo;
267         private final Type type;
268         private Optional<String> value;
269
270         private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) {
271             this.type = type;
272             this.defaultValue = Optional.ofNullable(defaultValue);
273             this.mappedTo = Optional.ofNullable(mappedTo);
274             value = Optional.empty();
275         }
276
277         private Optional<String> getValue() {
278             return value.or(() -> defaultValue);
279         }
280
281         private void setValue(String value) {
282             this.value = Optional.of(value);
283         }
284
285         private Optional<String> mappedTo() {
286             return mappedTo;
287         }
288
289         private enum Type {
290             SYSTEM_PROPERTY,
291             RUBY_ENVIRONMENT,
292             GEM
293         }
294
295         private static class Builder {
296             private final Type type;
297             private @Nullable String defaultValue = null;
298             private @Nullable String mappedTo = null;
299
300             private Builder(Type type) {
301                 this.type = type;
302             }
303
304             private Builder mappedTo(String mappedTo) {
305                 this.mappedTo = mappedTo;
306                 return this;
307             }
308
309             private Builder defaultValue(String value) {
310                 this.defaultValue = value;
311                 return this;
312             }
313
314             private OptionalConfigurationElement build() {
315                 return new OptionalConfigurationElement(type, mappedTo, defaultValue);
316             }
317         }
318     }
319 }