diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java index 26e57188a..bfd27d92b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java @@ -28,24 +28,28 @@ public class ControllerPollProperties implements Serializable { private static final long serialVersionUID = 1L; /** - * Maximum polling time that can be configured system-wide and by tenant in HH:mm:ss notation. - */ - private String maxPollingTime = "23:59:59"; - - /** - * Minimum polling time that can be configured by a tenant in HH:mm:ss notation. + * Minimum polling time that can be configured by a tenant in HH:mm:ss or ISO-8601 notation. */ private String minPollingTime = "00:00:30"; /** - * Controller polling time that can be configured system-wide and by tenant in HH:mm:ss(~\d{1,2}%)? notation, plus + * Maximum polling time that can be configured system-wide and by tenant in HH:mm:ss or ISO-8601 notation. + */ + private String maxPollingTime = "23:59:59"; + + /** + * Controller polling time that can be configured system-wide and by tenant in (HH:mm:ss|ISO-8601)(~\d{1,2}%)? notation, plus * followed (optionally and ordered) by a comma separated @lt;QL filter@gt; -@gt; polling time that overrides the * default polling time for the targets that match the filter. + *

+ * If the computed polling time returned by DDI to device (i.e. poling time + deviation) is not less then a day then it will be + * sent to device in ISO-8601 format. This may brake backward compatibility for devices expecting polling time in HH:mm:ss format. + * In order to prevent this, for legacy devices, keep the maxPollingTime less than a day. */ private String pollingTime = "00:05:00"; /** - * Controller polling overdue time that can be configured system-wide and by tenant in HH:mm:ss notation. + * Controller polling overdue time that can be configured system-wide and by tenant in HH:mm:ss or ISO-8601 notation. */ private String pollingOverdueTime = "00:05:00"; diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java index 8f7a6af21..ba987f5aa 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java @@ -18,39 +18,36 @@ import lombok.NoArgsConstructor; /** * This class is a helper for converting a duration into a string and for the other way. The string is in the format expected - * in configuration and database - in {@link Duration} default format or in custom format like "01:00:00" or "01:01:50:50" - * (starting with seconds, minutes, hours, days from the end). + * in configuration and database - in {@link Duration} default format or in custom format like "01:00:00" or standard ISO-8601. */ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public final class DurationHelper { private static final DateTimeFormatter DURATION_FORMATER = DateTimeFormatter.ofPattern("HH:mm:ss"); - private static final long SECONDS_PER_DAY = 24 * 60 * 60L; // 24 hours * 60 minutes * 60 seconds private static final Duration DAY = Duration.ofDays(1); /** * Converts a Duration into a formatted String * * @param duration duration, which will be converted into a formatted String - * @return String in the duration format, specified as HH:mm:ss or d+:HH:mm:ss + * @return String in the duration format, specified as HH:mm:ss (of possible, i.e. less than a day) or ISO-8601 format */ public static String toString(final Duration duration) { if (duration == null) { return null; } - if (duration.compareTo(DAY) < 0) { // backward compatible HH:mm:ss + if (duration.compareTo(DAY) < 0) { // backward compatible HH:mm:ss (if possible, could be sent to devices via DDI) return LocalTime.ofSecondOfDay(duration.toSeconds()).format(DURATION_FORMATER); - } else { // custom format d+:HH:mm:ss - return duration.toDays() + ":" + LocalTime.ofSecondOfDay(duration.toSeconds() % SECONDS_PER_DAY).format(DURATION_FORMATER); + } else { // ISO-8601 + return duration.toString(); } } /** * Converts a formatted String into a Duration object. * - * @param durationStr String in {@link Duration} default format or in custom format like "01:00:00" or "01:01:50:50" - * (starting with seconds, minutes, hours, days from the end) + * @param durationStr String in {@link Duration} default format or in custom format like "01:00:00" or standard ISO-8601 * @return duration as a {@link Duration} object * @throws DateTimeParseException when String is in wrong format */ @@ -76,12 +73,6 @@ public final class DurationHelper { .ofHours(Long.parseLong(split[0])) .plusMinutes(Long.parseLong(split[1])) .plusSeconds(Long.parseLong(split[2])); - } else if (split.length == 4) { // d:HH:mm:ss - return Duration - .ofDays(Long.parseLong(split[0])) - .plusHours(Long.parseLong(split[1])) - .plusMinutes(Long.parseLong(split[2])) - .plusSeconds(Long.parseLong(split[3])); } else { throw new IllegalArgumentException("No more then 4 chunks (split by ':') are allowed in duration"); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java index fbfec15f2..c43b1f731 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java @@ -10,9 +10,7 @@ package org.eclipse.hawkbit.tenancy.configuration; import java.time.Duration; -import java.time.format.DateTimeParseException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Random; @@ -25,31 +23,35 @@ import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorExce @Value public class PollingTime { - private static final Pattern OVERRIDE_PATTERN = Pattern.compile( - "\\s{0,5},\\s{0,5}(?[^,]*)\\s{0,5}->\\s{0,5}(?" + PollingInterval.POLLING_INTERVALE_REGEX + ")\\s{0,5}"); - PollingInterval pollingInterval; List overrides; + @SuppressWarnings("java:S127") public PollingTime(final String pollingTime) { final int indexOfComma = pollingTime.indexOf(','); if (indexOfComma == -1) { // no overrides pollingInterval = new PollingInterval(pollingTime); - overrides = Collections.emptyList(); + overrides = List.of(); } else { // Extract the main polling interval and overrides final String pollingIntervalStr = pollingTime.substring(0, indexOfComma); pollingInterval = new PollingInterval(pollingIntervalStr); overrides = new ArrayList<>(); - final String overridesStr = pollingTime.substring(indexOfComma).trim(); // with initial comma - final Matcher overridesMatcher = OVERRIDE_PATTERN.matcher(overridesStr); - for (int start = 0; start < overridesStr.length(); start = overridesMatcher.end()) { - if (overridesMatcher.find(start)) { - overrides.add(new Override( - overridesMatcher.group("qlStr").trim(), - new PollingInterval(overridesMatcher.group("pollInterval").trim()))); + final String overridesStr = pollingTime.substring(indexOfComma + 1).trim(); + for (int start = 0; ; ) { + final int separatorIndex = overridesStr.indexOf("->", start); + if (separatorIndex == -1) { + throw new TenantConfigurationValidatorException("Invalid pollingTime override: '" + overridesStr.substring(start) + "'"); } else { - throw new TenantConfigurationValidatorException("Invalid pollingTime overrides: " + overridesStr); + final String ql = overridesStr.substring(start, separatorIndex).trim(); + final int nextCommaIndex = overridesStr.indexOf(',', separatorIndex); + if (nextCommaIndex == -1) { // last override + overrides.add(new Override(ql, new PollingInterval(overridesStr.substring(separatorIndex + 2).trim()))); + break; + } else { + overrides.add(new Override(ql, new PollingInterval(overridesStr.substring(separatorIndex + 2, nextCommaIndex).trim()))); + start = nextCommaIndex + 1; + } } } } @@ -61,7 +63,7 @@ public class PollingTime { @SuppressWarnings("java:S1068") // used for random delay only, no need of secure random private static final Random RANDOM = new Random(); - public static final String POLLING_INTERVALE_REGEX = "\\s{0,5}(?\\d{2}:[0-5]\\d:[0-5]\\d)\\s{0,5}(~(?\\d{1,2})%)?\\s{0,5}"; + private static final String POLLING_INTERVALE_REGEX = "(?[^~]+)(~(?\\d{1,2})%)?\\s{0,5}"; private static final Pattern POLLING_INTERVAL_PATTERN = Pattern.compile(POLLING_INTERVALE_REGEX); Duration interval; @@ -69,16 +71,17 @@ public class PollingTime { public PollingInterval(final String pollingInterval) { final Matcher matcher = POLLING_INTERVAL_PATTERN.matcher(pollingInterval); - if (matcher.matches()) { - try { - this.interval = DurationHelper.fromString(matcher.group("pollingInterval")); - } catch (final DateTimeParseException ex) { - throw new TenantConfigurationValidatorException( - "The given configuration value is expected as a string in the format HH:mm:ss(~\\d{1,2})?."); + try { + if (matcher.matches()) { + interval = DurationHelper.fromString(matcher.group("pollingInterval").trim()); + deviationPercent = Optional.ofNullable(matcher.group("deviationPercent")) + .map(String::trim).map(Integer::parseInt).orElse(0); + } else { + throw new IllegalArgumentException(); } - this.deviationPercent = Optional.ofNullable(matcher.group("deviationPercent")).map(Integer::parseInt).orElse(0); - } else { - throw new TenantConfigurationValidatorException("Invalid pollingInterval: " + pollingInterval); + } catch (final Exception e) { + throw new TenantConfigurationValidatorException( + "Invalid pollingInterval: '" + pollingInterval + "', expecting: (HH:mm:ss|ISO-8601)(~\\d{1,2}%)?"); } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java index d741499b3..3d73fe6cf 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java +++ b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java @@ -41,6 +41,7 @@ class PollingTimeTest { @Test void testComplexWithOverrides() { assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync -> 00:05:00"); + assertExpectedComplexWithOverrides("PT1H~10%, group == 'eu' -> PT2M~15%, status != in_sync -> PT5M"); } @Test @@ -48,16 +49,36 @@ class PollingTimeTest { assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync ->00:05:00"); assertExpectedComplexWithOverrides(" 01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync ->00:05:00 "); assertExpectedComplexWithOverrides(" 01:00:00~10% , group == 'eu' -> 00:02:00 ~15%, status != in_sync ->00:05:00 "); + assertExpectedComplexWithOverrides("PT1H~10%, group == 'eu' -> PT2M~15%, status != in_sync ->PT5M"); + assertExpectedComplexWithOverrides(" PT1H~10%, group == 'eu' -> PT2M~15%, status != in_sync ->PT5M"); + assertExpectedComplexWithOverrides(" PT1H~10% , group == 'eu' -> PT2M~15%, status != in_sync ->PT5M"); + } + + @Test + void testComplexWithOverridesWithCommaInQl() { + assertExpectedComplexWithOverrides( + "01:00:00~10%, group == 'eu' -> 00:02:00~15%, status =in= (in_sync,pending) -> 00:05:00", + "group == 'eu'", "status =in= (in_sync,pending)"); + assertExpectedComplexWithOverrides( + "PT1H~10%, group == 'eu' -> PT2M~15%, status =in= (in_sync,pending) -> PT5M", + "group == 'eu'", "status =in= (in_sync,pending)"); + assertExpectedComplexWithOverrides( + "01:00:00~10%, group == 'eu' -> 00:02:00~15%, status =in= (in_sync,pending) -> 00:05:00, region == us -> 00:10:00", + "group == 'eu'", "status =in= (in_sync,pending)", "region == us"); } private static void assertExpectedComplexWithOverrides(final String pollingTimeStr) { + assertExpectedComplexWithOverrides(pollingTimeStr, "group == 'eu'", "status != in_sync"); + } + + private static void assertExpectedComplexWithOverrides(final String pollingTimeStr, final String... expectedQls) { final PollingTime pollingTime = new PollingTime(pollingTimeStr); assertThat(pollingTime.getPollingInterval().getInterval()).hasToString("PT1H"); assertThat(pollingTime.getPollingInterval().getDeviationPercent()).isEqualTo(10); - assertThat(pollingTime.getOverrides().get(0).qlStr()).isEqualTo("group == 'eu'"); + assertThat(pollingTime.getOverrides().get(0).qlStr()).isEqualTo(expectedQls[0]); assertThat(pollingTime.getOverrides().get(0).pollingInterval().getInterval()).hasToString("PT2M"); assertThat(pollingTime.getOverrides().get(0).pollingInterval().getDeviationPercent()).isEqualTo(15); - assertThat(pollingTime.getOverrides().get(1).qlStr()).isEqualTo("status != in_sync"); + assertThat(pollingTime.getOverrides().get(1).qlStr()).isEqualTo(expectedQls[1]); assertThat(pollingTime.getOverrides().get(1).pollingInterval().getInterval()).hasToString("PT5M"); assertThat(pollingTime.getOverrides().get(1).pollingInterval().getDeviationPercent()).isZero(); }