2 * Copyright (c) 2010-2024 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.map.internal;
15 import java.io.IOException;
16 import java.io.StringReader;
18 import java.util.Collection;
19 import java.util.LinkedHashMap;
20 import java.util.Locale;
22 import java.util.Map.Entry;
23 import java.util.Properties;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.core.common.registry.RegistryChangeListener;
33 import org.openhab.core.config.core.ConfigOptionProvider;
34 import org.openhab.core.config.core.ParameterOption;
35 import org.openhab.core.transform.Transformation;
36 import org.openhab.core.transform.TransformationException;
37 import org.openhab.core.transform.TransformationRegistry;
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.Deactivate;
42 import org.osgi.service.component.annotations.Reference;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
48 * The implementation of {@link TransformationService} which simply maps strings to other strings
50 * @author Kai Kreuzer - Initial contribution and API
51 * @author Gaƫl L'hopital - Make it localizable
52 * @author Jan N. Klug - Refactored to use {@link TransformationRegistry}
55 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = {
56 "openhab.transform=MAP" })
57 public class MapTransformationService
58 implements TransformationService, ConfigOptionProvider, RegistryChangeListener<Transformation> {
59 private static final String SOURCE_VALUE = "_source_";
60 private static final String PROFILE_CONFIG_URI = "profile:transform:MAP";
61 private static final String CONFIG_PARAM_FUNCTION = "function";
62 private static final Set<String> SUPPORTED_CONFIGURATION_TYPES = Set.of("map");
63 private static final Pattern INLINE_MAP_CONFIG_PATTERN = Pattern.compile("\\s*\\|(?<map>.+)", Pattern.DOTALL);
65 private final Logger logger = LoggerFactory.getLogger(MapTransformationService.class);
66 private final TransformationRegistry transformationRegistry;
67 private final Map<String, Properties> cachedTransformations = new ConcurrentHashMap<>();
68 private final Map<String, Properties> cachedInlineMap = new LRUMap<>(1000);
71 public MapTransformationService(@Reference TransformationRegistry transformationRegistry) {
72 this.transformationRegistry = transformationRegistry;
73 transformationRegistry.addRegistryChangeListener(this);
77 public void deactivate() {
78 transformationRegistry.removeRegistryChangeListener(this);
82 public @Nullable String transform(String function, String source) throws TransformationException {
83 Properties properties = null;
85 Matcher matcher = INLINE_MAP_CONFIG_PATTERN.matcher(function);
86 if (matcher.matches()) {
87 properties = cachedInlineMap.computeIfAbsent(function, f -> {
88 Properties props = new Properties();
89 String map = matcher.group("map").trim();
90 if (!map.contains("\n")) {
91 map = map.replace(";", "\n");
94 props.load(new StringReader(map));
95 logger.trace("Parsed inline map configuration '{}'", props);
96 } catch (IOException e) {
97 logger.warn("Failed to parse inline map configuration '{}': {}", map, e.getMessage());
103 // always get a configuration from the registry to account for changed system locale
104 Transformation transformation = transformationRegistry.get(function, null);
105 if (transformation != null) {
106 properties = cachedTransformations.get(transformation.getUID());
107 if (properties == null) {
108 properties = importConfiguration(transformation);
113 if (properties != null) {
114 String target = properties.getProperty(source);
116 if (target == null) {
117 target = properties.getProperty("");
118 if (target == null) {
119 throw new TransformationException("Target value not found in map for '" + source + "'");
120 } else if (SOURCE_VALUE.equals(target)) {
125 logger.debug("Transformation resulted in '{}'", target);
128 throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
132 public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
133 @Nullable Locale locale) {
134 if (PROFILE_CONFIG_URI.equals(uri.toString())) {
135 if (CONFIG_PARAM_FUNCTION.equals(param)) {
136 return transformationRegistry.getTransformations(SUPPORTED_CONFIGURATION_TYPES).stream()
137 .map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
144 public void added(Transformation element) {
145 // do nothing, configurations are added to cache if needed
149 public void removed(Transformation element) {
150 cachedTransformations.remove(element.getUID());
154 public void updated(Transformation oldElement, Transformation element) {
155 if (cachedTransformations.remove(oldElement.getUID()) != null) {
156 // import only if it was present before
157 importConfiguration(element);
161 private @Nullable Properties importConfiguration(@Nullable Transformation transformation) {
162 if (transformation != null) {
164 Properties properties = new Properties();
165 String function = transformation.getConfiguration().get(Transformation.FUNCTION);
166 if (function == null || function.isBlank()) {
167 logger.warn("Function not defined for transformation '{}'", transformation.getUID());
170 properties.load(new StringReader(function));
171 cachedTransformations.put(transformation.getUID(), properties);
173 } catch (IOException ignored) {
179 class LRUMap<K, V> extends LinkedHashMap<K, V> {
180 private final int maxEntries;
182 public LRUMap(int maxEntries) {
183 super(10, 0.75f, true);
184 this.maxEntries = maxEntries;
187 protected boolean removeEldestEntry(@Nullable Entry<K, V> eldest) {
188 return size() > maxEntries;