diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java index 8b68e21412..b4f6bb7ae0 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/ExpireManager.java @@ -14,6 +14,7 @@ package org.openhab.core.internal.items; import java.time.Duration; import java.time.Instant; +import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -289,19 +290,24 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener CONFIG_KEYS = Set.of(CONFIG_DURATION, CONFIG_COMMAND, CONFIG_STATE, + CONFIG_IGNORE_STATE_UPDATES, CONFIG_IGNORE_COMMANDS); private static final StringType STRING_TYPE_NULL_HYPHEN = new StringType("'NULL'"); private static final StringType STRING_TYPE_NULL = new StringType("NULL"); private static final StringType STRING_TYPE_UNDEF_HYPHEN = new StringType("'UNDEF'"); private static final StringType STRING_TYPE_UNDEF = new StringType("UNDEF"); - protected static final String COMMAND_PREFIX = "command="; - protected static final String STATE_PREFIX = "state="; + protected static final String COMMAND_PREFIX = CONFIG_COMMAND + "="; + protected static final String STATE_PREFIX = CONFIG_STATE + "="; - protected static final Pattern DURATION_PATTERN = Pattern - .compile("(?:([0-9]+)H)?\\s*(?:([0-9]+)M)?\\s*(?:([0-9]+)S)?", Pattern.CASE_INSENSITIVE); + protected static final Pattern DURATION_PATTERN = Pattern.compile( + "(?:([0-9]+)D)?\\s*(?:([0-9]+)H)?\\s*(?:([0-9]+)M)?\\s*(?:([0-9]+)S)?", Pattern.CASE_INSENSITIVE); final @Nullable Command expireCommand; final @Nullable State expireState; @@ -315,19 +321,48 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener + * {@code <duration>[,(state=|command=|)<stateOrCommand>]}
* if neither state= or command= is present, assume state + * + * {@code duration} is a string of the form "1d1h15m30s" or "1d" or "1h" or "15m" or "30s", + * or an ISO-8601 duration string (e.g. "PT1H15M30S"). + * + * {@code configuration} is a map of configuration keys and values: + * - {@code duration}: the duration string + * - {@code command}: the {@link Command} to send when the item expires + * - {@code state}: the {@link State} to send when the item expires + * - {@code ignoreStateUpdates}: if true, ignore state updates + * - {@code ignoreCommands}: if true, ignore commands + * + * - When neither command nor state is specified, the default is to post an {@link UNDEF} state. * * @param item the item to which we are bound - * @param configString the string that the user specified in the metadate - * @throws IllegalArgumentException if it is ill-formatted + * @param configString the string that the user specified in the metadata + * @param configuration the configuration map + * @throws IllegalArgumentException if it is ill-formatted, or the configuration contains an unknown key, + * or any setting is specified more than once */ public ExpireConfig(Item item, String configString, Map configuration) throws IllegalArgumentException { int commaPos = configString.indexOf(','); + String commandString = null; + String stateString = null; - durationString = (commaPos >= 0) ? configString.substring(0, commaPos).trim() : configString.trim(); + String durationStr = (commaPos >= 0) ? configString.substring(0, commaPos).trim() : configString.trim(); + if (configuration.containsKey(CONFIG_DURATION)) { + if (!durationStr.isEmpty()) { + throw new IllegalArgumentException("Expire duration for item " + item.getName() + + " is specified in both the value string and the configuration"); + } + durationStr = configuration.get(CONFIG_DURATION).toString(); + } + + durationString = durationStr; duration = parseDuration(durationString); + if (duration.isNegative()) { + throw new IllegalArgumentException( + "Expire duration for item " + item.getName() + " must be a positive value"); + } String stateOrCommand = ((commaPos >= 0) && (configString.length() - 1) > commaPos) ? configString.substring(commaPos + 1).trim() @@ -336,40 +371,70 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener unknownKeys = new HashSet(configuration.keySet()); + unknownKeys.removeAll(CONFIG_KEYS); + throw new IllegalArgumentException( + "Expire configuration for item " + item.getName() + " contains unknown keys: " + unknownKeys); + } } /** @@ -394,21 +459,31 @@ public class ExpireManager implements EventSubscriber, RegistryChangeListener new ExpireManager.ExpireConfig(testItem, "1h", Map.of("duration", "1h"))); + assertThrows(IllegalArgumentException.class, + () -> new ExpireManager.ExpireConfig(testItem, "1h,state=ON", Map.of("state", "ON"))); + assertThrows(IllegalArgumentException.class, + () -> new ExpireManager.ExpireConfig(testItem, "1h,state=ON", Map.of("command", "ON"))); + assertThrows(IllegalArgumentException.class, + () -> new ExpireManager.ExpireConfig(testItem, "1h,command=ON", Map.of("command", "ON"))); + assertThrows(IllegalArgumentException.class, + () -> new ExpireManager.ExpireConfig(testItem, "1h,command=ON", Map.of("state", "ON"))); + assertThrows(IllegalArgumentException.class, // + () -> new ExpireManager.ExpireConfig(testItem, "1h,command=ON", + Map.of("command", "ON", "state", "ON"))); + assertThrows(IllegalArgumentException.class, + () -> new ExpireManager.ExpireConfig(testItem, "1h", Map.of("command", "ON", "state", "ON"))); + } + + @Test + void testUnknownConfigKeys() { + Item testItem = new SwitchItem(ITEMNAME); + assertThrows(IllegalArgumentException.class, + () -> new ExpireManager.ExpireConfig(testItem, "1h", Map.of("unknownKey", "unknownValue"))); + } + + @Test + void testNegativeDuration() { + Item testItem = new SwitchItem(ITEMNAME); + assertThrows(IllegalArgumentException.class, () -> new ExpireManager.ExpireConfig(testItem, "-1h", Map.of())); + assertThrows(IllegalArgumentException.class, () -> new ExpireManager.ExpireConfig(testItem, "-PT1H", Map.of())); + } + private Metadata config(String metadata) { return new Metadata(METADATA_KEY, metadata, null); }