[map] Add a way to customize inline-map delimiters (#17327)

* [map] Add a way to customize inline-map delimiters

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
jimtng 2024-09-10 07:01:56 +10:00 committed by Ciprian Pascu
parent e93e0a360c
commit 0224c3a457
5 changed files with 60 additions and 15 deletions

View File

@ -21,11 +21,20 @@ 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.
Instead of providing the file name from which to load, the mapping table can be specified inline by prefixing it with the pipe character `|` .
The inline map entries are delimited with semicolons (`;`) by default.
For example, the following map function translates open/closed to ON/OFF: `|open=ON; closed=OFF`
The delimiters can be changed by adding `?delimiter=` immediately after the pipe character `|`.
Some examples of changing to different delimiters:
- `|?delimiter=,online=ON,offline=OFF`
- `|?delimiter=|online=ON|offline=OFF`
- `|?delimiter=##online=ON##offline=OFF`
To use `?delimiter` as an actual map key, do not place it at the beginning of the map.
## Example
transform/binary.map:
@ -54,7 +63,7 @@ The functionality of this `TransformationService` can be used in a `Profile` on
To do so, it can be configured in the `.items` file as follows:
```java
String <itemName> { channel="<channelUID>"[profile="transform:MAP", function="<filename>", sourceFormat="<valueFormat>"]}
String <itemName> { channel="<channelUID>" [profile="transform:MAP", function="<filename>", sourceFormat="<valueFormat>" ] }
```
The mapping filename (within the `transform` folder) has to be set in the `function` parameter.
@ -62,3 +71,9 @@ The parameter `sourceFormat` is optional and can be used to format the input val
If omitted the default is `%s`, so the input value will be put into the transformation without any format changes.
Please note: This profile is a one-way transformation, i.e. only values from a device towards the item are changed, the other direction is left untouched.
To use an inline map in the profile:
```java
String <itemName> { channel="<channelUID>" [ profile="transform:MAP", function="|open=ON;closed=OFF" ] }
```

View File

@ -20,6 +20,8 @@ import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -60,7 +62,9 @@ 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 static final String INLINE_MAP_DEFAULT_DELIMITER = ";";
private static final Pattern INLINE_MAP_CONFIG_PATTERN = Pattern
.compile("\\s*\\|(?:\\?delimiter=(?<delimiter>\\W+?))?(?<map>.+)", Pattern.DOTALL);
private final Logger logger = LoggerFactory.getLogger(MapTransformationService.class);
private final TransformationRegistry transformationRegistry;
@ -87,9 +91,9 @@ public class MapTransformationService
properties = cachedInlineMap.computeIfAbsent(function, f -> {
Properties props = new Properties();
String map = matcher.group("map").trim();
if (!map.contains("\n")) {
map = map.replace(";", "\n");
}
String delimiter = Objects.requireNonNull(Optional.ofNullable(matcher.group("delimiter"))
.map(String::trim).orElse(INLINE_MAP_DEFAULT_DELIMITER));
map = map.replace(delimiter, "\n");
try {
props.load(new StringReader(map));
logger.trace("Parsed inline map configuration '{}'", props);

View File

@ -7,7 +7,16 @@
<config-description uri="profile:transform:MAP">
<parameter name="function" type="text" required="true">
<label>Filename</label>
<description>Filename containing the mapping information.</description>
<description><![CDATA[Filename containing the mapping information.
<br /><br />
Inline map is supported, e.g. "|online=ON;offline=OFF".
<br /><br />
The inline map entries are delimited with semicolons ("<code>;</code>") by default.
<br />
To use a different delimiter, for example a comma: "<code>|?delimiter=,;online=ON,offline=OFF</code>"
<br />
To use "<code>?delimiter</code>" as an actual map key, do not place it at the beginning of the map.
]]></description>
<limitToOptions>false</limitToOptions>
</parameter>
<parameter name="sourceFormat" type="text">

View File

@ -1,7 +1,12 @@
# add-on
addon.map.name = MAP transformation
addon.map.description = Transforms the input by mapping it to another string.
# bundle config
profile-type.transform.MAP.label = MAP
profile.config.transform.MAP.function.label = Filename
profile.config.transform.MAP.function.description = Filename containing the mapping information.
profile.config.transform.MAP.function.description = Filename containing the mapping information.<br /><br />Inline map is supported, e.g. "|online=ON;offline=OFF".<br /><br />The inline map entries are delimited with semicolons ("<code>;</code>") by default. <br /> To use a different delimiter, for example a comma: "<code>|?delimiter=,;online=ON,offline=OFF</code>" <br /> To use "<code>?delimiter</code>" as an actual map key, do not place it at the beginning of the map.
profile.config.transform.MAP.sourceFormat.label = State Formatter
profile.config.transform.MAP.sourceFormat.description = How to format the state on the channel before transforming it, i.e. %s or %.1f °C (default is %s).

View File

@ -159,12 +159,6 @@ public class MapTransformationServiceTest extends JavaTest {
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";
@ -176,4 +170,22 @@ public class MapTransformationServiceTest extends JavaTest {
String transformation = "|key1=value1;key2=value;=_source_";
assertEquals("nonexistent", processor.transform(transformation, "nonexistent"));
}
@Test
public void customSeparatorTest() throws TransformationException {
String transformation = "|?delimiter=,key1=value1;with;semicolons,key2;too=value2,?delimiter=value3";
assertEquals("value1;with;semicolons", processor.transform(transformation, "key1"));
assertEquals("value2", processor.transform(transformation, "key2;too"));
assertEquals("value3", processor.transform(transformation, "?delimiter"));
transformation = "|?delimiter=||key1=value1;with;semicolons||key2;too=value2||?delimiter=value3";
assertEquals("value1;with;semicolons", processor.transform(transformation, "key1"));
assertEquals("value2", processor.transform(transformation, "key2;too"));
assertEquals("value3", processor.transform(transformation, "?delimiter"));
transformation = "|key1=value1;key2=value2;?delimiter=value3";
assertEquals("value1", processor.transform(transformation, "key1"));
assertEquals("value2", processor.transform(transformation, "key2"));
assertEquals("value3", processor.transform(transformation, "?delimiter"));
}
}