14
14
15
15
import java .time .Duration ;
16
16
import java .time .Instant ;
17
+ import java .util .HashSet ;
17
18
import java .util .Map ;
18
19
import java .util .Optional ;
19
20
import java .util .Set ;
@@ -289,19 +290,24 @@ public void updated(Metadata oldElement, Metadata element) {
289
290
}
290
291
291
292
static class ExpireConfig {
293
+ static final String CONFIG_DURATION = "duration" ;
294
+ static final String CONFIG_COMMAND = "command" ;
295
+ static final String CONFIG_STATE = "state" ;
292
296
static final String CONFIG_IGNORE_STATE_UPDATES = "ignoreStateUpdates" ;
293
297
static final String CONFIG_IGNORE_COMMANDS = "ignoreCommands" ;
298
+ static final Set <String > CONFIG_KEYS = Set .of (CONFIG_DURATION , CONFIG_COMMAND , CONFIG_STATE ,
299
+ CONFIG_IGNORE_STATE_UPDATES , CONFIG_IGNORE_COMMANDS );
294
300
295
301
private static final StringType STRING_TYPE_NULL_HYPHEN = new StringType ("'NULL'" );
296
302
private static final StringType STRING_TYPE_NULL = new StringType ("NULL" );
297
303
private static final StringType STRING_TYPE_UNDEF_HYPHEN = new StringType ("'UNDEF'" );
298
304
private static final StringType STRING_TYPE_UNDEF = new StringType ("UNDEF" );
299
305
300
- protected static final String COMMAND_PREFIX = "command =" ;
301
- protected static final String STATE_PREFIX = "state =" ;
306
+ protected static final String COMMAND_PREFIX = CONFIG_COMMAND + " =" ;
307
+ protected static final String STATE_PREFIX = CONFIG_STATE + " =" ;
302
308
303
- protected static final Pattern DURATION_PATTERN = Pattern
304
- . compile ( " (?:([0-9]+)H)?\\ s*(?:([0-9]+)M)?\\ s*(?:([0-9]+)S)?" , Pattern .CASE_INSENSITIVE );
309
+ protected static final Pattern DURATION_PATTERN = Pattern . compile (
310
+ "(?:([0-9]+)D)? \\ s* (?:([0-9]+)H)?\\ s*(?:([0-9]+)M)?\\ s*(?:([0-9]+)S)?" , Pattern .CASE_INSENSITIVE );
305
311
306
312
final @ Nullable Command expireCommand ;
307
313
final @ Nullable State expireState ;
@@ -315,19 +321,48 @@ static class ExpireConfig {
315
321
*
316
322
* Valid syntax:
317
323
*
318
- * {@code <duration>[,(state=|command=|)<stateOrCommand>][,ignoreStateUpdates][,ignoreCommands] }<br>
324
+ * {@code <duration>[,(state=|command=|)<stateOrCommand>]}<br>
319
325
* if neither state= or command= is present, assume state
326
+ *
327
+ * {@code duration} is a string of the form "1d1h15m30s" or "1d" or "1h" or "15m" or "30s",
328
+ * or an ISO-8601 duration string (e.g. "PT1H15M30S").
329
+ *
330
+ * {@code configuration} is a map of configuration keys and values:
331
+ * - {@code duration}: the duration string
332
+ * - {@code command}: the {@link Command} to send when the item expires
333
+ * - {@code state}: the {@link State} to send when the item expires
334
+ * - {@code ignoreStateUpdates}: if true, ignore state updates
335
+ * - {@code ignoreCommands}: if true, ignore commands
336
+ *
337
+ * - When neither command nor state is specified, the default is to post an {@link UNDEF} state.
320
338
*
321
339
* @param item the item to which we are bound
322
- * @param configString the string that the user specified in the metadate
323
- * @throws IllegalArgumentException if it is ill-formatted
340
+ * @param configString the string that the user specified in the metadata
341
+ * @param configuration the configuration map
342
+ * @throws IllegalArgumentException if it is ill-formatted, or the configuration contains an unknown key,
343
+ * or any setting is specified more than once
324
344
*/
325
345
public ExpireConfig (Item item , String configString , Map <String , Object > configuration )
326
346
throws IllegalArgumentException {
327
347
int commaPos = configString .indexOf (',' );
348
+ String commandString = null ;
349
+ String stateString = null ;
350
+
351
+ String durationStr = (commaPos >= 0 ) ? configString .substring (0 , commaPos ).trim () : configString .trim ();
352
+ if (configuration .containsKey (CONFIG_DURATION )) {
353
+ if (!durationStr .isEmpty ()) {
354
+ throw new IllegalArgumentException ("Expire duration for item " + item .getName ()
355
+ + " is specified in both the value string and the configuration" );
356
+ }
357
+ durationStr = configuration .get (CONFIG_DURATION ).toString ();
358
+ }
328
359
329
- durationString = ( commaPos >= 0 ) ? configString . substring ( 0 , commaPos ). trim () : configString . trim () ;
360
+ durationString = durationStr ;
330
361
duration = parseDuration (durationString );
362
+ if (duration .isNegative ()) {
363
+ throw new IllegalArgumentException (
364
+ "Expire duration for item " + item .getName () + " must be a positive value" );
365
+ }
331
366
332
367
String stateOrCommand = ((commaPos >= 0 ) && (configString .length () - 1 ) > commaPos )
333
368
? configString .substring (commaPos + 1 ).trim ()
@@ -336,40 +371,70 @@ public ExpireConfig(Item item, String configString, Map<String, Object> configur
336
371
ignoreStateUpdates = getBooleanConfigValue (configuration , CONFIG_IGNORE_STATE_UPDATES );
337
372
ignoreCommands = getBooleanConfigValue (configuration , CONFIG_IGNORE_COMMANDS );
338
373
374
+ if (configuration .containsKey (CONFIG_COMMAND )) {
375
+ commandString = configuration .get (CONFIG_COMMAND ).toString ();
376
+ }
377
+
378
+ if (configuration .containsKey (CONFIG_STATE )) {
379
+ if (commandString != null ) {
380
+ throw new IllegalArgumentException (
381
+ "Expire configuration for item " + item .getName () + " contains both command and state" );
382
+ }
383
+ stateString = configuration .get (CONFIG_STATE ).toString ();
384
+ }
385
+
339
386
if ((stateOrCommand != null ) && (!stateOrCommand .isEmpty ())) {
387
+ if (commandString != null || stateString != null ) {
388
+ throw new IllegalArgumentException ("Expire state/command for item " + item .getName ()
389
+ + " is specified in both the value string and the configuration" );
390
+ }
391
+
340
392
if (stateOrCommand .startsWith (COMMAND_PREFIX )) {
341
- String commandString = stateOrCommand .substring (COMMAND_PREFIX .length ());
342
- expireCommand = TypeParser .parseCommand (item .getAcceptedCommandTypes (), commandString );
343
- expireState = null ;
344
- if (expireCommand == null ) {
345
- throw new IllegalArgumentException ("The string '" + commandString
346
- + "' does not represent a valid command for item " + item .getName ());
347
- }
393
+ commandString = stateOrCommand .substring (COMMAND_PREFIX .length ());
348
394
} else {
349
395
if (stateOrCommand .startsWith (STATE_PREFIX )) {
350
396
stateOrCommand = stateOrCommand .substring (STATE_PREFIX .length ());
351
397
}
352
- String stateString = stateOrCommand ;
353
- State state = TypeParser .parseState (item .getAcceptedDataTypes (), stateString );
354
- // do special handling to allow NULL and UNDEF as strings when being put in single quotes
355
- if (STRING_TYPE_NULL_HYPHEN .equals (state )) {
356
- expireState = STRING_TYPE_NULL ;
357
- } else if (STRING_TYPE_UNDEF_HYPHEN .equals (state )) {
358
- expireState = STRING_TYPE_UNDEF ;
359
- } else {
360
- expireState = state ;
361
- }
362
- expireCommand = null ;
363
- if (expireState == null ) {
364
- throw new IllegalArgumentException ("The string '" + stateString
365
- + "' does not represent a valid state for item " + item .getName ());
366
- }
398
+ stateString = stateOrCommand ;
399
+ }
400
+ }
401
+
402
+ if (commandString != null ) {
403
+ expireCommand = TypeParser .parseCommand (item .getAcceptedCommandTypes (), commandString );
404
+ expireState = null ;
405
+ if (expireCommand == null ) {
406
+ throw new IllegalArgumentException ("The string '" + commandString
407
+ + "' does not represent a valid command for item " + item .getName ());
408
+ }
409
+ } else if (stateString != null ) {
410
+ // default is to post state
411
+ expireCommand = null ;
412
+ State state = TypeParser .parseState (item .getAcceptedDataTypes (), stateString );
413
+ // do special handling to allow NULL and UNDEF as strings when being put in single quotes
414
+ if (STRING_TYPE_NULL_HYPHEN .equals (state )) {
415
+ expireState = STRING_TYPE_NULL ;
416
+ } else if (STRING_TYPE_UNDEF_HYPHEN .equals (state )) {
417
+ expireState = STRING_TYPE_UNDEF ;
418
+ } else {
419
+ expireState = state ;
420
+ }
421
+
422
+ if (expireState == null ) {
423
+ throw new IllegalArgumentException ("The string '" + stateString
424
+ + "' does not represent a valid state for item " + item .getName ());
367
425
}
368
426
} else {
369
427
// default is to post Undefined state
370
428
expireCommand = null ;
371
429
expireState = UnDefType .UNDEF ;
372
430
}
431
+
432
+ if (!CONFIG_KEYS .containsAll (configuration .keySet ())) {
433
+ Set <String > unknownKeys = new HashSet <String >(configuration .keySet ());
434
+ unknownKeys .removeAll (CONFIG_KEYS );
435
+ throw new IllegalArgumentException (
436
+ "Expire configuration for item " + item .getName () + " contains unknown keys: " + unknownKeys );
437
+ }
373
438
}
374
439
375
440
/**
@@ -394,21 +459,31 @@ private boolean getBooleanConfigValue(Map<String, Object> configuration, String
394
459
}
395
460
396
461
private Duration parseDuration (String durationString ) throws IllegalArgumentException {
462
+ try {
463
+ return Duration .parse (durationString );
464
+ } catch (Exception e ) {
465
+ // ignore
466
+ }
467
+
397
468
Matcher m = DURATION_PATTERN .matcher (durationString );
398
- if (!m .matches () || (m .group (1 ) == null && m .group (2 ) == null && m .group (3 ) == null )) {
469
+ if (!m .matches ()
470
+ || (m .group (1 ) == null && m .group (2 ) == null && m .group (3 ) == null && m .group (4 ) == null )) {
399
471
throw new IllegalArgumentException (
400
- "Invalid duration: " + durationString + ". Expected something like: '1h 15m 30s'" );
472
+ "Invalid duration: " + durationString + ". Expected something like: '1d 1h 15m 30s'" );
401
473
}
402
474
403
475
Duration duration = Duration .ZERO ;
404
476
if (m .group (1 ) != null ) {
405
- duration = duration .plus (Duration .ofHours (Long .parseLong (m .group (1 ))));
477
+ duration = duration .plus (Duration .ofDays (Long .parseLong (m .group (1 ))));
406
478
}
407
479
if (m .group (2 ) != null ) {
408
- duration = duration .plus (Duration .ofMinutes (Long .parseLong (m .group (2 ))));
480
+ duration = duration .plus (Duration .ofHours (Long .parseLong (m .group (2 ))));
409
481
}
410
482
if (m .group (3 ) != null ) {
411
- duration = duration .plus (Duration .ofSeconds (Long .parseLong (m .group (3 ))));
483
+ duration = duration .plus (Duration .ofMinutes (Long .parseLong (m .group (3 ))));
484
+ }
485
+ if (m .group (4 ) != null ) {
486
+ duration = duration .plus (Duration .ofSeconds (Long .parseLong (m .group (4 ))));
412
487
}
413
488
return duration ;
414
489
}
0 commit comments