]> git.basschouten.com Git - openhab-addons.git/blob
c17d5be0b4763b750f9d0cdf074b943d4c86e7bb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.map.internal;
14
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.net.URI;
18 import java.util.Collection;
19 import java.util.LinkedHashMap;
20 import java.util.Locale;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Properties;
24 import java.util.Set;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
29
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;
45
46 /**
47  * <p>
48  * The implementation of {@link TransformationService} which simply maps strings to other strings
49  *
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}
53  */
54 @NonNullByDefault
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);
64
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);
69
70     @Activate
71     public MapTransformationService(@Reference TransformationRegistry transformationRegistry) {
72         this.transformationRegistry = transformationRegistry;
73         transformationRegistry.addRegistryChangeListener(this);
74     }
75
76     @Deactivate
77     public void deactivate() {
78         transformationRegistry.removeRegistryChangeListener(this);
79     }
80
81     @Override
82     public @Nullable String transform(String function, String source) throws TransformationException {
83         Properties properties = null;
84
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");
92                 }
93                 try {
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());
98                     return null;
99                 }
100                 return props;
101             });
102         } else {
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);
109                 }
110             }
111         }
112
113         if (properties != null) {
114             String target = properties.getProperty(source);
115
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)) {
121                     target = source;
122                 }
123             }
124
125             logger.debug("Transformation resulted in '{}'", target);
126             return target;
127         }
128         throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
129     }
130
131     @Override
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());
138             }
139         }
140         return null;
141     }
142
143     @Override
144     public void added(Transformation element) {
145         // do nothing, configurations are added to cache if needed
146     }
147
148     @Override
149     public void removed(Transformation element) {
150         cachedTransformations.remove(element.getUID());
151     }
152
153     @Override
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);
158         }
159     }
160
161     private @Nullable Properties importConfiguration(@Nullable Transformation transformation) {
162         if (transformation != null) {
163             try {
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());
168                     return null;
169                 }
170                 properties.load(new StringReader(function));
171                 cachedTransformations.put(transformation.getUID(), properties);
172                 return properties;
173             } catch (IOException ignored) {
174             }
175         }
176         return null;
177     }
178
179     class LRUMap<K, V> extends LinkedHashMap<K, V> {
180         private final int maxEntries;
181
182         public LRUMap(int maxEntries) {
183             super(10, 0.75f, true);
184             this.maxEntries = maxEntries;
185         }
186
187         protected boolean removeEldestEntry(@Nullable Entry<K, V> eldest) {
188             return size() > maxEntries;
189         }
190     }
191 }