]> git.basschouten.com Git - openhab-addons.git/blob
7a712568c2449f757b1be980b515b3ba6e50ca4f
[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. 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 filename 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. Transformation service inject input (source)
83      *            to 'input' variable.
84      * @param source the input to transform
85      */
86     @Override
87     public @Nullable String transform(String filename, String source) throws TransformationException {
88         final long startTime = System.currentTimeMillis();
89         logger.debug("about to transform '{}' by the JavaScript '{}'", source, filename);
90
91         Map<String, String> vars = Collections.emptyMap();
92         String fn = filename;
93
94         if (filename.contains("?")) {
95             String[] parts = filename.split("\\?");
96             if (parts.length > 2) {
97                 throw new TransformationException("Questionmark should be defined only once in the filename");
98             }
99             fn = parts[0];
100             try {
101                 vars = splitQuery(parts[1]);
102             } catch (UnsupportedEncodingException e) {
103                 throw new TransformationException("Illegal filename syntax");
104             }
105             if (isReservedWordUsed(vars)) {
106                 throw new TransformationException(
107                         "'" + SCRIPT_DATA_WORD + "' word is reserved and can't be used in additional parameters");
108             }
109         }
110
111         String result = "";
112
113         try {
114             final CompiledScript cScript = manager.getScript(fn);
115             final Bindings bindings = cScript.getEngine().createBindings();
116             bindings.put(SCRIPT_DATA_WORD, source);
117             vars.forEach((k, v) -> bindings.put(k, v));
118             result = String.valueOf(cScript.eval(bindings));
119             return result;
120         } catch (ScriptException e) {
121             throw new TransformationException("An error occurred while executing script. " + e.getMessage(), e);
122         } finally {
123             logger.trace("JavaScript execution elapsed {} ms. Result: {}", System.currentTimeMillis() - startTime,
124                     result);
125         }
126     }
127
128     private boolean isReservedWordUsed(Map<String, String> map) {
129         for (String key : map.keySet()) {
130             if (SCRIPT_DATA_WORD.equals(key)) {
131                 return true;
132             }
133         }
134         return false;
135     }
136
137     private Map<String, String> splitQuery(@Nullable String query) throws UnsupportedEncodingException {
138         Map<String, String> result = new LinkedHashMap<>();
139         if (query != null) {
140             String[] pairs = query.split("&");
141             for (String pair : pairs) {
142                 String[] keyval = pair.split("=");
143                 if (keyval.length != 2) {
144                     throw new UnsupportedEncodingException();
145                 } else {
146                     result.put(URLDecoder.decode(keyval[0], "UTF-8"), URLDecoder.decode(keyval[1], "UTF-8"));
147                 }
148             }
149         }
150         return result;
151     }
152
153     @Override
154     public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
155             @Nullable Locale locale) {
156         if (PROFILE_CONFIG_URI.equals(uri.toString())) {
157             switch (param) {
158                 case CONFIG_PARAM_FUNCTION:
159                     return getFilenames(FILE_NAME_EXTENSIONS).stream().map(f -> new ParameterOption(f, f))
160                             .collect(Collectors.toList());
161             }
162         }
163         return null;
164     }
165
166     /**
167      * Returns a list of all files with the given extensions in the transformation folder
168      */
169     private List<String> getFilenames(String[] validExtensions) {
170         File path = new File(TransformationScriptWatcher.TRANSFORM_FOLDER + File.separator);
171         return Arrays.asList(path.listFiles(new FileExtensionsFilter(validExtensions))).stream().map(f -> f.getName())
172                 .collect(Collectors.toList());
173     }
174
175     private class FileExtensionsFilter implements FilenameFilter {
176
177         private final String[] validExtensions;
178
179         public FileExtensionsFilter(String[] validExtensions) {
180             this.validExtensions = validExtensions;
181         }
182
183         @Override
184         public boolean accept(@Nullable File dir, @Nullable String name) {
185             if (name != null) {
186                 for (String extension : validExtensions) {
187                     if (name.toLowerCase().endsWith(EXTENSION_SEPARATOR + extension)) {
188                         return true;
189                     }
190                 }
191             }
192             return false;
193         }
194     }
195 }