Add support for pollingTime overrides (#2533)

* Add support for pollingTime overrides

* the current format HH:mm:ss is still supported
* add option for deviation percent (HH:mm:ss~\d{1,2}%) which allows the system to do some randomizing of the poll interval
* add support for overriding default polling time interval for devices matching some RSQL filters (in order), e.g. 01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync -> 00:05:00
* IMPORTANT: overdue time is calculated according to the default polling time. So, the overdue status might be incorrect for targets with overridden poll interval

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* Remove min polling time from the tenant config - it is a system configuration

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* Add support for bigger poll intervals and overdue + duration format config support

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-07 16:33:55 +03:00
committed by GitHub
parent baab05f009
commit 7f97d6f441
31 changed files with 664 additions and 514 deletions

View File

@@ -35,6 +35,7 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -188,38 +189,24 @@ public interface ControllerManagement {
/**
* Returns configured polling interval at which the controller polls hawkBit server.
*
* @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}.
* @param target {@link Target} for which polling time is calculated (it could be overridden for a specific targets).
* @return current {@link TenantConfigurationKey#POLLING_TIME}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
String getPollingTime();
/**
* Returns the configured minimum polling interval.
*
* @return current {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
String getMinPollingTime();
/**
* Returns the count to be used for reducing polling interval while calling {@link ControllerManagement#getPollingTimeForAction(Action)}.
*
* @return configured value of {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
int getMaintenanceWindowPollCount();
String getPollingTime(Target target);
/**
* Returns polling time based on the maintenance window for an action. Server will reduce the polling interval as the start time for
* maintenance window approaches, so that at least these many attempts are made between current polling until start of maintenance window.
* Poll time keeps reducing with MinPollingTime as lower limit {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. After the start
* of maintenance window, it resets to default {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}.
* Poll time keeps reducing with MinPollingTime as lower limit {@link ControllerPollProperties#getMinPollingTime()}. After the start
* of maintenance window, it resets to default {@link TenantConfigurationKey#POLLING_TIME}.
*
* @param target {@link Target} for which polling time is calculated
* @param action {@link Action} for which polling time is calculated based on it having maintenance window or not
* @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}.
* @return current {@link TenantConfigurationKey#POLLING_TIME}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
String getPollingTimeForAction(Action action);
String getPollingTimeForAction(Target target, Action action);
/**
* Checks if a given target has currently or has even been assigned to the given artifact through the action history list. This can e.g.

View File

@@ -31,11 +31,10 @@ public interface TenantConfigurationManagement {
* Adds or updates a specific configuration for a specific tenant.
*
* @param configurationKeyName the key of the configuration
* @param value the configuration value which will be written into the
* database.
* @param value the configuration value which will be written into the database.
* @return the configuration value which was just written into the database.
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in general does not
* match the expected type and format defined by the Key
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in general does not match the expected type and
* format defined by the Key
* @throws ConversionFailedException if the property cannot be converted to the given
*/
@PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION)

View File

@@ -16,11 +16,9 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Defines global configuration for the controllers/clients on the provisioning
* targets/devices.
*
* Note: many of the controller related properties can be overridden on tenant
* level.
* Defines global configuration for the controllers/clients on the provisioning targets/devices.
* <p/>
* Note: many of the controller related properties can be overridden on tenant level.
*/
@Data
@ConfigurationProperties(prefix = "hawkbit.controller")
@@ -30,22 +28,24 @@ 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.
* 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 notation.
*/
private String minPollingTime = "00:00:30";
/**
* Controller polling time that can be configured system-wide and by tenant in HH:MM:SS notation.
* Controller polling time that can be configured system-wide and by tenant in HH:mm:ss(~\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.
*/
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 notation.
*/
private String pollingOverdueTime = "00:05:00";

View File

@@ -13,101 +13,78 @@ import java.time.Duration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
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
* {@link #DURATION_FORMAT}.
* 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).
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public final class DurationHelper {
/**
* Format of the String expected in configuration file and in the database.
*/
public static final String DURATION_FORMAT = "HH:mm:ss";
private DurationHelper() {
// utility class
}
/**
* Creates a {@link DurationRangeValidator}.
*
* @param min imum of range.
* @param max imum of range.
* @return {@link DurationRangeValidator} range.
*/
public static DurationRangeValidator durationRangeValidator(final Duration min, final Duration max) {
return new DurationRangeValidator(min, max);
}
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 at
* {@link #DURATION_FORMAT}
* @return String in the duration format, specified as HH:mm:ss or d+:HH:mm:ss
*/
public static String durationToFormattedString(final Duration duration) {
public static String toString(final Duration duration) {
if (duration == null) {
return null;
}
return LocalTime.ofNanoOfDay(duration.toNanos()).format(DateTimeFormatter.ofPattern(DURATION_FORMAT));
if (duration.compareTo(DAY) < 0) { // backward compatible HH:mm:ss
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);
}
}
/**
* Converts a formatted String into a Duration object.
*
* @param formattedDuration String in {@link #DURATION_FORMAT}
* @return duration
* @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)
* @return duration as a {@link Duration} object
* @throws DateTimeParseException when String is in wrong format
*/
public static Duration formattedStringToDuration(final String formattedDuration) {
if (formattedDuration == null) {
public static Duration fromString(final String durationStr) {
if (durationStr == null) {
return null;
}
final TemporalAccessor ta = DateTimeFormatter.ofPattern(DURATION_FORMAT).parse(formattedDuration.trim());
return Duration.between(LocalTime.MIDNIGHT, LocalTime.from(ta));
}
/**
* converts values of time constants to a Duration object..
*
* @param hours count of hours
* @param minutes count of minutes
* @param seconds count of seconds
* @return duration
*/
public static Duration getDurationByTimeValues(final long hours, final long minutes, final long seconds) {
return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds);
}
/**
* Duration validation utility class. Checks if the requested duration is in
* the defined min/max range.
*/
public static final class DurationRangeValidator {
private final Duration min;
private final Duration max;
private DurationRangeValidator(final Duration min, final Duration max) {
this.min = min;
this.max = max;
}
/**
* Checks if the requested duration is in the defined min/max range.
*
* @param duration to checked
* @return <code>true</code> if in time range
*/
public boolean isWithinRange(final Duration duration) {
return duration.compareTo(min) >= 0 && duration.compareTo(max) <= 0;
if (durationStr.charAt(0) == 'P') {
// Handle ISO-8601 format, e.g., "PT1H30M"
return Duration.parse(durationStr);
} else {
// Handle custom format, e.g., "01:00:00" or "01:01:50:50"
final String[] split = durationStr.split(":");
if (split.length == 1) { // ss
return Duration.ofSeconds(Long.parseLong(split[0]));
} else if (split.length == 2) { // mm:ss
return Duration
.ofMinutes(Long.parseLong(split[0]))
.plusSeconds(Long.parseLong(split[1]));
} else if (split.length == 3) { // HH:mm:ss
return Duration
.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");
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Value;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
@Value
public class PollingTime {
private static final Pattern OVERRIDE_PATTERN = Pattern.compile(
"\\s{0,5},\\s{0,5}(?<qlStr>[^,]*)\\s{0,5}->\\s{0,5}(?<pollInterval>" + PollingInterval.POLLING_INTERVALE_REGEX + ")\\s{0,5}");
PollingInterval pollingInterval;
List<Override> overrides;
public PollingTime(final String pollingTime) {
final int indexOfComma = pollingTime.indexOf(',');
if (indexOfComma == -1) { // no overrides
pollingInterval = new PollingInterval(pollingTime);
overrides = Collections.emptyList();
} 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())));
} else {
throw new TenantConfigurationValidatorException("Invalid pollingTime overrides: " + overridesStr);
}
}
}
}
@Value
public static class PollingInterval {
private static final Random RANDOM = new Random();
public static final String POLLING_INTERVALE_REGEX = "\\s{0,5}(?<pollingInterval>\\d{2}:[0-5]\\d:[0-5]\\d)\\s{0,5}(~(?<deviationPercent>\\d{1,2})%)?\\s{0,5}";
private static final Pattern POLLING_INTERVAL_PATTERN = Pattern.compile(POLLING_INTERVALE_REGEX);
Duration interval;
int deviationPercent;
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})?.");
}
this.deviationPercent = Optional.ofNullable(matcher.group("deviationPercent")).map(Integer::parseInt).orElse(0);
} else {
throw new TenantConfigurationValidatorException("Invalid pollingInterval: " + pollingInterval);
}
}
public String getFormattedIntervalWithDeviation(final Duration minPollingTime, final Duration maxPollingTime) {
if (deviationPercent > 0) {
final long millis = interval.toMillis();
final long maxDeviationMillis = (millis * deviationPercent) / 100;
final long deviation = RANDOM.nextLong(-maxDeviationMillis, maxDeviationMillis + 1);
if (deviation != 0) {
final Duration intervalWithDeviation = Duration.ofMillis(millis + deviation);
if (minPollingTime != null && intervalWithDeviation.compareTo(minPollingTime) < 0) {
return DurationHelper.toString(minPollingTime);
} else if (maxPollingTime != null && intervalWithDeviation.compareTo(maxPollingTime) > 0) {
return DurationHelper.toString(maxPollingTime);
} else {
return DurationHelper.toString(intervalWithDeviation);
}
}
}
return DurationHelper.toString(interval);
}
}
// This record holds the override information for a specific QL string and its associated polling interval.
public record Override(String qlStr, PollingInterval pollingInterval) {}
}

