Commit 492b6821 authored by Bergamote Orange's avatar Bergamote Orange
Browse files

Merge branch 'develop'

parents c123eb37 91e96543
Pipeline #576003 passed with stages
in 20 minutes and 34 seconds
......@@ -23,6 +23,7 @@ Jujube Orange <13631-x-JOrang@users.noreply.gitlab.inria.fr>
Kiwi Orange <13476-x-KOrang@users.noreply.gitlab.inria.fr>
Figue Orange <12540-x-FOrang@users.noreply.gitlab.inria.fr>
Sapotille Orange <15519-x-SaOrang@users.noreply.gitlab.inria.fr>
Jamalac Orange <14492-x-JaOrang@users.noreply.gitlab.inria.fr>
"
remote="$1"
......
......@@ -22,8 +22,6 @@
<modules>
<module>robert-push-notif-server-ws-rest</module>
<module>robert-push-notif-server-database</module>
<module>robert-push-notif-server-common</module>
<module>robert-push-notif-server-scheduler</module>
</modules>
......@@ -42,27 +40,48 @@
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
......@@ -105,19 +124,6 @@
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
<version>0.14.1</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
......
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-push-notif-server</artifactId>
<version>0-SNAPSHOT</version>
</parent>
<artifactId>robert-push-notif-server-common</artifactId>
<name>robert-push-notif-server-common</name>
<description>Common Module</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
package fr.gouv.stopc.robert.pushnotif.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Builder
@Data
@AllArgsConstructor
public class PushDate {
private int minPushHour;
private int maxPushHour;
private String timezone;
private Date lastPushDate;
}
package fr.gouv.stopc.robert.pushnotif.common.utils;
import fr.gouv.stopc.robert.pushnotif.common.PushDate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import static java.time.temporal.ChronoUnit.HOURS;
@Slf4j
public final class TimeUtils {
public final static String UTC = "UTC";
private static Random random = new Random();
private TimeUtils() {
throw new AssertionError();
}
private static Optional<LocalDateTime> toDateAtTimezone(PushDate pushdate) {
return Optional.ofNullable(
Instant.ofEpochMilli(pushdate.getLastPushDate().getTime())
.atZone(ZoneId.of(UTC))
.withZoneSameInstant(ZoneId.of(pushdate.getTimezone()))
.toLocalDateTime()
);
}
private static Optional<Date> toDateTimezoneUTC(LocalDateTime dateTime, String currentTimezone) {
return Optional.ofNullable(
Date.from(
dateTime.atZone(ZoneId.of(currentTimezone))
.withZoneSameInstant(ZoneId.of(UTC)).toInstant()
)
);
}
private static boolean isValidHour(PushDate pushDate) {
return pushDate.getMinPushHour() < pushDate.getMaxPushHour();
}
private static int getRandomNumberInRange(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("max must be greater or equals to min");
}
return random.nextInt((max - min) + 1) + min;
}
public static Optional<LocalDateTime> toLocalDateTime(Date date) {
return Optional.ofNullable(
Instant.ofEpochMilli(date.getTime())
.atZone(ZoneId.of(UTC))
.toLocalDateTime()
);
}
public static Date getNowAtTimeZoneUTC() {
return Date.from(
LocalDateTime.now().atZone(ZoneId.systemDefault())
.withZoneSameInstant(ZoneId.of(UTC))
.toInstant()
);
}
public static Date getNowZoneUTC() {
LocalDateTime datetime = LocalDateTime.now().atZone(ZoneId.systemDefault())
.withZoneSameInstant(ZoneId.of(UTC)).toLocalDateTime();
return toSqlDate(datetime);
}
public static Date toSqlDate(LocalDateTime date) {
return java.sql.Timestamp.valueOf(date);
}
public static Optional<Date> getNextPushDate(PushDate pushDate) {
if (Objects.nonNull(pushDate)) {
if (!isValidHour(pushDate)) {
log.warn(
"Invalid hours (minHour >= maxHour) {} > {}", pushDate.getMinPushHour(),
pushDate.getMaxPushHour()
);
return Optional.empty();
}
if (StringUtils.isBlank(pushDate.getTimezone())) {
log.warn("The timezone should not be null or blank");
return Optional.empty();
}
if (Objects.isNull(pushDate.getLastPushDate())) {
log.warn("The last push date is null. Using now");
pushDate.setLastPushDate(getNowAtTimeZoneUTC());
}
try {
// Convert to timezone
Optional<LocalDateTime> dateTime = toDateAtTimezone(pushDate);
if (!dateTime.isPresent()) {
log.warn("Failed to convert the push date at timezone");
return Optional.empty();
}
LocalDateTime dateAtTimezone = null;
do {
int pushHour = getRandomNumberInRange(pushDate.getMinPushHour(), pushDate.getMaxPushHour() - 1);
// add distribution on minutes ==> allowing to execute the batch every x minutes
// instead of every hour
int pushMinute = getRandomNumberInRange(0, 59);
dateAtTimezone = dateTime.get().toLocalDate().plusDays(1).atStartOfDay().withHour(pushHour)
.withMinute(pushMinute);
} while (!isBetween(
dateAtTimezone, dateAtTimezone.withHour(pushDate.getMinPushHour()),
dateAtTimezone.withHour(pushDate.getMaxPushHour())
));
return toDateTimezoneUTC(dateAtTimezone, pushDate.getTimezone());
} catch (DateTimeException e) {
log.error("Failed to calculate the next push date due to {}", e.getMessage());
}
}
return Optional.empty();
}
public static boolean isDateBetween(Date dateToCompare, Date dateDebut, Date dateFin) {
return Optional.ofNullable(dateToCompare)
.filter(date -> Objects.nonNull(dateDebut))
.filter(date -> Objects.nonNull(dateFin))
.map(date -> {
LocalDateTime dateInitiale = toLocalDateTime(dateToCompare).get();
LocalDateTime dateDeDebut = toLocalDateTime(dateDebut).get();
LocalDateTime dateDeFin = toLocalDateTime(dateFin).get();
return isBetween(dateInitiale, dateDeDebut, dateDeFin);
}).orElse(false);
}
public static boolean isBetween(LocalDateTime dateToCompare, LocalDateTime dateDebut, LocalDateTime dateFin) {
return Optional.ofNullable(dateToCompare)
.filter(date -> Objects.nonNull(dateDebut))
.filter(date -> Objects.nonNull(dateFin))
.filter(date -> HOURS.between(dateDebut, dateFin) > 0)
.map(date -> {
long secondssAfterDebut = HOURS.between(dateDebut, date);
long secondsBeforeFin = HOURS.between(date, dateFin);
return secondssAfterDebut >= 0 && secondsBeforeFin >= 0;
}).orElse(false);
}
}
package test.fr.gouv.stopc.robert.pushnotif.common.utils;
import fr.gouv.stopc.robert.pushnotif.common.PushDate;
import fr.gouv.stopc.robert.pushnotif.common.utils.TimeUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
public class TimeUtilsTest {
@Test
public void testConstructorShouldThrowAssertionError() {
// Then
Assertions.assertThrows(AssertionError.class, () -> {
Constructor<TimeUtils> constructor;
try {
// Given
constructor = TimeUtils.class.getDeclaredConstructor(null);
assertNotNull(constructor);
constructor.setAccessible(true);
// When
constructor.newInstance(null);
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException e) {
Assertions.fail("Should not throw these exceptions");
}
});
}
private LocalDateTime getMinNextPushDate(PushDate pushDate) {
ZonedDateTime now = toDateAtTimezone(pushDate.getLastPushDate(), pushDate)
.toLocalDate().atStartOfDay(ZoneId.of(pushDate.getTimezone()))
.plusDays(1);
return now.withHour(pushDate.getMinPushHour()).toLocalDateTime();
}
private LocalDateTime getMaxNextPushDate(PushDate pushDate) {
ZonedDateTime now = toDateAtTimezone(pushDate.getLastPushDate(), pushDate)
.toLocalDate().atStartOfDay(ZoneId.of(pushDate.getTimezone())).plusDays(1);
return now.withHour(pushDate.getMaxPushHour()).toLocalDateTime();
}
private LocalDateTime toDateAtTimezone(Date nextPushDate, PushDate pushdate) {
return Instant.ofEpochMilli(nextPushDate.getTime())
.atZone(ZoneId.of(TimeUtils.UTC))
.withZoneSameInstant(ZoneId.of(pushdate.getTimezone()))
.toLocalDateTime();
}
private boolean isBetween(Date nextPushDate, PushDate pushDate) {
LocalDateTime minNextPushDate = this.getMinNextPushDate(pushDate);
LocalDateTime maxNextPushDate = this.getMaxNextPushDate(pushDate);
// When
LocalDateTime nextPushDateime = this.toDateAtTimezone(nextPushDate, pushDate);
// Then
return TimeUtils.isBetween(nextPushDateime, minNextPushDate, maxNextPushDate);
}
@Test
public void testGetNextPushDateWhenPushDateIsNull() {
// Given
PushDate pushDate = null;
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertFalse(nextPushDate.isPresent());
}
@Test
public void testGetNextPushDateWhenPushDateMinHourIsGreaterThanMaxHour() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(17)
.maxPushHour(10)
.lastPushDate(new Date())
.build();
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertFalse(nextPushDate.isPresent());
}
@Test
public void testGetNextPushDateWhenTimezoneIsNull() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(6)
.maxPushHour(10)
.lastPushDate(new Date())
.build();
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertFalse(nextPushDate.isPresent());
}
@Test
public void testGetNextPushDateWhenTimezoneIsBlank() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(6)
.maxPushHour(10)
.timezone("")
.lastPushDate(new Date())
.build();
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertFalse(nextPushDate.isPresent());
}
@Test
public void testGetNextPushDateWhenTimezoneIsInvalid() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(6)
.maxPushHour(10)
.timezone("Fake timezone")
.lastPushDate(new Date())
.build();
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertFalse(nextPushDate.isPresent());
}
@RepeatedTest(1000)
public void testGetNextPushDateSucceeds() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(6)
.maxPushHour(10)
.timezone("Europe/Paris")
.lastPushDate(TimeUtils.getNowAtTimeZoneUTC())
.build();
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertTrue(nextPushDate.isPresent());
assertTrue(this.isBetween(nextPushDate.get(), pushDate));
}
@RepeatedTest(1000)
public void testGetNextPushDateSucceedsWhenLastPushDateIsNull() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(6)
.maxPushHour(10)
.timezone("Europe/Paris")
.build();
// When
Optional<Date> nextPushDate = TimeUtils.getNextPushDate(pushDate);
// Then
assertTrue(nextPushDate.isPresent());
assertTrue(this.isBetween(nextPushDate.get(), pushDate));
}
@RepeatedTest(1000)
public void testGetNextPushDateSucceedsWithAnotherTimezone() {
// Given
PushDate pushDate = PushDate.builder()
.minPushHour(8)
.maxPushHour(19)
.timezone("America/Cayenne")
.lastPushDate(TimeUtils.getNowAtTimeZoneUTC())
.build();