Fix optimize ui maintenance window (#668)

* Optimize maintenance window UI

Signed-off-by: Melanie Retter <melanie.retter@bosch-si.com>

* Refactor

Signed-off-by: Melanie Retter <melanie.retter@bosch-si.com>

* Add new downloaded status to UI.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Changed Accordion to Tabsheet for better visualization of action types
and maintanance window.

Signed-off-by: Markus Block <markus.block@bosch-si.com>

* Refined UI for maintenance window, refactoring

Added ENTER shortcut for save button in dialog windows

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Extended validation for maintenance window, refactored the maintenance window helper class

Added text change listeners for the schedule and duration text fields in order to activate "save all" button
Added client Locale identification for cron expression translation
Moved maintenance window validation from TargetWithActionType constructor to saveAll method of assignment tab

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added SupressWarnings annotation for exception handling cases

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Fixed Sonar issue: added private constructor to Maintenance schedule helper class

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Sonar Issue: make utility class Maintenance Schedule final

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added Maintenance Window validation to Distribution Set and Target Management API

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added unit tests for MaintenanceScheduleHelper class

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added the license header to MaintenanceScheduleHelperTest class

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Small changes after PR review

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added Id for Maintenance Window layout for UI Tests

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>
This commit is contained in:
Bondar Bogdan
2018-04-04 10:01:55 +02:00
committed by Dominic Schabel
parent 8fd601f8b9
commit 4c28c4d905
21 changed files with 632 additions and 293 deletions

View File

@@ -8,8 +8,6 @@
*/
package org.eclipse.hawkbit.repository;
import static com.cronutils.model.CronType.QUARTZ;
import java.time.Duration;
import java.time.LocalTime;
import java.time.ZoneOffset;
@@ -21,7 +19,7 @@ import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleExcept
import org.springframework.util.StringUtils;
import com.cronutils.model.Cron;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
@@ -32,62 +30,67 @@ import com.cronutils.parser.CronParser;
* also provides a helper method for conversion of duration specified in
* HH:mm:ss format to ISO format.
*/
public class MaintenanceScheduleHelper {
public final class MaintenanceScheduleHelper {
private final ExecutionTime scheduleExecutor;
private static final CronParser cronParser = new CronParser(
CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
/**
* Constructor that accepts a cron expression, duration and time zone and
* instantiates the cron parser and scheduler executor.
*
* @param cronSchedule
* is the cron expression to be used for scheduling the
* maintenance window. Expression has 6 mandatory fields and 1
* last optional field: "second minute hour dayofmonth month
* weekday year"
*/
public MaintenanceScheduleHelper(final String cronSchedule) {
final CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(QUARTZ);
final CronParser parser = new CronParser(cronDefinition);
final Cron quartzCron = parser.parse(cronSchedule);
this.scheduleExecutor = ExecutionTime.forCron(quartzCron);
private MaintenanceScheduleHelper() {
throw new IllegalStateException("Utility class");
}
/**
* Method calculates the next available maintenance window within the
* schedule but after a given time.
* Calculate the next available maintenance window.
*
* @param after
* is the {@link ZonedDateTime} after which the window is
* required
* @param cronSchedule
* is a cron expression with 6 mandatory fields and 1 last
* optional field: "second minute hour dayofmonth month weekday
* year".
* @param duration
* in HH:mm:ss format specifying the duration of a maintenance
* window, for example 00:30:00 for 30 minutes.
* @param timezone
* is the time zone specified as +/-hh:mm offset from UTC. For
* example +02:00 for CET summer time and +00:00 for UTC. The
* start time of a maintenance window calculated based on the
* cron expression is relative to this time zone.
*
* @return {@link Optional<ZonedDateTime>} of the next available window. In
* case there is none, returns empty value.
* case there is none, or there are maintenance window validation
* errors, returns empty value.
*
*/
// Exception squid:S1166 - lib throws exception as well if no value found
// Exception squid:S1166 - if there are validation error(format of cron
// expression or duration is wrong), we simply return empty value
@SuppressWarnings("squid:S1166")
public Optional<ZonedDateTime> nextExecution(final ZonedDateTime after) {
public static Optional<ZonedDateTime> getNextMaintenanceWindow(final String cronSchedule, final String duration,
final String timezone) {
try {
final ZonedDateTime next = this.scheduleExecutor.nextExecution(after);
return Optional.ofNullable(next);
} catch (final IllegalArgumentException ignored) {
final ExecutionTime scheduleExecutor = ExecutionTime.forCron(getCronFromExpression(cronSchedule));
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.of(timezone));
final ZonedDateTime after = now.minus(convertToISODuration(duration));
final ZonedDateTime next = scheduleExecutor.nextExecution(after);
return Optional.of(next);
} catch (final RuntimeException ignored) {
return Optional.empty();
}
}
/**
* Method checks if there are any more valid maintenance windows after a
* given time.
* Parse the given cron expression with quartz parser.
*
* @param after
* is the {@link ZonedDateTime} after which the windows are
* checked
* @param cronSchedule
* is a cron expression with 6 mandatory fields and 1 last
* optional field: "second minute hour dayofmonth month weekday
* year".
*
* @return true if there is at least one valid schedule remaining, else
* false.
* @return {@link Cron} object, that corresponds to the expression.
*
* @throws IllegalArgumentException
* if the cron expression doesn't have a valid format.
*/
private boolean hasValidScheduleAfter(final ZonedDateTime after) {
return nextExecution(after).isPresent();
public static Cron getCronFromExpression(final String cronSchedule) {
return cronParser.parse(cronSchedule);
}
/**
@@ -100,39 +103,30 @@ public class MaintenanceScheduleHelper {
* @param cronSchedule
* is a cron expression with 6 mandatory fields and 1 last
* optional field: "second minute hour dayofmonth month weekday
* year"
* year".
* @param duration
* in HH:mm:ss format specifying the duration of a maintenance
* window, for example 00:30:00 for 30 minutes
* window, for example 00:30:00 for 30 minutes.
* @param timezone
* is the time zone specified as +/-hh:mm offset from UTC. For
* example +02:00 for CET summer time and +00:00 for UTC. The
* start time of a maintenance window calculated based on the
* cron expression is relative to this time zone
* cron expression is relative to this time zone.
*
* @throws InvalidMaintenanceScheduleException
* if the defined schedule fails the validity criteria.
*/
public static void validateMaintenanceSchedule(final String cronSchedule, final String duration,
final String timezone) {
// check if schedule, duration and timezone are all not empty.
if (allNotEmpty(cronSchedule, duration, timezone)) {
final ZonedDateTime now;
try {
now = ZonedDateTime.now(ZoneOffset.of(timezone));
Duration.parse(convertToISODuration(duration));
} catch (final RuntimeException validationFailed) {
throw new InvalidMaintenanceScheduleException("No valid maintenance window provided", validationFailed);
}
final MaintenanceScheduleHelper scheduleHelper = new MaintenanceScheduleHelper(cronSchedule);
validateCronSchedule(cronSchedule);
validateDuration(duration);
// check if there is a window currently active or available in
// future.
if (!scheduleHelper.hasValidScheduleAfter(now.minus(Duration.parse(convertToISODuration(duration))))) {
if (!getNextMaintenanceWindow(cronSchedule, duration, timezone).isPresent()) {
throw new InvalidMaintenanceScheduleException(
"No valid maintenance window available after current time");
}
} else if (atLeastOneNotEmpty(cronSchedule, duration, timezone)) {
throw new InvalidMaintenanceScheduleException(
"All of schedule, duration and timezone should either be null or non empty.");
@@ -154,14 +148,61 @@ public class MaintenanceScheduleHelper {
* @param timeInterval
* in "HH:mm:ss" string format. This format is popularly used but
* can be confused with time of the day, hence conversion to ISO
* specified format for time duration is required
* specified format for time duration is required.
*
* @return the time interval or duration in ISO format
* @return {@link Duration} in ISO format.
*
* @throws DateTimeParseException
* if the text cannot be converted to ISO format.
*/
public static String convertToISODuration(final String timeInterval) {
return Duration.between(LocalTime.MIN, LocalTime.parse(timeInterval)).toString();
public static Duration convertToISODuration(final String timeInterval) {
return Duration.between(LocalTime.MIN, convertDurationToLocalTime(timeInterval));
}
private static LocalTime convertDurationToLocalTime(final String timeInterval) {
return LocalTime.parse(StringUtils.trimWhitespace(timeInterval));
}
/**
* Validates the format of the maintenance window duration
*
* @param duration
* in "HH:mm:ss" string format. This format is popularly used but
* can be confused with time of the day, hence conversion to ISO
* specified format for time duration is required.
*
* @throws InvalidMaintenanceScheduleException
* if the duration doesn't have a valid format to be converted
* to ISO.
*/
public static void validateDuration(final String duration) {
try {
if (StringUtils.hasText(duration)) {
convertDurationToLocalTime(duration);
}
} catch (final DateTimeParseException e) {
throw new InvalidMaintenanceScheduleException("Provided duration is not valid", e, e.getErrorIndex());
}
}
/**
* Validates the format of the maintenance window cron expression
*
* @param cronSchedule
* is a cron expression with 6 mandatory fields and 1 last
* optional field: "second minute hour dayofmonth month weekday
* year".
*
* @throws InvalidMaintenanceScheduleException
* if the cron expression doesn't have a valid quartz format.
*/
public static void validateCronSchedule(final String cronSchedule) {
try {
if (StringUtils.hasText(cronSchedule)) {
getCronFromExpression(cronSchedule);
}
} catch (final IllegalArgumentException e) {
throw new InvalidMaintenanceScheduleException(e.getMessage(), e);
}
}
}

View File

@@ -20,6 +20,8 @@ import org.eclipse.hawkbit.exception.SpServerError;
public class InvalidMaintenanceScheduleException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
private final int durationErrorIndex;
/**
* Constructor for {@link InvalidMaintenanceScheduleException}.
*
@@ -27,7 +29,20 @@ public class InvalidMaintenanceScheduleException extends AbstractServerRtExcepti
* the message for this exception.
*/
public InvalidMaintenanceScheduleException(final String message) {
this(message, -1);
}
/**
* Constructor for {@link InvalidMaintenanceScheduleException}.
*
* @param message
* the message for this exception.
* @param errorIndex
* the error index of maintenance duration.
*/
public InvalidMaintenanceScheduleException(final String message, final int errorIndex) {
super(message, SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID);
this.durationErrorIndex = errorIndex;
}
/**
@@ -39,6 +54,30 @@ public class InvalidMaintenanceScheduleException extends AbstractServerRtExcepti
* the cause for this exception.
*/
public InvalidMaintenanceScheduleException(final String message, final Throwable cause) {
this(message, cause, -1);
}
/**
* Constructor for {@link InvalidMaintenanceScheduleException}.
*
* @param message
* the message for this exception
* @param cause
* the cause for this exception.
* @param errorIndex
* the error index of maintenance duration.
*/
public InvalidMaintenanceScheduleException(final String message, final Throwable cause, final int errorIndex) {
super(message, SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID, cause);
this.durationErrorIndex = errorIndex;
}
/**
* Get the error index position for maintenance window duration.
*
* @return error index.
*/
public int getDurationErrorIndex() {
return durationErrorIndex;
}
}

View File

@@ -8,7 +8,6 @@
*/
package org.eclipse.hawkbit.repository.model;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
@@ -69,9 +68,6 @@ public class TargetWithActionType {
this.maintenanceSchedule = maintenanceSchedule;
this.maintenanceWindowDuration = maintenanceWindowDuration;
this.maintenanceWindowTimeZone = maintenanceWindowTimeZone;
MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone);
}
public ActionType getActionType() {

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.time.Duration;
import java.time.ZonedDateTime;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.junit.Test;
import com.cronutils.model.Cron;
import ru.yandex.qatools.allure.annotations.Description;
import ru.yandex.qatools.allure.annotations.Features;
import ru.yandex.qatools.allure.annotations.Stories;
@Features("Unit Tests - Repository")
@Stories("Maintenance Schedule Utility")
public class MaintenanceScheduleHelperTest {
@Test
@Description("Verifies that the Cron object is returned for valid cron expression")
public void getCronFromExpressionValid() {
final String validCron = "0 0 0 ? * 6"; // at 00:00 every Saturday
assertThat(MaintenanceScheduleHelper.getCronFromExpression(validCron)).isNotNull().isInstanceOf(Cron.class);
}
@Test
@Description("Verifies that the Duration object is returned for valid duration format (hh:mm or hh:mm:ss)")
public void convertToISODurationValid() {
final String duration = "00:10";
assertThat(MaintenanceScheduleHelper.convertToISODuration(duration)).isNotNull().isInstanceOf(Duration.class);
}
@Test
@Description("Verifies that the InvalidMaintenanceScheduleException is thrown for invalid duration format")
public void validateDurationInvalid() {
final String duration = "10";
assertThatThrownBy(() -> MaintenanceScheduleHelper.validateDuration(duration))
.isInstanceOf(InvalidMaintenanceScheduleException.class).hasMessage("Provided duration is not valid")
.extracting("durationErrorIndex").containsExactly(2);
}
@Test
@Description("Verifies that the InvalidMaintenanceScheduleException is thrown for invalid cron expression")
public void validateCronScheduleInvalid() {
final String invalidCron = "0 0 0 * * 6";
assertThatThrownBy(() -> MaintenanceScheduleHelper.validateCronSchedule(invalidCron))
.isInstanceOf(InvalidMaintenanceScheduleException.class)
.hasMessageContaining("Both, a day-of-week AND a day-of-month parameter, are not supported");
}
@Test
@Description("Verifies that there is a maintenance window available for correct schedule, duration and timezone")
public void getNextMaintenanceWindowValid() {
final ZonedDateTime currentTime = ZonedDateTime.now();
final String cronSchedule = String.format("0 %d %d %d %d ? %d", currentTime.getMinute(), currentTime.getHour(),
currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear());
final String duration = "00:10";
final String timezone = ZonedDateTime.now().getOffset().getId().replace("Z", "+00:00");
assertThat(MaintenanceScheduleHelper.getNextMaintenanceWindow(cronSchedule, duration, timezone)).isPresent();
}
@Test
@Description("Verifies the maintenance schedule when only one required field is present")
public void validateMaintenanceScheduleAtLeastOneNotEmpty() {
final String duration = "00:10";
assertThatThrownBy(() -> MaintenanceScheduleHelper.validateMaintenanceSchedule(null, duration, null))
.isInstanceOf(InvalidMaintenanceScheduleException.class)
.hasMessage("All of schedule, duration and timezone should either be null or non empty.");
}
@Test
@Description("Verifies that there is no valid maintenance window available, scheduled before current time")
public void validateMaintenanceScheduleBeforeCurrentTime() {
ZonedDateTime currentTime = ZonedDateTime.now();
currentTime = currentTime.plusMinutes(-30);
final String cronSchedule = String.format("0 %d %d %d %d ? %d", currentTime.getMinute(), currentTime.getHour(),
currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear());
final String duration = "00:10";
final String timezone = ZonedDateTime.now().getOffset().getId().replace("Z", "+00:00");
assertThatThrownBy(
() -> MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone))
.isInstanceOf(InvalidMaintenanceScheduleException.class)
.hasMessage("No valid maintenance window available after current time");
}
}