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.Locale;
21 import java.util.Properties;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.common.registry.RegistryChangeListener;
29 import org.openhab.core.config.core.ConfigOptionProvider;
30 import org.openhab.core.config.core.ParameterOption;
31 import org.openhab.core.transform.Transformation;
32 import org.openhab.core.transform.TransformationException;
33 import org.openhab.core.transform.TransformationRegistry;
34 import org.openhab.core.transform.TransformationService;
35 import org.osgi.service.component.annotations.Activate;
36 import org.osgi.service.component.annotations.Component;
37 import org.osgi.service.component.annotations.Deactivate;
38 import org.osgi.service.component.annotations.Reference;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
44 * The implementation of {@link TransformationService} which simply maps strings to other strings
46 * @author Kai Kreuzer - Initial contribution and API
47 * @author Gaƫl L'hopital - Make it localizable
48 * @author Jan N. Klug - Refactored to use {@link TransformationRegistry}
51 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = {
52 "openhab.transform=MAP" })
53 public class MapTransformationService
54 implements TransformationService, ConfigOptionProvider, RegistryChangeListener<Transformation> {
55 private static final String SOURCE_VALUE = "_source_";
56 private static final String PROFILE_CONFIG_URI = "profile:transform:MAP";
57 private static final String CONFIG_PARAM_FUNCTION = "function";
58 private static final Set<String> SUPPORTED_CONFIGURATION_TYPES = Set.of("map");
60 private final Logger logger = LoggerFactory.getLogger(MapTransformationService.class);
61 private final TransformationRegistry transformationRegistry;
62 private final Map<String, Properties> cachedTransformations = new ConcurrentHashMap<>();
65 public MapTransformationService(@Reference TransformationRegistry transformationRegistry) {
66 this.transformationRegistry = transformationRegistry;
67 transformationRegistry.addRegistryChangeListener(this);
71 public void deactivate() {
72 transformationRegistry.removeRegistryChangeListener(this);
76 public @Nullable String transform(String function, String source) throws TransformationException {
77 // always get a configuration from the registry to account for changed system locale
78 Transformation transformation = transformationRegistry.get(function, null);
80 if (transformation != null) {
81 if (!cachedTransformations.containsKey(transformation.getUID())) {
82 importConfiguration(transformation);
84 Properties properties = cachedTransformations.get(transformation.getUID());
85 if (properties != null) {
86 String target = properties.getProperty(source);
89 target = properties.getProperty("");
91 throw new TransformationException("Target value not found in map for '" + source + "'");
92 } else if (SOURCE_VALUE.equals(target)) {
97 logger.debug("Transformation resulted in '{}'", target);
101 throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
105 public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
106 @Nullable Locale locale) {
107 if (PROFILE_CONFIG_URI.equals(uri.toString())) {
108 if (CONFIG_PARAM_FUNCTION.equals(param)) {
109 return transformationRegistry.getTransformations(SUPPORTED_CONFIGURATION_TYPES).stream()
110 .map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
117 public void added(Transformation element) {
118 // do nothing, configurations are added to cache if needed
122 public void removed(Transformation element) {
123 cachedTransformations.remove(element.getUID());
127 public void updated(Transformation oldElement, Transformation element) {
128 if (cachedTransformations.remove(oldElement.getUID()) != null) {
129 // import only if it was present before
130 importConfiguration(element);
134 private void importConfiguration(@Nullable Transformation transformation) {
135 if (transformation != null) {
137 Properties properties = new Properties();
138 String function = transformation.getConfiguration().get(Transformation.FUNCTION);
139 if (function == null || function.isBlank()) {
140 logger.warn("Function not defined for transformation '{}'", transformation.getUID());
143 properties.load(new StringReader(function));
144 cachedTransformations.put(transformation.getUID(), properties);
145 } catch (IOException ignored) {