]> git.basschouten.com Git - openhab-addons.git/blob
df0db8c8d535aafa07b54c7b64225679e16b1370
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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
51     // Map of configuration parameters
52     private static final Map<String, OptionalConfigurationElement> CONFIGURATION_PARAMETERS = Map.ofEntries(
53             Map.entry("local_context",
54                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
55                             .mappedTo("org.jruby.embed.localcontext.scope").defaultValue("singlethread").build()),
56
57             Map.entry("local_variable",
58                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY)
59                             .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()),
60
61             Map.entry(GEM_HOME,
62                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
63                             .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()),
64
65             Map.entry("rubylib",
66                     new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT)
67                             .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()),
68
69             Map.entry("gems", new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()));
70
71     private static final Map<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS
72             .values().stream().collect(Collectors.groupingBy(v -> v.type));
73
74     /**
75      * Update configuration
76      * 
77      * @param config Configuration parameters to apply to ScriptEngine
78      * @param factory ScriptEngineFactory to configure
79      */
80     void update(Map<String, Object> config, ScriptEngineFactory factory) {
81         logger.trace("JRuby Script Engine Configuration: {}", config);
82         config.forEach(this::processConfigValue);
83         configureScriptEngine(factory);
84     }
85
86     /**
87      * Apply configuration key/value to known configuration parameters
88      * 
89      * @param key Configuration key
90      * @param value Configuration value
91      */
92     private void processConfigValue(String key, Object value) {
93         OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key);
94         if (configurationElement != null) {
95             configurationElement.setValue(value.toString());
96         } else {
97             logger.debug("Ignoring unexpected configuration key: {}", key);
98         }
99     }
100
101     /**
102      * Configure the ScriptEngine
103      * 
104      * @param factory Script Engine to configure
105      */
106     void configureScriptEngine(ScriptEngineFactory factory) {
107         configureSystemProperties(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.SYSTEM_PROPERTY,
108                 Collections.<OptionalConfigurationElement> emptyList()));
109
110         ScriptEngine engine = factory.getScriptEngine();
111
112         configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
113                 Collections.<OptionalConfigurationElement> emptyList()), engine);
114
115         configureGems(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.GEM,
116                 Collections.<OptionalConfigurationElement> emptyList()), engine);
117     }
118
119     /**
120      * Makes Gem home directory if it does not exist
121      */
122     private void ensureGemHomeExists() {
123         OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME);
124         if (gemHomeConfigElement != null) {
125             Optional<String> gemHome = gemHomeConfigElement.getValue();
126             if (gemHome.isPresent()) {
127                 File gemHomeDirectory = new File(gemHome.get());
128                 if (!gemHomeDirectory.exists()) {
129                     logger.debug("gem_home directory does not exist, creating");
130                     if (!gemHomeDirectory.mkdirs()) {
131                         logger.debug("Error creating gem_home direcotry");
132                     }
133                 }
134             } else {
135                 logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists");
136             }
137         }
138     }
139
140     /**
141      * Install a gems in ScriptEngine
142      * 
143      * @param gemsDirectives List of gems to install
144      * @param engine Engine to install gems
145      */
146     private synchronized void configureGems(List<OptionalConfigurationElement> gemDirectives, ScriptEngine engine) {
147         for (OptionalConfigurationElement gemDirective : gemDirectives) {
148             if (gemDirective.getValue().isPresent()) {
149                 ensureGemHomeExists();
150
151                 String[] gems = gemDirective.getValue().get().split(",");
152                 for (String gem : gems) {
153                     gem = gem.trim();
154                     String gemCommand;
155                     if (gem.contains("=")) {
156                         String[] gemParts = gem.split("=");
157                         gem = gemParts[0];
158                         String version = gemParts[1];
159                         gemCommand = "Gem.install('" + gem + "',version='" + version + "')\n";
160                     } else {
161                         gemCommand = "Gem.install('" + gem + "')\n";
162                     }
163
164                     try {
165                         logger.debug("Installing Gem: {} ", gem);
166                         logger.trace("Gem install code:\n{}\n", gemCommand);
167                         engine.eval(gemCommand);
168                     } catch (Exception e) {
169                         logger.error("Error installing Gem", e);
170                     }
171                 }
172             } else {
173                 logger.debug("Ruby gem property has no value");
174             }
175         }
176     }
177
178     /**
179      * Configure the base Ruby Environment
180      * 
181      * @param engine Engine to configure
182      */
183     public ScriptEngine configureRubyEnvironment(ScriptEngine engine) {
184         configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
185                 Collections.<OptionalConfigurationElement> emptyList()), engine);
186         return engine;
187     }
188
189     /**
190      * Configure the optional elements of the Ruby Environment
191      * 
192      * @param optionalConfigurationElements Optional elements to configure in the ruby environment
193      * @param engine Engine in which to configure environment
194      */
195     private void configureRubyEnvironment(List<OptionalConfigurationElement> optionalConfigurationElements,
196             ScriptEngine engine) {
197         for (OptionalConfigurationElement configElement : optionalConfigurationElements) {
198             String environmentProperty = configElement.mappedTo().get();
199             if (configElement.getValue().isPresent()) {
200                 String environmentSetting = "ENV['" + environmentProperty + "']='" + configElement.getValue().get()
201                         + "'";
202                 try {
203                     logger.trace("Setting Ruby environment with code: {} ", environmentSetting);
204                     engine.eval(environmentSetting);
205                 } catch (ScriptException e) {
206                     logger.error("Error setting ruby environment", e);
207                 }
208             } else {
209                 logger.debug("Ruby environment property ({}) has no value", environmentProperty);
210             }
211         }
212     }
213
214     /**
215      * Configure system properties
216      * 
217      * @param optionalConfigurationElements Optional system properties to configure
218      */
219     private void configureSystemProperties(List<OptionalConfigurationElement> optionalConfigurationElements) {
220         for (OptionalConfigurationElement configElement : optionalConfigurationElements) {
221             String systemProperty = configElement.mappedTo().get();
222             if (configElement.getValue().isPresent()) {
223                 String propertyValue = configElement.getValue().get();
224                 logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue);
225                 System.setProperty(systemProperty, propertyValue);
226             } else {
227                 logger.warn("System property ({}) has no value", systemProperty);
228             }
229         }
230     }
231
232     /**
233      * Inner static companion class for configuration elements
234      */
235     private static class OptionalConfigurationElement {
236
237         private final Optional<String> defaultValue;
238         private final Optional<String> mappedTo;
239         private final Type type;
240         private Optional<String> value;
241
242         private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) {
243             this.type = type;
244             this.defaultValue = Optional.ofNullable(defaultValue);
245             this.mappedTo = Optional.ofNullable(mappedTo);
246             value = Optional.empty();
247         }
248
249         private Optional<String> getValue() {
250             return value.or(() -> defaultValue);
251         }
252
253         private void setValue(String value) {
254             this.value = Optional.of(value);
255         }
256
257         private Optional<String> mappedTo() {
258             return mappedTo;
259         }
260
261         private enum Type {
262             SYSTEM_PROPERTY,
263             RUBY_ENVIRONMENT,
264             GEM
265         }
266
267         private static class Builder {
268             private final Type type;
269             private @Nullable String defaultValue = null;
270             private @Nullable String mappedTo = null;
271
272             private Builder(Type type) {
273                 this.type = type;
274             }
275
276             private Builder mappedTo(String mappedTo) {
277                 this.mappedTo = mappedTo;
278                 return this;
279             }
280
281             private Builder defaultValue(String value) {
282                 this.defaultValue = value;
283                 return this;
284             }
285
286             private OptionalConfigurationElement build() {
287                 return new OptionalConfigurationElement(type, mappedTo, defaultValue);
288             }
289         }
290     }
291 }