]> git.basschouten.com Git - openhab-addons.git/commitdiff
[map] Support inline map (#17300)
authorjimtng <2554958+jimtng@users.noreply.github.com>
Tue, 20 Aug 2024 10:15:02 +0000 (20:15 +1000)
committerGitHub <noreply@github.com>
Tue, 20 Aug 2024 10:15:02 +0000 (12:15 +0200)
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
bundles/org.openhab.transform.map/README.md
bundles/org.openhab.transform.map/src/main/java/org/openhab/transform/map/internal/MapTransformationService.java
bundles/org.openhab.transform.map/src/test/java/org/openhab/transform/map/internal/MapTransformationServiceTest.java

index ebd0873649b8fcd3b6eebd88635d221c15819b15..efc8317f1e5c79171dd47a52a0affae247bd29d3 100644 (file)
@@ -1,15 +1,30 @@
 # Map Transformation Service
 
-Transforms the input by mapping it to another string. It expects the mappings to be read from a file which is stored under the `transform` folder.
-The file name must have the `.map` extension. 
+Transforms the input by mapping it to another string.
 
-This file should be in property syntax, i.e. simple lines with "key=value" pairs. 
-The file format is documented [here](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Properties.html#load(java.io.Reader)).
-To organize the various transformations one might use subfolders.
+## Map Syntax
+
+The mapping is performed based on "key=value" pairs.
+When the input matches a `key` in the mapping table, the corresponding `value` is given as the output of the transformation.
+
+The format of the mapping table is documented [here](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Properties.html#load(java.io.Reader)).
 
-A default value can be provided if no matching entry is found by using "=value" syntax. 
+A default value can be provided if no matching entry is found by using "=value" syntax.
 Defining this default value using `_source_` would then return the non transformed input string.
 
+## File-based Map
+
+The mapping table can be stored in a file under the `transform` folder.
+The file name must have the `.map` extension.
+
+To organize the various transformations one might use subfolders.
+
+## Inline Map
+
+Instead of providing the file name from which to load, the mapping table can be specified inline by prefixing it with the `|` character.
+The "key=value" pairs are separated with a semicolon (`;`) or a newline character.
+
+For example, the following map function translates open/closed to ON/OFF: `|open=ON; closed=OFF`
 
 ## Example
 
@@ -33,7 +48,6 @@ white\ space=using escape
 | `white space` | `using escape` |
 | `anything`    | `default`      |
 
-
 ## Usage as a Profile
 
 The functionality of this `TransformationService` can be used in a `Profile` on an `ItemChannelLink` too.
index 076be76ef06023179847a2d7b3672a0b1798561f..c17d5be0b4763b750f9d0cdf074b943d4c86e7bb 100644 (file)
@@ -16,11 +16,15 @@ import java.io.IOException;
 import java.io.StringReader;
 import java.net.URI;
 import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -56,10 +60,12 @@ public class MapTransformationService
     private static final String PROFILE_CONFIG_URI = "profile:transform:MAP";
     private static final String CONFIG_PARAM_FUNCTION = "function";
     private static final Set<String> SUPPORTED_CONFIGURATION_TYPES = Set.of("map");
+    private static final Pattern INLINE_MAP_CONFIG_PATTERN = Pattern.compile("\\s*\\|(?<map>.+)", Pattern.DOTALL);
 
     private final Logger logger = LoggerFactory.getLogger(MapTransformationService.class);
     private final TransformationRegistry transformationRegistry;
     private final Map<String, Properties> cachedTransformations = new ConcurrentHashMap<>();
+    private final Map<String, Properties> cachedInlineMap = new LRUMap<>(1000);
 
     @Activate
     public MapTransformationService(@Reference TransformationRegistry transformationRegistry) {
@@ -74,29 +80,50 @@ public class MapTransformationService
 
     @Override
     public @Nullable String transform(String function, String source) throws TransformationException {
-        // always get a configuration from the registry to account for changed system locale
-        Transformation transformation = transformationRegistry.get(function, null);
+        Properties properties = null;
 
-        if (transformation != null) {
-            if (!cachedTransformations.containsKey(transformation.getUID())) {
-                importConfiguration(transformation);
+        Matcher matcher = INLINE_MAP_CONFIG_PATTERN.matcher(function);
+        if (matcher.matches()) {
+            properties = cachedInlineMap.computeIfAbsent(function, f -> {
+                Properties props = new Properties();
+                String map = matcher.group("map").trim();
+                if (!map.contains("\n")) {
+                    map = map.replace(";", "\n");
+                }
+                try {
+                    props.load(new StringReader(map));
+                    logger.trace("Parsed inline map configuration '{}'", props);
+                } catch (IOException e) {
+                    logger.warn("Failed to parse inline map configuration '{}': {}", map, e.getMessage());
+                    return null;
+                }
+                return props;
+            });
+        } else {
+            // always get a configuration from the registry to account for changed system locale
+            Transformation transformation = transformationRegistry.get(function, null);
+            if (transformation != null) {
+                properties = cachedTransformations.get(transformation.getUID());
+                if (properties == null) {
+                    properties = importConfiguration(transformation);
+                }
             }
-            Properties properties = cachedTransformations.get(transformation.getUID());
-            if (properties != null) {
-                String target = properties.getProperty(source);
+        }
+
+        if (properties != null) {
+            String target = properties.getProperty(source);
 
+            if (target == null) {
+                target = properties.getProperty("");
                 if (target == null) {
-                    target = properties.getProperty("");
-                    if (target == null) {
-                        throw new TransformationException("Target value not found in map for '" + source + "'");
-                    } else if (SOURCE_VALUE.equals(target)) {
-                        target = source;
-                    }
+                    throw new TransformationException("Target value not found in map for '" + source + "'");
+                } else if (SOURCE_VALUE.equals(target)) {
+                    target = source;
                 }
-
-                logger.debug("Transformation resulted in '{}'", target);
-                return target;
             }
+
+            logger.debug("Transformation resulted in '{}'", target);
+            return target;
         }
         throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
     }
@@ -131,19 +158,34 @@ public class MapTransformationService
         }
     }
 
-    private void importConfiguration(@Nullable Transformation transformation) {
+    private @Nullable Properties importConfiguration(@Nullable Transformation transformation) {
         if (transformation != null) {
             try {
                 Properties properties = new Properties();
                 String function = transformation.getConfiguration().get(Transformation.FUNCTION);
                 if (function == null || function.isBlank()) {
                     logger.warn("Function not defined for transformation '{}'", transformation.getUID());
-                    return;
+                    return null;
                 }
                 properties.load(new StringReader(function));
                 cachedTransformations.put(transformation.getUID(), properties);
+                return properties;
             } catch (IOException ignored) {
             }
         }
+        return null;
+    }
+
+    class LRUMap<K, V> extends LinkedHashMap<K, V> {
+        private final int maxEntries;
+
+        public LRUMap(int maxEntries) {
+            super(10, 0.75f, true);
+            this.maxEntries = maxEntries;
+        }
+
+        protected boolean removeEldestEntry(@Nullable Entry<K, V> eldest) {
+            return size() > maxEntries;
+        }
     }
 }
index 47fd29a155948786ab3b54d4cf0917ec96f44402..28fd8ead8739189ab1f5dad94c737a061bbeb5c9 100644 (file)
@@ -152,4 +152,28 @@ public class MapTransformationServiceTest extends JavaTest {
         // ensure modified configuration is applied
         assertEquals("fermé", processor.transform(NON_DEFAULTED_TRANSFORMATION_DE, SOURCE_CLOSED));
     }
+
+    @Test
+    public void oneLineInlineMapTest() throws TransformationException {
+        String transformation = "|key1=semicolons_are_the_separators ; key2 = value2";
+        assertEquals("value2", processor.transform(transformation, "key2"));
+    }
+
+    @Test
+    public void multiLineInlineMapTest() throws TransformationException {
+        String transformation = "|key1=semicolons_arent_separators;1 \n key2 = value;2";
+        assertEquals("value;2", processor.transform(transformation, "key2"));
+    }
+
+    @Test
+    public void defaultInlineTest() throws TransformationException {
+        String transformation = "|key1=value1;key2=value;=default";
+        assertEquals("default", processor.transform(transformation, "nonexistent"));
+    }
+
+    @Test
+    public void defaultSourceInlineTest() throws TransformationException {
+        String transformation = "|key1=value1;key2=value;=_source_";
+        assertEquals("nonexistent", processor.transform(transformation, "nonexistent"));
+    }
 }