View File

@@ -67,50 +67,41 @@ public class TenantConfigurationProperties {
/**
* Header based authentication enabled.
*/
public static final String AUTHENTICATION_MODE_HEADER_ENABLED = "authentication.header.enabled";
public static final String AUTHENTICATION_HEADER_ENABLED = "authentication.header.enabled";
/**
* Header based authentication authority name.
*/
public static final String AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME = "authentication.header.authority";
public static final String AUTHENTICATION_HEADER_AUTHORITY_NAME = "authentication.header.authority";
/**
* Target token based authentication enabled.
*/
public static final String AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled";
public static final String AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled";
/**
* Gateway token based authentication enabled.
*/
public static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled";
public static final String AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled";
/**
* Gateway token value.
*/
public static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key";
public static final String AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key";
/**
* See system default in
* {@link ControllerPollProperties#getPollingTime()}.
* See system default in {@link ControllerPollProperties#getPollingTime()}.
*/
public static final String POLLING_TIME_INTERVAL = "pollingTime";
public static final String POLLING_TIME = "pollingTime";
/**
* See system default in
* {@link ControllerPollProperties#getMinPollingTime()}.
* See system default in {@link ControllerPollProperties#getPollingOverdueTime()}.
*/
public static final String MIN_POLLING_TIME_INTERVAL = "minPollingTime";
public static final String POLLING_OVERDUE_TIME = "pollingOverdueTime";
/**
* See system default in
* {@link ControllerPollProperties#getMaintenanceWindowPollCount()}.
* See system default in {@link ControllerPollProperties#getMaintenanceWindowPollCount()}.
*/
public static final String MAINTENANCE_WINDOW_POLL_COUNT = "maintenanceWindowPollCount";
/**
* See system default in
* {@link ControllerPollProperties#getPollingOverdueTime()}.
*/
public static final String POLLING_OVERDUE_TIME_INTERVAL = "pollingOverdueTime";
/**
* Represents setting if approval for a rollout is needed.
*/
public static final String ROLLOUT_APPROVAL_ENABLED = "rollout.approval.enabled";
/**
* Repository on autoclose mode instead of canceling in case of new DS
* assignment over active actions.
* Repository on autoclose mode instead of canceling in case of new Distribution Set assignment over active actions.
*/
public static final String REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED = "repository.actions.autoclose.enabled";
/**
@@ -118,7 +109,7 @@ public class TenantConfigurationProperties {
*/
public static final String ACTION_CLEANUP_ENABLED = "action.cleanup.enabled";
/**
* Specifies the action expiry in milli-seconds.
* Specifies the action expiry in milliseconds.
*/
public static final String ACTION_CLEANUP_ACTION_EXPIRY = "action.cleanup.actionExpiry";
/**
@@ -156,11 +147,11 @@ public class TenantConfigurationProperties {
/**
* Validates if an object matches the allowed data format of the corresponding key
*
* @param context application context
* @param value which will be validated
* @param context application context
* @throws TenantConfigurationValidatorException is thrown, when object is invalid
*/
public void validate(final ApplicationContext context, final Object value) {
public void validate(final Object value, final ApplicationContext context) {
if (validator == null) {
Objects.requireNonNull(DEFAULT_TYPE_VALIDATORS.get(dataType), "No validator defined for " + keyName).validate(value);
} else {

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration.validator;
import static org.eclipse.hawkbit.tenancy.configuration.DurationHelper.fromString;
import java.time.Duration;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
/**
* This class is used to validate, that the property is a String and that it is in the correct duration format.
*/
public class TenantConfigurationDurationValidator extends TenantConfigurationStringValidator {
// Exception squid:S1166 - Hide origin exception
@SuppressWarnings({ "squid:S1166" })
@Override
public void validate(final Object tenantConfigurationObject) {
super.validate(tenantConfigurationObject);
final String tenantConfigurationString = (String) tenantConfigurationObject;
final Duration duration = fromString(tenantConfigurationString);
if (duration.isNegative()) {
throw new TenantConfigurationValidatorException("The given configuration value is not in the allowed to be negative.");
}
}
}

View File

@@ -1,67 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration.validator;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
/**
* This class is used to validate, that the property is a String and that it is in the correct duration format.
*/
public class TenantConfigurationPollingDurationValidator implements TenantConfigurationValidator {
private final Duration minDuration;
private final Duration maxDuration;
/**
* This constructor is called by {@link org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties} using
* ApplicationContext.getAutowireCapableBeanFactory().createBean(Class) to validate the polling duration configuration.
* This insures the wiring of the properties is done correctly.
*
* @param properties property accessor for poll configuration
*/
public TenantConfigurationPollingDurationValidator(final ControllerPollProperties properties) {
minDuration = DurationHelper.formattedStringToDuration(properties.getMinPollingTime());
maxDuration = DurationHelper.formattedStringToDuration(properties.getMaxPollingTime());
}
@Override
// Exception squid:S1166 - Hide origin exception
@SuppressWarnings({ "squid:S1166" })
public void validate(final Object tenantConfigurationObject) {
TenantConfigurationValidator.super.validate(tenantConfigurationObject);
final String tenantConfigurationString = (String) tenantConfigurationObject;
final Duration tenantConfigurationValue;
try {
tenantConfigurationValue = DurationHelper.formattedStringToDuration(tenantConfigurationString);
} catch (final DateTimeParseException ex) {
throw new TenantConfigurationValidatorException(
String.format("The given configuration value is expected as a string in the format %s.",
DurationHelper.DURATION_FORMAT));
}
if (!DurationHelper.durationRangeValidator(minDuration, maxDuration).isWithinRange(tenantConfigurationValue)) {
throw new TenantConfigurationValidatorException(
String.format("The given configuration value is not in the allowed range from %s to %s.",
DurationHelper.durationToFormattedString(minDuration),
DurationHelper.durationToFormattedString(maxDuration)));
}
}
@Override
public Class<?> validateToClass() {
return String.class;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration.validator;
import static org.eclipse.hawkbit.tenancy.configuration.DurationHelper.fromString;
import java.time.Duration;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
/**
* This class is used to validate, that the property is a String and that it is in the correct polling time format.
*/
public class TenantConfigurationPollingTimeValidator extends TenantConfigurationStringValidator {
private final Duration minPollingInterval;
private final Duration maxPollingInterval;
/**
* This constructor is called by {@link org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties} using
* ApplicationContext.getAutowireCapableBeanFactory().createBean(Class) to validate the polling duration configuration.
* This insures the wiring of the properties is done correctly.
*
* @param properties property accessor for poll configuration
*/
public TenantConfigurationPollingTimeValidator(final ControllerPollProperties properties) {
this.minPollingInterval = fromString(properties.getMinPollingTime());
this.maxPollingInterval = fromString(properties.getMaxPollingTime());
}
@Override
public void validate(final Object tenantConfigurationObject) {
super.validate(tenantConfigurationObject);
final String tenantConfigurationString = (String) tenantConfigurationObject;
// validate parsable
final PollingTime pollingTime = new PollingTime(tenantConfigurationString);
// validate polling interval in range
validateInRange(pollingTime.getPollingInterval().getInterval());
for (final PollingTime.Override override : pollingTime.getOverrides()) {
validateInRange(override.pollingInterval().getInterval());
}
}
private void validateInRange(final Duration pollingInterval) {
if (pollingInterval.compareTo(minPollingInterval) < 0) {
throw new TenantConfigurationValidatorException(String.format(
"The polling interval is smaller then minimum polling interval. The allowed range is [%s, %s].",
DurationHelper.toString(minPollingInterval), DurationHelper.toString(maxPollingInterval)));
}
if (pollingInterval.compareTo(maxPollingInterval) > 0) {
throw new TenantConfigurationValidatorException(String.format(
"The polling interval is bigger then minimum polling interval. The allowed range is [%s, %s].",
DurationHelper.toString(minPollingInterval), DurationHelper.toString(maxPollingInterval)));
}
}
}

View File

@@ -12,12 +12,12 @@ package org.eclipse.hawkbit.tenancy.configuration.validator;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
/**
* base interface for clases which can validate tenant configuration values.
* base interface for classes which can validate tenant configuration values.
*/
public interface TenantConfigurationValidator {
/**
* validates the tenant configuration value
* Validates the tenant configuration value
*
* @param tenantConfigurationValue value which will be validated.
* @throws TenantConfigurationValidatorException is thrown, when parameter is invalid.

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
import org.junit.jupiter.api.Test;
class PollingTimeTest {
@Test
void testBackwardsCompatibility() {
final PollingTime pollingTime = new PollingTime("01:00:00");
assertThat(pollingTime.getPollingInterval().getInterval()).hasToString("PT1H");
assertThat(pollingTime.getPollingInterval().getDeviationPercent()).isZero();
assertThat(pollingTime.getOverrides()).isEmpty();
}
@Test
void testDeviation() {
final PollingTime pollingTime = new PollingTime("01:00:00~10%");
final long maxDeviation = (Duration.ofHours(1).toMillis() / 10);
final long deviation = Duration.ofHours(1).toMillis()
- DurationHelper.fromString(pollingTime.getPollingInterval().getFormattedIntervalWithDeviation(null, null)).toMillis();
assertThat(deviation)
.isGreaterThanOrEqualTo(-maxDeviation)
.isLessThanOrEqualTo(maxDeviation);
}
@Test
void testComplexWithOverrides() {
assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync -> 00:05:00");
}
@Test
void testComplexWithOverridesWithWhitespaces() {
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 ");
}
private static void assertExpectedComplexWithOverrides(final String pollingTimeStr) {
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).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).pollingInterval().getInterval()).hasToString("PT5M");
assertThat(pollingTime.getOverrides().get(1).pollingInterval().getDeviationPercent()).isZero();
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -17,6 +17,8 @@ import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime.PollingInterval;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
/**
@@ -29,19 +31,26 @@ public final class TimestampCalculator {
/**
* Calculates the overdue timestamp (<em>overdue_ts</em>) based on the current timestamp and the intervals for polling and poll-overdue:
* <p>
* <p/>
* <em>overdue_ts = now_ts - pollingInterval - pollingOverdueInterval</em>;<br>
* <em>pollingInterval</em> and <em>pollingOverdueInterval</em> are retrieved from tenant-specific system configuration.
* <p/>
* Note: this method checks against the default polling time interval. I.e. overrides are not considered.
*
* @return <em>overdue_ts</em> in milliseconds since Unix epoch as long value
*/
public static long calculateOverdueTimestamp() {
return System.currentTimeMillis() - getDurationForKey(TenantConfigurationKey.POLLING_TIME_INTERVAL).toMillis()
- getDurationForKey(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL).toMillis();
return calculateOverdueTimestamp(
new PollingTime(getRawStringForKey(TenantConfigurationKey.POLLING_TIME)).getPollingInterval(),
DurationHelper.fromString(getRawStringForKey(TenantConfigurationKey.POLLING_OVERDUE_TIME)));
}
private static Duration getDurationForKey(final String key) {
return DurationHelper.formattedStringToDuration(getRawStringForKey(key));
private static long calculateOverdueTimestamp(final PollingInterval pollingInterval, final Duration pollingOverdueTime) {
return System.currentTimeMillis()
- (pollingInterval.getDeviationPercent() == 0
? pollingInterval.getInterval().toMillis()
: pollingInterval.getInterval().toMillis() * (100 + pollingInterval.getDeviationPercent()) / 100)
- pollingOverdueTime.toMillis();
}
private static String getRawStringForKey(final String key) {

View File

@@ -8,7 +8,7 @@
# SPDX-License-Identifier: EPL-2.0
#
# Defines the polling time for the controllers in HH:MM:SS notation
# Defines the polling time for the controllers in HH:mm:ss notation
hawkbit.controller.pollingTime=00:05:00
hawkbit.controller.pollingOverdueTime=00:05:00
hawkbit.controller.maxPollingTime=23:59:59
@@ -52,24 +52,16 @@ hawkbit.server.tenant.configuration.authentication-gatewaytoken-key.defaultValue
hawkbit.server.tenant.configuration.polling-time.keyName=pollingTime
hawkbit.server.tenant.configuration.polling-time.defaultValue=${hawkbit.controller.pollingTime}
hawkbit.server.tenant.configuration.polling-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingDurationValidator
hawkbit.server.tenant.configuration.min-polling-time.keyName=minPollingTime
hawkbit.server.tenant.configuration.min-polling-time.defaultValue=${hawkbit.controller.minPollingTime}
hawkbit.server.tenant.configuration.min-polling-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingDurationValidator
hawkbit.server.tenant.configuration.polling-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingTimeValidator
hawkbit.server.tenant.configuration.polling-overdue-time.keyName=pollingOverdueTime
hawkbit.server.tenant.configuration.polling-overdue-time.defaultValue=${hawkbit.controller.pollingOverdueTime}
hawkbit.server.tenant.configuration.polling-overdue-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingDurationValidator
hawkbit.server.tenant.configuration.polling-overdue-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationDurationValidator
hawkbit.server.tenant.configuration.maintenance-window-poll-count.keyName=maintenanceWindowPollCount
hawkbit.server.tenant.configuration.maintenance-window-poll-count.defaultValue=${hawkbit.controller.maintenanceWindowPollCount}
hawkbit.server.tenant.configuration.maintenance-window-poll-count.dataType=java.lang.Integer
#hawkbit.server.tenant.configuration.anonymous-download-enabled.keyName=anonymous.download.enabled
#hawkbit.server.tenant.configuration.anonymous-download-enabled.defaultValue=${hawkbit.server.download.anonymous.enabled}
#hawkbit.server.tenant.configuration.anonymous-download-enabled.dataType=java.lang.Boolean
hawkbit.server.tenant.configuration.rollout-approval-enabled.keyName=rollout.approval.enabled
hawkbit.server.tenant.configuration.rollout-approval-enabled.defaultValue=false
hawkbit.server.tenant.configuration.rollout-approval-enabled.dataType=java.lang.Boolean

View File

@@ -161,6 +161,7 @@ import org.eclipse.hawkbit.security.SecurityTokenGenerator;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.springframework.beans.BeansException;
@@ -850,16 +851,15 @@ public class RepositoryApplicationConfiguration {
final DeploymentManagement deploymentManagement, final ConfirmationManagement confirmationManagement,
final SoftwareModuleRepository softwareModuleRepository, final SoftwareModuleMetadataRepository softwareModuleMetadataRepository,
final DistributionSetManagement distributionSetManagement,
final TenantConfigurationManagement tenantConfigurationManagement,
final TenantConfigurationManagement tenantConfigurationManagement, final ControllerPollProperties controllerPollProperties,
final PlatformTransactionManager txManager, final EntityFactory entityFactory, final EntityManager entityManager,
final AfterTransactionCommitExecutor afterCommit,
final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware,
final ScheduledExecutorService executorService) {
return new JpaControllerManagement(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties,
targetRepository, targetTypeManagement, deploymentManagement, confirmationManagement, softwareModuleRepository,
softwareModuleMetadataRepository, distributionSetManagement, tenantConfigurationManagement, txManager,
entityFactory, entityManager, afterCommit, systemSecurityContext, tenantAware,
executorService);
softwareModuleMetadataRepository, distributionSetManagement, tenantConfigurationManagement, controllerPollProperties,
txManager,entityFactory, entityManager, afterCommit, systemSecurityContext, tenantAware, executorService);
}
@Bean

View File

@@ -79,6 +79,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaAction_;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleMetadataRepository;
@@ -101,6 +102,9 @@ import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
@@ -116,6 +120,7 @@ import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
@@ -140,6 +145,8 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
private final SoftwareModuleMetadataRepository softwareModuleMetadataRepository;
private final DistributionSetManagement distributionSetManagement;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final ControllerPollProperties controllerPollProperties;
private final Duration minPollingTime, maxPollingTime;
private final PlatformTransactionManager txManager;
private final EntityFactory entityFactory;
private final EntityManager entityManager;
@@ -155,7 +162,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
final DeploymentManagement deploymentManagement, final ConfirmationManagement confirmationManagement,
final SoftwareModuleRepository softwareModuleRepository, final SoftwareModuleMetadataRepository softwareModuleMetadataRepository,
final DistributionSetManagement distributionSetManagement,
final TenantConfigurationManagement tenantConfigurationManagement,
final TenantConfigurationManagement tenantConfigurationManagement, final ControllerPollProperties controllerPollProperties,
final PlatformTransactionManager txManager, final EntityFactory entityFactory, final EntityManager entityManager,
final AfterTransactionCommitExecutor afterCommit,
final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware,
@@ -170,6 +177,13 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
this.softwareModuleMetadataRepository = softwareModuleMetadataRepository;
this.distributionSetManagement = distributionSetManagement;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.controllerPollProperties = controllerPollProperties;
minPollingTime = controllerPollProperties.getMinPollingTime() == null
? Duration.of(0, ChronoUnit.SECONDS)
: DurationHelper.fromString(controllerPollProperties.getMinPollingTime());
maxPollingTime = controllerPollProperties.getMaxPollingTime() == null
? Duration.of(100, ChronoUnit.YEARS)
: DurationHelper.fromString(controllerPollProperties.getMaxPollingTime());
this.txManager = txManager;
this.entityFactory = entityFactory;
this.entityManager = entityManager;
@@ -345,6 +359,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
public Target findOrRegisterTargetIfItDoesNotExist(final String controllerId, final URI address, final String name, final String type) {
return findOrRegisterTargetIfItDoesNotExist0(controllerId, address, name, type);
}
private Target findOrRegisterTargetIfItDoesNotExist0(final String controllerId, final URI address, final String name, final String type) {
final Specification<JpaTarget> spec = (targetRoot, query, cb) -> cb.equal(targetRoot.get(JpaTarget_.controllerId), controllerId);
return targetRepository.findOne(spec)
@@ -373,42 +388,39 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
}
@Override
public String getPollingTime() {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class).getValue());
}
/**
* Returns the configured minimum polling interval.
*
* @return current {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}.
*/
@Override
public String getMinPollingTime() {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.MIN_POLLING_TIME_INTERVAL, String.class).getValue());
}
/**
* Returns the count to be used for reducing polling interval while calling {@link ControllerManagement#getPollingTimeForAction(Action)}.
*
* @return configured value of
* {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}.
*/
@Override
public int getMaintenanceWindowPollCount() {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.MAINTENANCE_WINDOW_POLL_COUNT, Integer.class).getValue());
public String getPollingTime(final Target target) {
return systemSecurityContext.runAsSystem(() -> {
final PollingTime pollingTime = new PollingTime(
tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class).getValue());
if (!ObjectUtils.isEmpty(pollingTime.getOverrides()) && target instanceof JpaTarget jpaTarget) {
for (final PollingTime.Override override : pollingTime.getOverrides()) {
try {
if (EntityMatcher.forRsql(override.qlStr()).match(jpaTarget)) {
return override.pollingInterval().getFormattedIntervalWithDeviation(minPollingTime, maxPollingTime);
}
} catch (final Exception e) {
log.warn("Error while evaluating polling override for target {}: {}", jpaTarget.getId(), e.getMessage());
}
}
}
// returns default - no overrides or not applicable for the target
return pollingTime.getPollingInterval().getFormattedIntervalWithDeviation(minPollingTime, maxPollingTime);
});
}
@Override
public String getPollingTimeForAction(final Action action) {
public String getPollingTimeForAction(final Target target, final Action action) {
final String pollingTime = getPollingTime(target);
if (!action.hasMaintenanceSchedule() || action.isMaintenanceScheduleLapsed()) {
return getPollingTime();
return pollingTime;
} else {
// the count to be used for reducing polling interval -> the configured value of {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}
final int maintenanceWindowPollCount = systemSecurityContext.runAsSystem(
() -> tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.MAINTENANCE_WINDOW_POLL_COUNT, Integer.class).getValue());
return new EventTimer(pollingTime, controllerPollProperties.getMinPollingTime(), ChronoUnit.SECONDS)
.timeToNextEvent(maintenanceWindowPollCount, action.getMaintenanceWindowStartTime().orElse(null));
}
return new EventTimer(getPollingTime(), getMinPollingTime(), ChronoUnit.SECONDS)
.timeToNextEvent(getMaintenanceWindowPollCount(), action.getMaintenanceWindowStartTime().orElse(null));
}
@Override

View File

@@ -9,8 +9,10 @@
*/
package org.eclipse.hawkbit.repository.jpa.management;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.POLLING_TIME;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED;
import java.io.Serializable;
@@ -18,8 +20,8 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -33,7 +35,9 @@ import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorExce
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValueChangeNotAllowedException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTenantConfiguration;
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
import org.eclipse.hawkbit.repository.jpa.repository.TenantConfigurationRepository;
import org.eclipse.hawkbit.repository.model.PollStatus;
import org.eclipse.hawkbit.repository.model.Target;
@@ -42,6 +46,7 @@ import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.springframework.cache.Cache;
@@ -55,6 +60,7 @@ import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
/**
@@ -90,9 +96,8 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
@Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public <T extends Serializable> TenantConfigurationValue<T> addOrUpdateConfiguration(
final String configurationKeyName, final T value) {
return addOrUpdateConfiguration0(Collections.singletonMap(configurationKeyName, value)).values().iterator().next();
public <T extends Serializable> TenantConfigurationValue<T> addOrUpdateConfiguration(final String configurationKeyName, final T value) {
return addOrUpdateConfiguration0(Map.of(configurationKeyName, value)).values().iterator().next();
}
@Override
@@ -138,11 +143,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
checkAccess(configurationKeyName);
final TenantConfigurationKey configurationKey = tenantConfigurationProperties.fromKeyName(configurationKeyName);
validateTenantConfigurationDataType(configurationKey, propertyType);
final TenantConfiguration tenantConfiguration = tenantConfigurationRepository.findByKey(configurationKey.getKeyName());
return buildTenantConfigurationValueByKey(configurationKey, propertyType, tenantConfiguration);
}
@@ -151,7 +154,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
checkAccess(configurationKeyName);
final TenantConfigurationKey key = tenantConfigurationProperties.fromKeyName(configurationKeyName);
if (!key.getDataType().isAssignableFrom(propertyType)) {
throw new TenantConfigurationValidatorException(String.format(
"Cannot parse the database value of type %s into the type %s.", key.getDataType(), propertyType));
@@ -162,47 +164,62 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
@Override
public Function<Target, PollStatus> pollStatusResolver() {
final Duration pollTime = DurationHelper.formattedStringToDuration(
getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class).getValue());
final Duration overdueTime = DurationHelper.formattedStringToDuration(
getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class)
.getValue());
final PollingTime pollingTime = new PollingTime(
getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class).getValue());
final Duration pollingOverdueTime = DurationHelper.fromString(
getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME, String.class).getValue());
return target -> {
final Long lastTargetQuery = target.getLastTargetQuery();
if (lastTargetQuery == null) {
return null;
}
final LocalDateTime currentDate = LocalDateTime.now();
final LocalDateTime lastPollDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastTargetQuery),
ZoneId.systemDefault());
final LocalDateTime nextPollDate = lastPollDate.plus(pollTime);
final LocalDateTime overdueDate = nextPollDate.plus(overdueTime);
return new PollStatus(lastPollDate, nextPollDate, overdueDate, currentDate);
if (!ObjectUtils.isEmpty(pollingTime.getOverrides()) && target instanceof JpaTarget jpaTarget) {
for (final PollingTime.Override override : pollingTime.getOverrides()) {
try {
if (EntityMatcher.forRsql(override.qlStr()).match(jpaTarget)) {
return pollStatus(lastTargetQuery, override.pollingInterval(), pollingOverdueTime);
}
} catch (final Exception e) {
log.warn("Error while evaluating polling override for target {}: {}", jpaTarget.getId(), e.getMessage());
}
}
}
// returns default - no overrides or not applicable for the target
return pollStatus(lastTargetQuery, pollingTime.getPollingInterval(), pollingOverdueTime);
};
}
/**
* Validates the data type of the tenant configuration. If it is possible to
* cast to the given data type.
*
* @param configurationKey the key
* @param propertyType the class
*/
private static <T> void validateTenantConfigurationDataType(final TenantConfigurationKey configurationKey,
final Class<T> propertyType) {
private static PollStatus pollStatus(
final long lastTargetQuery,
final PollingTime.PollingInterval pollingInterval, final Duration pollingOverdueTime) {
final LocalDateTime currentDate = LocalDateTime.now();
final LocalDateTime lastPollDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastTargetQuery), ZoneId.systemDefault());
LocalDateTime nextPollDate = lastPollDate.plus(pollingInterval.getInterval());
if (pollingInterval.getDeviationPercent() > 0) {
nextPollDate = nextPollDate.plus(
pollingInterval.getInterval().toMillis() * pollingInterval.getDeviationPercent() / 100,
ChronoUnit.MILLIS);
}
final LocalDateTime overdueDate = nextPollDate.plus(pollingOverdueTime);
return new PollStatus(lastPollDate, nextPollDate, overdueDate, currentDate);
}
/**
* Validates the data type of the tenant configuration. If it is possible to cast to the given data type.
*/
private static <T> void validateTenantConfigurationDataType(final TenantConfigurationKey configurationKey, final Class<T> propertyType) {
if (!configurationKey.getDataType().isAssignableFrom(propertyType)) {
throw new TenantConfigurationValidatorException(
String.format("Cannot parse the database value of type %s into the type %s.",
String.format(
"Cannot parse the database value of type %s into the type %s.",
configurationKey.getDataType(), propertyType));
}
}
private void checkAccess(final String configurationKeyName) {
if (TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY
.equalsIgnoreCase(configurationKeyName)) {
final SystemSecurityContext systemSecurityContext =
SystemSecurityContextHolder.getInstance().getSystemSecurityContext();
if (AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY.equalsIgnoreCase(configurationKeyName)) {
final SystemSecurityContext systemSecurityContext = SystemSecurityContextHolder.getInstance().getSystemSecurityContext();
if (!systemSecurityContext.isCurrentThreadSystemCode() &&
!systemSecurityContext.hasPermission(SpPermission.READ_GATEWAY_SEC_TOKEN)) {
throw new InsufficientPermissionException(
@@ -211,22 +228,28 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
}
}
private <T extends Serializable> Map<String, TenantConfigurationValue<T>> addOrUpdateConfiguration0(Map<String, T> configurations) {
List<JpaTenantConfiguration> configurationList = new ArrayList<>();
private <T extends Serializable> Map<String, TenantConfigurationValue<T>> addOrUpdateConfiguration0(final Map<String, T> configurations) {
final List<JpaTenantConfiguration> configurationList = new ArrayList<>();
configurations.forEach((configurationKeyName, value) -> {
final TenantConfigurationKey configurationKey = tenantConfigurationProperties.fromKeyName(configurationKeyName);
if (!configurationKey.getDataType().isAssignableFrom(value.getClass())) {
throw new TenantConfigurationValidatorException(String.format(
"Cannot parse the value %s of type %s into the type %s defined by the configuration key.", value,
value.getClass(), configurationKey.getDataType()));
}
configurationKey.validate(value, applicationContext);
// additional validation for specific configuration keys
if (POLLING_TIME.equals(configurationKey.getKeyName())) {
final PollingTime pollingTime = new PollingTime(value.toString());
if (!ObjectUtils.isEmpty(pollingTime.getOverrides())) {
// validate that the QL strings are valid RSQL queries,
// nevertheless always when parse them we shall be prepared to catch exceptions if the parsers
// has been changed in non backward compatible way
pollingTime.getOverrides().forEach(override -> EntityMatcher.forRsql(override.qlStr()));
}
}
configurationKey.validate(applicationContext, value);
JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository
.findByKey(configurationKey.getKeyName());
JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository.findByKey(configurationKey.getKeyName());
if (tenantConfiguration == null) {
tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString());
} else {
@@ -237,15 +260,12 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
configurationList.add(tenantConfiguration);
});
List<JpaTenantConfiguration> jpaTenantConfigurations = tenantConfigurationRepository
.saveAll(configurationList);
final List<JpaTenantConfiguration> jpaTenantConfigurations = tenantConfigurationRepository.saveAll(configurationList);
return jpaTenantConfigurations.stream().collect(Collectors.toMap(
JpaTenantConfiguration::getKey,
updatedTenantConfiguration -> {
@SuppressWarnings("unchecked") final Class<T> clazzT = (Class<T>) configurations.get(updatedTenantConfiguration.getKey())
.getClass();
@SuppressWarnings("unchecked")
final Class<T> clazzT = (Class<T>) configurations.get(updatedTenantConfiguration.getKey()).getClass();
return TenantConfigurationValue.<T> builder().global(false)
.createdBy(updatedTenantConfiguration.getCreatedBy())
.createdAt(updatedTenantConfiguration.getCreatedAt())
@@ -257,27 +277,25 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
}
private <T extends Serializable> TenantConfigurationValue<T> buildTenantConfigurationValueByKey(
final TenantConfigurationKey configurationKey, final Class<T> propertyType,
final TenantConfiguration tenantConfiguration) {
final TenantConfigurationKey configurationKey, final Class<T> propertyType, final TenantConfiguration tenantConfiguration) {
if (tenantConfiguration != null) {
return TenantConfigurationValue.<T> builder().global(false).createdBy(tenantConfiguration.getCreatedBy())
.createdAt(tenantConfiguration.getCreatedAt())
.lastModifiedAt(tenantConfiguration.getLastModifiedAt())
.lastModifiedBy(tenantConfiguration.getLastModifiedBy())
.value(CONVERSION_SERVICE.convert(tenantConfiguration.getValue(), propertyType)).build();
} else if (configurationKey.getDefaultValue() != null) {
return TenantConfigurationValue.<T> builder().global(true).createdBy(null).createdAt(null)
.lastModifiedAt(null).lastModifiedBy(null)
.value(getGlobalConfigurationValue(configurationKey.getKeyName(), propertyType)).build();
} else {
return null;
}
return null;
}
/**
* Asserts that the requested configuration value change is allowed. Throws
* a {@link TenantConfigurationValueChangeNotAllowedException} otherwise.
* Asserts that the requested configuration value change is allowed. Throws a {@link TenantConfigurationValueChangeNotAllowedException}
* otherwise.
*
* @param key The configuration key.
* @param valueChange The configuration to be validated.
@@ -293,9 +311,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
private void assertAutoCloseValueChange(final String key, final JpaTenantConfiguration valueChange) {
if (REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED.equals(key)
&& Boolean.TRUE.equals(getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue())) {
log.debug(
"The property '{}' must not be changed because the Multi-Assignments feature is currently enabled.",
key);
log.debug("The property '{}' must not be changed because the Multi-Assignments feature is currently enabled.", key);
throw new TenantConfigurationValueChangeNotAllowedException();
}
}
@@ -308,8 +324,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
if (MULTI_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) {
JpaTenantConfiguration batchConfig = tenantConfigurationRepository.findByKey(BATCH_ASSIGNMENTS_ENABLED);
if (batchConfig != null && Boolean.parseBoolean(batchConfig.getValue())) {
log.debug("The Multi-Assignments '{}' feature cannot be enabled as it contradicts with " +
"The Batch-Assignments feature, which is already enabled .", key);
log.debug("The Multi-Assignments '{}' feature cannot be enabled as it contradicts with the Batch-Assignments feature, which is already enabled .", key);
throw new TenantConfigurationValueChangeNotAllowedException();
}
}
@@ -319,10 +334,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
if (BATCH_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) {
JpaTenantConfiguration multiConfig = tenantConfigurationRepository.findByKey(MULTI_ASSIGNMENTS_ENABLED);
if (multiConfig != null && Boolean.parseBoolean(multiConfig.getValue())) {
log.debug("The Batch-Assignments '{}' feature cannot be enabled as it contradicts with " +
"The Multi-Assignments feature, which is already enabled .", key);
log.debug("The Batch-Assignments '{}' feature cannot be enabled as it contradicts with the Multi-Assignments feature, which is already enabled .", key);
throw new TenantConfigurationValueChangeNotAllowedException();
}
}
}
}
}

View File

@@ -139,24 +139,7 @@ class ControllerManagementSecurityTest extends AbstractJpaIntegrationTest {
*/
@Test
void getPollingTimePermissionsCheck() {
assertPermissions(() -> controllerManagement.getPollingTime(), List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE));
}
/**
* Tests ControllerManagement#getMinPollingTime() method
*/
@Test
void getMinPollingTimePermissionsCheck() {
assertPermissions(() -> controllerManagement.getMinPollingTime(), List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE));
}
/**
* Tests ControllerManagement#getMaxPollingTime() method
*/
@Test
void getMaintenanceWindowPollCountPermissionsCheck() {
assertPermissions(() -> controllerManagement.getMaintenanceWindowPollCount(),
List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE));
assertPermissions(() -> controllerManagement.getPollingTime(null), List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE));
}
/**
@@ -168,7 +151,7 @@ class ControllerManagementSecurityTest extends AbstractJpaIntegrationTest {
action.setId(1L);
assertPermissions(() -> {
try {
controllerManagement.getPollingTimeForAction(action);
controllerManagement.getPollingTimeForAction(action.getTarget(), action);
} catch (final CancelActionNotAllowedException e) {
// expected since action is not found
}

View File

@@ -51,7 +51,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
// get the configuration from the system management
final TenantConfigurationValue<String> defaultConfigValue = tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class);
TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class);
assertThat(defaultConfigValue.isGlobal()).isTrue();
assertThat(defaultConfigValue.getValue()).isEqualTo(envPropertyDefault);
@@ -60,11 +60,11 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
final String newConfigurationValue = "thisIsAnotherTokenName";
assertThat(newConfigurationValue).isNotEqualTo(defaultConfigValue.getValue());
tenantConfigurationManagement.addOrUpdateConfiguration(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, newConfigurationValue);
TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, newConfigurationValue);
// verify that new configuration value is used
final TenantConfigurationValue<String> updatedConfigurationValue = tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class);
.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class);
assertThat(updatedConfigurationValue.isGlobal()).isFalse();
assertThat(updatedConfigurationValue.getValue()).isEqualTo(newConfigurationValue);
@@ -76,7 +76,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void updateTenantSpecificConfiguration() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY;
final String value1 = "firstValue";
final String value2 = "secondValue";
@@ -95,14 +95,14 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
@Test
void batchUpdateTenantSpecificConfiguration() {
Map<String, Serializable> configuration = new HashMap<>() {{
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
}};
// add value first
tenantConfigurationManagement.addOrUpdateConfiguration(configuration);
assertThat(tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue())
TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue())
.isEqualTo("token_123");
assertThat(
tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue())
@@ -114,7 +114,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storeAndUpdateTenantSpecificConfigurationAsBoolean() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
final String configKey = TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED;
final Boolean value1 = true;
tenantConfigurationManagement.addOrUpdateConfiguration(configKey, value1);
assertThat(tenantConfigurationManagement.getConfigurationValue(configKey, Boolean.class).getValue()).isEqualTo(value1);
@@ -128,7 +128,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void wrongTenantConfigurationValueTypeThrowsException() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
final String configKey = TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED;
final String value1 = "thisIsNotABoolean";
// add value as String
@@ -143,9 +143,9 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
@Test
void batchWrongTenantConfigurationValueTypeThrowsException() {
final Map<String, Serializable> configuration = new HashMap<>() {{
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, "wrong");
put(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED, "wrong");
}};
try {
@@ -154,7 +154,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
} catch (final TenantConfigurationValidatorException e) {
assertThat(
tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue())
TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue())
.isNotEqualTo("token_123");
assertThat(
tenantConfigurationManagement.getConfigurationValue(
@@ -168,7 +168,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void deleteConfigurationReturnNullConfiguration() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY;
// gateway token does not have default value so no configuration value should be available
final String defaultConfigValue = tenantConfigurationManagement.getConfigurationValue(configKey, String.class).getValue();
@@ -195,7 +195,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storesIntegerWhenStringIsExpected() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY;
final Integer wrongDatType = 123;
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDatType))
.as("Should not have worked as integer is not a string")
@@ -207,7 +207,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storesIntegerWhenBooleanIsExpected() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED;
final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED;
final Integer wrongDataType = 123;
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType))
.as("Should not have worked as integer is not a boolean")
@@ -219,7 +219,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storesIntegerWhenPollingIntervalIsExpected() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String configKey = TenantConfigurationKey.POLLING_TIME;
final Integer wrongDataType = 123;
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType))
.as("Should not have worked as integer is not a time field")
@@ -231,7 +231,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storesWrongFormattedStringAsPollingInterval() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String configKey = TenantConfigurationKey.POLLING_TIME;
final String wrongFormatted = "wrongFormatted";
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongFormatted))
.as("should not have worked as string is not a time field")
@@ -243,10 +243,9 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storesTooSmallDurationAsPollingInterval() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String configKey = TenantConfigurationKey.POLLING_TIME;
final String tooSmallDuration = DurationHelper
.durationToFormattedString(DurationHelper.getDurationByTimeValues(0, 0, 1));
final String tooSmallDuration = DurationHelper.toString(getDurationByTimeValues(0, 0, 1));
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, tooSmallDuration))
.as("Should not have worked as string has an invalid format")
.isInstanceOf(TenantConfigurationValidatorException.class);
@@ -257,15 +256,15 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void storesCorrectDurationAsPollingInterval() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String configKey = TenantConfigurationKey.POLLING_TIME;
final Duration duration = DurationHelper.getDurationByTimeValues(1, 2, 0);
final Duration duration = getDurationByTimeValues(1, 2, 0);
assertThat(duration).isEqualTo(Duration.ofHours(1).plusMinutes(2));
tenantConfigurationManagement.addOrUpdateConfiguration(configKey, DurationHelper.durationToFormattedString(duration));
tenantConfigurationManagement.addOrUpdateConfiguration(configKey, DurationHelper.toString(duration));
final String storedDurationString = tenantConfigurationManagement.getConfigurationValue(configKey, String.class).getValue();
assertThat(duration).isEqualTo(DurationHelper.formattedStringToDuration(storedDurationString));
assertThat(duration).isEqualTo(DurationHelper.fromString(storedDurationString));
}
/**
@@ -274,7 +273,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
@Test
void requestConfigValueWithWrongType() {
assertThatThrownBy(() -> tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.POLLING_TIME_INTERVAL, Serializable.class))
TenantConfigurationKey.POLLING_TIME, Serializable.class))
.isInstanceOf(TenantConfigurationValidatorException.class);
}
@@ -297,7 +296,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
*/
@Test
void getTenantConfigurationKeyByName() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String configKey = TenantConfigurationKey.POLLING_TIME;
assertThat(tenantConfigurationProperties.fromKeyName(configKey).getKeyName()).isEqualTo(configKey);
}
@@ -311,4 +310,8 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
.as("Expected InvalidTenantConfigurationKeyException for tenant configuration key which is not declared")
.isInstanceOf(InvalidTenantConfigurationKeyException.class);
}
private static Duration getDurationByTimeValues(final long hours, final long minutes, final long seconds) {
return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds);
}
}

View File

@@ -411,9 +411,9 @@ class RsqlUtilityTest {
VirtualPropertyReplacer setupMacroLookup() {
when(securityContext.runAsSystem(Mockito.any())).thenAnswer(a -> ((Callable<?>) a.getArgument(0)).call());
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class))
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class))
.thenReturn(TEST_POLLING_TIME_INTERVAL);
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class))
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME, String.class))
.thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL);
return macroResolver;

View File

@@ -54,9 +54,9 @@ class VirtualPropertyResolverTest {
@BeforeEach
void before() {
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class))
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class))
.thenReturn(TEST_POLLING_TIME_INTERVAL);
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class))
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME, String.class))
.thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL);
}