# 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
| `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.
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;
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) {
@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.");
}
}
}
- 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;
+ }
}
}
// 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"));
+ }
}