diff --git a/iris-client-bff/pom.xml b/iris-client-bff/pom.xml index 14b5a4a6b..067c2a3e2 100644 --- a/iris-client-bff/pom.xml +++ b/iris-client-bff/pom.xml @@ -159,6 +159,12 @@ 3.0.4 test + + com.github.javafaker + javafaker + 1.0.2 + test + diff --git a/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataRequestRepository.java b/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataRequestRepository.java index 2677f49b8..0b2b0084d 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataRequestRepository.java +++ b/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataRequestRepository.java @@ -10,6 +10,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.util.Streamable; /** * @author Jens Kutzsche @@ -27,9 +28,17 @@ public interface CaseDataRequestRepository extends JpaRepository findByStatus(Status status, Pageable pageable); - Page findByRefIdContainsOrNameContainsAllIgnoreCase(String search, String search1, Pageable pageable); + Page findByRefIdContainsOrNameContainsAllIgnoreCase(String search, String search1, + Pageable pageable); @Query("select r from CaseDataRequest r where r.status = :status and ( upper(r.refId) like concat('%', upper(:search), '%') or upper(r.name) like concat('%', upper(:search), '%'))") Page findByStatusAndSearchByRefIdOrName(Status status, String search, Pageable pageable); + /** + * Returns the {@link CaseDataRequest}s created before the given {@link Instant}. + * + * @param refDate must not be {@literal null}. + * @return + */ + Streamable findByMetadataCreatedIsBefore(Instant refDate); } diff --git a/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataSubmissionRepository.java b/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataSubmissionRepository.java index 722d3cd52..ac08ca08f 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataSubmissionRepository.java +++ b/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDataSubmissionRepository.java @@ -1,15 +1,18 @@ package iris.client_bff.cases; import iris.client_bff.cases.model.CaseDataSubmission; - import iris.client_bff.cases.model.CaseDataSubmission.DataSubmissionIdentifier; + import javax.transaction.Transactional; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.util.Streamable; -public interface CaseDataSubmissionRepository extends CrudRepository { +public interface CaseDataSubmissionRepository extends JpaRepository { @Transactional Streamable findAllByRequest(CaseDataRequest request); + + @Transactional + void deleteAllByRequestIn(Iterable requests); } diff --git a/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDeleteJob.java b/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDeleteJob.java new file mode 100644 index 000000000..ed157fe5a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/cases/CaseDeleteJob.java @@ -0,0 +1,76 @@ +package iris.client_bff.cases; + +import iris.client_bff.cases.CaseDataRequest.DataRequestIdentifier; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDate; +import java.time.Period; +import java.time.ZoneId; +import java.util.stream.Collectors; + +import javax.transaction.Transactional; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * This class collects all old cases and deletes this. + * + * @author Jens Kutzsche + */ +@Component +@Slf4j +@RequiredArgsConstructor +class CaseDeleteJob { + + private final @NonNull CaseDataRequestRepository caseRequests; + private final @NonNull CaseDataSubmissionRepository caseSubmissions; + private final @NonNull CaseDeletionProperties properties; + + @Transactional + @Scheduled(cron = "${iris.client.case.delete-cron:-}") + void deleteCaseRequests() { + + var refDate = LocalDate.now().minus(properties.getDeleteAfter()).atStartOfDay().atZone(ZoneId.systemDefault()) + .toInstant(); + + var oldRequests = caseRequests.findByMetadataCreatedIsBefore(refDate).toList(); + + if (oldRequests.isEmpty()) { + return; + } + + log.debug("{} case data request(s) are deleted with period {} after their creation!", + oldRequests, + properties.getDeleteAfter(), + oldRequests.get(0).getCreatedAt()); + + caseSubmissions.deleteAllByRequestIn(oldRequests); + caseRequests.deleteAll(oldRequests); + + log.info("{} case data request(s) (IDs: {}) were deleted with period {} after their creation at {}!", + oldRequests.size(), + oldRequests.stream().map(CaseDataRequest::getId) + .map(DataRequestIdentifier::toString) + .collect(Collectors.joining(", ")), + properties.getDeleteAfter(), + oldRequests.get(0).getCreatedAt()); + } + + @ConstructorBinding + @RequiredArgsConstructor + @ConfigurationProperties("iris.client.case") + @Getter + public static class CaseDeletionProperties { + + /** + * Defines the {@link Period} after that a case will be deleted starting from the creation date. + */ + private final Period deleteAfter; + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/core/IrisDateTimeProvider.java b/iris-client-bff/src/main/java/iris/client_bff/core/IrisDateTimeProvider.java index 276e20471..13a8ca089 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/core/IrisDateTimeProvider.java +++ b/iris-client-bff/src/main/java/iris/client_bff/core/IrisDateTimeProvider.java @@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; -import java.time.Instant; +import java.time.LocalDateTime; import java.time.Period; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAmount; @@ -42,7 +42,7 @@ public void reset() { */ @Override public Optional getNow() { - return Optional.of(Instant.now().plus(delta)); + return Optional.of(LocalDateTime.now().plus(delta)); } @ConstructorBinding diff --git a/iris-client-bff/src/main/java/iris/client_bff/events/EventDataRequestRepository.java b/iris-client-bff/src/main/java/iris/client_bff/events/EventDataRequestRepository.java index d0c88ef91..5666ec39e 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/events/EventDataRequestRepository.java +++ b/iris-client-bff/src/main/java/iris/client_bff/events/EventDataRequestRepository.java @@ -10,6 +10,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.util.Streamable; /** * @author Jens Kutzsche @@ -27,8 +28,17 @@ public interface EventDataRequestRepository extends JpaRepository findByStatus(Status status, Pageable pageable); - Page findByRefIdContainsOrNameContainsAllIgnoreCase(String search, String search1, Pageable pageable); + Page findByRefIdContainsOrNameContainsAllIgnoreCase(String search, String search1, + Pageable pageable); @Query("select r from EventDataRequest r where r.status = :status and ( upper(r.refId) like concat('%', upper(:search), '%') or upper(r.name) like concat('%', upper(:search), '%'))") Page findByStatusAndSearchByRefIdOrName(Status status, String search, Pageable pageable); + + /** + * Returns the {@link EventDataRequest}s created before the given {@link Instant}. + * + * @param refDate must not be {@literal null}. + * @return + */ + Streamable findByMetadataCreatedIsBefore(Instant refDate); } diff --git a/iris-client-bff/src/main/java/iris/client_bff/events/EventDataSubmissionRepository.java b/iris-client-bff/src/main/java/iris/client_bff/events/EventDataSubmissionRepository.java index 3e92eb208..fdc4a2af1 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/events/EventDataSubmissionRepository.java +++ b/iris-client-bff/src/main/java/iris/client_bff/events/EventDataSubmissionRepository.java @@ -5,14 +5,17 @@ import javax.transaction.Transactional; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.util.Streamable; /** * @author Jens Kutzsche */ -public interface EventDataSubmissionRepository extends CrudRepository { +public interface EventDataSubmissionRepository extends JpaRepository { @Transactional Streamable findAllByRequest(EventDataRequest request); + + @Transactional + void deleteAllByRequestIn(Iterable requests); } diff --git a/iris-client-bff/src/main/java/iris/client_bff/events/EventDeleteJob.java b/iris-client-bff/src/main/java/iris/client_bff/events/EventDeleteJob.java new file mode 100644 index 000000000..6f5df94f3 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/events/EventDeleteJob.java @@ -0,0 +1,75 @@ +package iris.client_bff.events; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDate; +import java.time.Period; +import java.time.ZoneId; +import java.util.stream.Collectors; + +import javax.transaction.Transactional; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * This class collects all old events and deletes this. + * + * @author Jens Kutzsche + */ +@Component +@Slf4j +@RequiredArgsConstructor +class EventDeleteJob { + + private final @NonNull EventDataRequestRepository eventRequests; + private final @NonNull EventDataSubmissionRepository eventSubmissions; + private final @NonNull EventDeletionProperties properties; + + @Transactional + @Scheduled(cron = "${iris.client.event.delete-cron:-}") + void deleteEventRequests() { + + var refDate = LocalDate.now().minus(properties.getDeleteAfter()).atStartOfDay().atZone(ZoneId.systemDefault()) + .toInstant(); + + var oldRequests = eventRequests.findByMetadataCreatedIsBefore(refDate).toList(); + + if (oldRequests.isEmpty()) { + return; + } + + log.debug("{} event data request(s) are deleted with period {} after their creation!", + oldRequests, + properties.getDeleteAfter(), + oldRequests.get(0).getCreatedAt()); + + eventSubmissions.deleteAllByRequestIn(oldRequests); + eventRequests.deleteAll(oldRequests); + + log.info("{} event data request(s) (IDs: {}) were deleted with period {} after their creation at {}!", + oldRequests.size(), + oldRequests.stream().map(EventDataRequest::getId) + .map(EventDataRequest.DataRequestIdentifier::toString) + .collect(Collectors.joining(", ")), + properties.getDeleteAfter(), + oldRequests.get(0).getCreatedAt()); + } + + @ConstructorBinding + @RequiredArgsConstructor + @ConfigurationProperties("iris.client.event") + @Getter + public static class EventDeletionProperties { + + /** + * Defines the {@link Period} after that a event will be deleted starting from the creation date. + */ + private final Period deleteAfter; + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/events/model/Location.java b/iris-client-bff/src/main/java/iris/client_bff/events/model/Location.java index 68040328c..0c5a6a634 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/events/model/Location.java +++ b/iris-client-bff/src/main/java/iris/client_bff/events/model/Location.java @@ -1,6 +1,7 @@ package iris.client_bff.events.model; import iris.client_bff.core.Id; +import iris.client_bff.events.EventDataRequest; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,6 +14,7 @@ import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; +import javax.persistence.OneToOne; @Entity @Data @@ -45,6 +47,9 @@ public class Location { private String contactPhone; + @OneToOne(mappedBy = "location") + private EventDataRequest request; + @Embeddable @EqualsAndHashCode @Getter diff --git a/iris-client-bff/src/main/resources/application.properties b/iris-client-bff/src/main/resources/application.properties index 79a1bf7fa..5b3dfe507 100644 --- a/iris-client-bff/src/main/resources/application.properties +++ b/iris-client-bff/src/main/resources/application.properties @@ -9,6 +9,10 @@ spring.application.name=IRIS Client Backend for Frontend iris.client.mailing.active=false iris.client.sendAbort.active=false +iris.client.case.delete-after=6m +iris.client.case.delete-cron=0 30 1 * * * +iris.client.event.delete-after=6m +iris.client.event.delete-cron=0 30 1 * * * # Spring Mail spring.mail.host=127.0.0.1 diff --git a/iris-client-bff/src/test/java/iris/client_bff/FakerConfig.java b/iris-client-bff/src/test/java/iris/client_bff/FakerConfig.java new file mode 100644 index 000000000..50ec9223a --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/FakerConfig.java @@ -0,0 +1,43 @@ +package iris.client_bff; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Locale; +import java.util.Random; + +import org.apache.commons.lang3.RandomUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.github.javafaker.Faker; + +/** + * @author Jens Kutzsche + */ +@Configuration +@Slf4j +public class FakerConfig { + + @Value("${iris.test.faker.seed:#{null}}") + Long preConfiguredSeed; + + static Faker faker; + + @Bean + public Faker getFaker() { + + if (faker == null) { + + var seed = preConfiguredSeed == null ? RandomUtils.nextLong() : preConfiguredSeed; + + log.info( + "Faker is created with SEED = {}; The seed can be set with property iris.test.faker.seed or env IRIS_TEST_FAKER_SEED.", + seed); + + faker = new Faker(Locale.GERMANY, new Random(seed)); + } + + return faker; + } +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/cases/CaseDeleteJobIntegrationTests.java b/iris-client-bff/src/test/java/iris/client_bff/cases/CaseDeleteJobIntegrationTests.java new file mode 100644 index 000000000..3d7b87af6 --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/cases/CaseDeleteJobIntegrationTests.java @@ -0,0 +1,79 @@ +package iris.client_bff.cases; + +import static org.assertj.core.api.Assertions.*; + +import iris.client_bff.IrisWebIntegrationTest; +import iris.client_bff.cases.model.CaseDataSubmission; +import iris.client_bff.core.IrisDateTimeProvider; +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.github.javafaker.Faker; + +/** + * @author Jens Kutzsche + */ +@IrisWebIntegrationTest +@RequiredArgsConstructor +class CaseDeleteJobIntegrationTests { + + private final CaseDataRequestRepository caseRequests; + private final CaseDataSubmissionRepository caseSubmissions; + private final IrisDateTimeProvider dateTimeProvider; + private final CaseDeleteJob deleteJob; + private final Faker faker; + + @Test // Issue #244 + void testDeleteCaseRequests() { + + var requestsSize = caseRequests.findAll().size(); + var submissionSize = caseSubmissions.findAll().size(); + + // in time + dateTimeProvider.setDelta(Period.ofMonths(-6)); + + var request = createRequest(faker.name().name(), faker.idNumber().valid(), + faker.date().past(200, 186, TimeUnit.DAYS).toInstant()); + createSubmission(request); + + // to old + dateTimeProvider.setDelta(Period.ofMonths(-6).minusDays(1)); + + var oldName = faker.name().name(); + + request = createRequest(oldName, faker.idNumber().valid(), faker.date().past(200, 186, TimeUnit.DAYS).toInstant()); + createSubmission(request); + + dateTimeProvider.reset(); + + // extra element from data initialization + assertThat(caseRequests.findAll()).hasSize(requestsSize + 2).extracting(CaseDataRequest::getName).contains(oldName); + assertThat(caseSubmissions.findAll()).hasSize(submissionSize + 2); + + deleteJob.deleteCaseRequests(); + + assertThat(caseRequests.findAll()).hasSize(requestsSize + 1).extracting(CaseDataRequest::getName) + .doesNotContain(oldName); + assertThat(caseSubmissions.findAll()).hasSize(submissionSize + 1); + } + + private CaseDataRequest createRequest(String name, String refId, Instant date) { + + return caseRequests.save(CaseDataRequest.builder() + .name(name) + .refId(refId) + .requestStart(date) + .requestEnd(date.plus(6, ChronoUnit.HOURS)) + .build()); + } + + private void createSubmission(CaseDataRequest request) { + caseSubmissions.save(new CaseDataSubmission(request, null, null, null, null, null, null, null)); + } +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/core/mail/TestEmailServer.java b/iris-client-bff/src/test/java/iris/client_bff/core/mail/TestEmailServer.java index 92f0e1091..9cff17a1b 100644 --- a/iris-client-bff/src/test/java/iris/client_bff/core/mail/TestEmailServer.java +++ b/iris-client-bff/src/test/java/iris/client_bff/core/mail/TestEmailServer.java @@ -23,7 +23,6 @@ * Wrapper around a {@link GreenMail} instance to simplify testing assertions. * * @author Oliver Drotbohm - * @since 1.4 */ @Slf4j @Component diff --git a/iris-client-bff/src/test/java/iris/client_bff/events/EventDeleteJobIntegrationTests.java b/iris-client-bff/src/test/java/iris/client_bff/events/EventDeleteJobIntegrationTests.java new file mode 100644 index 000000000..76f26ed7f --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/events/EventDeleteJobIntegrationTests.java @@ -0,0 +1,89 @@ +package iris.client_bff.events; + +import static org.assertj.core.api.Assertions.*; + +import iris.client_bff.IrisWebIntegrationTest; +import iris.client_bff.core.IrisDateTimeProvider; +import iris.client_bff.events.model.EventDataSubmission; +import iris.client_bff.events.model.Location; +import iris.client_bff.events.model.Location.LocationIdentifier; +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.github.javafaker.Faker; + +/** + * @author Jens Kutzsche + */ +@IrisWebIntegrationTest +@RequiredArgsConstructor +class EventDeleteJobIntegrationTests { + + private final EventDataRequestRepository eventRequests; + private final EventDataSubmissionRepository eventSubmissions; + private final IrisDateTimeProvider dateTimeProvider; + private final EventDeleteJob deleteJob; + private final Faker faker; + + @Test // Issue #244 + void testDeleteEventRequests() { + + var requestsSize = eventRequests.findAll().size(); + var submissionSize = eventSubmissions.findAll().size(); + + // in time + dateTimeProvider.setDelta(Period.ofMonths(-6)); + + var request = createRequest(faker.name().name(), faker.idNumber().valid(), + faker.date().past(200, 186, TimeUnit.DAYS).toInstant()); + createSubmission(request); + + // to old + dateTimeProvider.setDelta(Period.ofMonths(-6).minusDays(1)); + + var oldName = faker.name().name(); + + request = createRequest(oldName, faker.idNumber().valid(), faker.date().past(200, 186, TimeUnit.DAYS).toInstant()); + createSubmission(request); + + dateTimeProvider.reset(); + + // extra element from data initialization + assertThat(eventRequests.findAll()).hasSize(requestsSize + 2).element(4).satisfies(it -> { + assertThat(it.getName()).isEqualTo(oldName); + }); + assertThat(eventSubmissions.findAll()).hasSize(submissionSize + 2); + + deleteJob.deleteEventRequests(); + + assertThat(eventRequests.findAll()).hasSize(requestsSize + 1).extracting(EventDataRequest::getName) + .doesNotContain(oldName); + assertThat(eventSubmissions.findAll()).hasSize(submissionSize + 1); + } + + private EventDataRequest createRequest(String name, String refId, Instant date) { + + var location = new Location(new LocationIdentifier(), faker.idNumber().valid(), faker.idNumber().valid(), null, + null, null, null, + null, null, + null, null, null, null); + + return eventRequests.save(EventDataRequest.builder() + .name(name) + .refId(refId) + .requestStart(date) + .requestEnd(date.plus(6, ChronoUnit.HOURS)) + .location(location) + .build()); + } + + private void createSubmission(EventDataRequest request) { + eventSubmissions.save(new EventDataSubmission(request, null, null, null, null, null)); + } +}