mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[map] Support inline map (#17300)
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
badbc7707c
commit
64331da0ad
@ -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
|
||||
|
||||
A default value can be provided if no matching entry is found by using "=value" 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.
|
||||
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.
|
||||
|
@ -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,30 +80,51 @@ 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);
|
||||
}
|
||||
Properties properties = cachedTransformations.get(transformation.getUID());
|
||||
if (properties != null) {
|
||||
String target = properties.getProperty(source);
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
logger.debug("Transformation resulted in '{}'", target);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
if (properties != null) {
|
||||
String target = properties.getProperty(source);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user