2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.transform.javascript.internal;
16 import java.io.FilenameFilter;
18 import java.net.URLDecoder;
19 import java.nio.charset.StandardCharsets;
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;
27 import java.util.stream.Collectors;
29 import javax.script.Bindings;
30 import javax.script.CompiledScript;
31 import javax.script.ScriptException;
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;
46 * The implementation of {@link TransformationService} which transforms the
47 * input by Java Script.
49 * @author Pauli Anttila - Initial contribution
50 * @author Thomas Kordelle - pre compiled scripts
53 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = { "openhab.transform=JS" })
54 public class JavaScriptTransformationService implements TransformationService, ConfigOptionProvider {
56 private final Logger logger = LoggerFactory.getLogger(JavaScriptTransformationService.class);
58 private static final char EXTENSION_SEPARATOR = '.';
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" };
64 private static final String SCRIPT_DATA_WORD = "input";
66 private final JavaScriptEngineManager manager;
69 public JavaScriptTransformationService(final @Reference JavaScriptEngineManager manager) {
70 this.manager = manager;
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.
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
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);
91 Map<String, String> vars = Collections.emptyMap();
94 CompiledScript cScript;
96 if (filenameOrInlineScript.startsWith("|")) {
98 cScript = manager.getCompiledScriptByInlineScript(filenameOrInlineScript.substring(1));
100 String filename = filenameOrInlineScript;
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");
109 vars = splitQuery(parts[1]);
110 } catch (IllegalArgumentException e) {
111 throw new TransformationException("Illegal filename syntax");
113 if (isReservedWordUsed(vars)) {
114 throw new TransformationException(
115 "'" + SCRIPT_DATA_WORD + "' word is reserved and can't be used in additional parameters");
119 cScript = manager.getCompiledScriptByFilename(filename);
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));
128 } catch (ScriptException e) {
129 throw new TransformationException("An error occurred while executing script. " + e.getMessage(), e);
131 logger.trace("JavaScript execution elapsed {} ms. Result: {}", System.currentTimeMillis() - startTime,
136 private boolean isReservedWordUsed(Map<String, String> map) {
137 for (String key : map.keySet()) {
138 if (SCRIPT_DATA_WORD.equals(key)) {
145 private Map<String, String> splitQuery(@Nullable String query) throws IllegalArgumentException {
146 Map<String, String> result = new LinkedHashMap<>();
148 String[] pairs = query.split("&");
149 for (String pair : pairs) {
150 String[] keyval = pair.split("=");
151 if (keyval.length != 2) {
152 throw new IllegalArgumentException();
154 result.put(URLDecoder.decode(keyval[0], StandardCharsets.UTF_8),
155 URLDecoder.decode(keyval[1], StandardCharsets.UTF_8));
163 public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
164 @Nullable Locale locale) {
165 if (PROFILE_CONFIG_URI.equals(uri.toString())) {
167 case CONFIG_PARAM_FUNCTION:
168 return getFilenames(FILE_NAME_EXTENSIONS).stream().map(f -> new ParameterOption(f, f))
169 .collect(Collectors.toList());
176 * Returns a list of all files with the given extensions in the transformation folder
178 private List<String> getFilenames(String[] validExtensions) {
179 File path = new File(TransformationScriptWatcher.TRANSFORM_FOLDER + File.separator);
180 return Arrays.asList(path.listFiles(new FileExtensionsFilter(validExtensions))).stream().map(f -> f.getName())
181 .collect(Collectors.toList());
184 private class FileExtensionsFilter implements FilenameFilter {
186 private final String[] validExtensions;
188 public FileExtensionsFilter(String[] validExtensions) {
189 this.validExtensions = validExtensions;
193 public boolean accept(@Nullable File dir, @Nullable String name) {
195 for (String extension : validExtensions) {
196 if (name.toLowerCase().endsWith(EXTENSION_SEPARATOR + extension)) {