]> git.basschouten.com Git - openhab-addons.git/blob
2294d11bf1b835bf875bad48ef917442d871ab46
[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.transform.javascript.internal;
14
15 import java.io.File;
16 import java.io.FilenameFilter;
17 import java.io.UnsupportedEncodingException;
18 import java.net.URI;
19 import java.net.URLDecoder;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.LinkedHashMap;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27 import java.util.stream.Collectors;
28
29 import javax.script.Bindings;
30 import javax.script.CompiledScript;
31 import javax.script.ScriptException;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.core.config.core.ConfigOptionProvider;
36 import org.openhab.core.config.core.ParameterOption;
37 import org.openhab.core.transform.TransformationException;
38 import org.openhab.core.transform.TransformationService;
39 import org.osgi.service.component.annotations.Activate;
40 import org.osgi.service.component.annotations.Component;
41 import org.osgi.service.component.annotations.Reference;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The implementation of {@link TransformationService} which transforms the
47  * input by Java Script.
48  *
49  * @author Pauli Anttila - Initial contribution
50  * @author Thomas Kordelle - pre compiled scripts
51  */
52 @NonNullByDefault
53 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = { "openhab.transform=JS" })
54 public class JavaScriptTransformationService implements TransformationService, ConfigOptionProvider {
55
56     private final Logger logger = LoggerFactory.getLogger(JavaScriptTransformationService.class);
57
58     private static final char EXTENSION_SEPARATOR = '.';
59
60     private static final String PROFILE_CONFIG_URI = "profile:transform:JS";
61     private static final String CONFIG_PARAM_FUNCTION = "function";
62     private static final String[] FILE_NAME_EXTENSIONS = { "js" };
63
64     private static final String SCRIPT_DATA_WORD = "input";
65
66     private final JavaScriptEngineManager manager;
67
68     @Activate
69     public JavaScriptTransformationService(final @Reference JavaScriptEngineManager manager) {
70         this.manager = manager;
71     }
72
73     /**
74      * Transforms the input <code>source</code> by Java Script. If script is a filename, it expects the
75      * transformation rule to be read from a file which is stored under the
76      * 'configurations/transform' folder. To organize the various
77      * transformations one should use subfolders.
78      *
79      * @param filenameOrInlineScript parameter can be 1) the name of the file which contains the Java script
80      *            transformation rule. Filename can also include additional
81      *            variables in URI query variable format which will be injected
82      *            to script engine. 2) inline script when starting with '|' character.
83      *            Transformation service inject input (source) to 'input' variable.
84      * @param source the input to transform
85      */
86     @Override
87     public @Nullable String transform(String filenameOrInlineScript, String source) throws TransformationException {
88         final long startTime = System.currentTimeMillis();
89         logger.debug("about to transform '{}' by the JavaScript '{}'", source, filenameOrInlineScript);
90
91         Map<String, String> vars = Collections.emptyMap();
92         String result = "";
93
94         CompiledScript cScript;
95
96         if (filenameOrInlineScript.startsWith("|")) {
97             // inline java script
98             cScript = manager.getCompiledScriptByInlineScript(filenameOrInlineScript.substring(1));
99         } else {
100             String filename = filenameOrInlineScript;
101
102             if (filename.contains("?")) {
103                 String[] parts = filename.split("\\?");
104                 if (parts.length > 2) {
105                     throw new TransformationException("Questionmark should be defined only once in the filename");
106                 }
107                 filename = parts[0];
108                 try {
109                     vars = splitQuery(parts[1]);
110                 } catch (UnsupportedEncodingException e) {
111                     throw new TransformationException("Illegal filename syntax");
112                 }
113                 if (isReservedWordUsed(vars)) {
114                     throw new TransformationException(
115                             "'" + SCRIPT_DATA_WORD + "' word is reserved and can't be used in additional parameters");
116                 }
117             }
118
119             cScript = manager.getCompiledScriptByFilename(filename);
120         }
121
122         try {
123             final Bindings bindings = cScript.getEngine().createBindings();
124             bindings.put(SCRIPT_DATA_WORD, source);
125             vars.forEach((k, v) -> bindings.put(k, v));
126             result = String.valueOf(cScript.eval(bindings));
127             return result;
128         } catch (ScriptException e) {
129             throw new TransformationException("An error occurred while executing script. " + e.getMessage(), e);
130         } finally {
131             logger.trace("JavaScript execution elapsed {} ms. Result: {}", System.currentTimeMillis() - startTime,
132                     result);
133         }
134     }
135
136     private boolean isReservedWordUsed(Map<String, String> map) {
137         for (String key : map.keySet()) {
138             if (SCRIPT_DATA_WORD.equals(key)) {
139                 return true;
140             }
141         }
142         return false;
143     }
144
145     private Map<String, String> splitQuery(@Nullable String query) throws UnsupportedEncodingException {
146         Map<String, String> result = new LinkedHashMap<>();
147         if (query != null) {
148             String[] pairs = query.split("&");
149             for (String pair : pairs) {
150                 String[] keyval = pair.split("=");
151                 if (keyval.length != 2) {
152                     throw new UnsupportedEncodingException();
153                 } else {
154                     result.put(URLDecoder.decode(keyval[0], "UTF-8"), URLDecoder.decode(keyval[1], "UTF-8"));
155                 }
156             }
157         }
158         return result;
159     }
160
161     @Override
162     public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
163             @Nullable Locale locale) {
164         if (PROFILE_CONFIG_URI.equals(uri.toString())) {
165             switch (param) {
166                 case CONFIG_PARAM_FUNCTION:
167                     return getFilenames(FILE_NAME_EXTENSIONS).stream().map(f -> new ParameterOption(f, f))
168                             .collect(Collectors.toList());
169             }
170         }
171         return null;
172     }
173
174     /**
175      * Returns a list of all files with the given extensions in the transformation folder
176      */
177     private List<String> getFilenames(String[] validExtensions) {
178         File path = new File(TransformationScriptWatcher.TRANSFORM_FOLDER + File.separator);
179         return Arrays.asList(path.listFiles(new FileExtensionsFilter(validExtensions))).stream().map(f -> f.getName())
180                 .collect(Collectors.toList());
181     }
182
183     private class FileExtensionsFilter implements FilenameFilter {
184
185         private final String[] validExtensions;
186
187         public FileExtensionsFilter(String[] validExtensions) {
188             this.validExtensions = validExtensions;
189         }
190
191         @Override
192         public boolean accept(@Nullable File dir, @Nullable String name) {
193             if (name != null) {
194                 for (String extension : validExtensions) {
195                     if (name.toLowerCase().endsWith(EXTENSION_SEPARATOR + extension)) {
196                         return true;
197                     }
198                 }
199             }
200             return false;
201         }
202     }
203 }