diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c159772..656f7485a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ _**For better traceability add the corresponding GitHub issue number in each cha ### Changed +- The date search operators `AFTER_LOCAL_DATE` and `BEFORE_LOCAL_DATE` for fields `createdOn` and `validUntil` support any ISO date time now (relates to #639 and #750). - Improved documentation for `GET /irs/policies/paged` endpoint. #639 - Cleanup in IrsApplicationTest.generatedOpenApiMatchesContract (removed obsolete ignoringFields, improved assertion message) diff --git a/docs/src/api/irs-api.yaml b/docs/src/api/irs-api.yaml index 3624b9357..8d338c6a7 100644 --- a/docs/src/api/irs-api.yaml +++ b/docs/src/api/irs-api.yaml @@ -1047,13 +1047,14 @@ paths: ```\n \n### Filtering\n \n`search=,[EQUALS|STARTS_WITH|BEFORE_LOCAL_DATE|AFTER_LOCAL_DATE],`.\n\ \ \nExample: `search=BPN,STARTS_WITH,BPNL12&search=policyId,STARTS_WITH,policy2`.\n\ \ \n| Field | Supported Operations | Value Format\ - \ |\n|--------------|------------------------------------------|----------------------|\n\ + \ |\n|--------------|------------------------------------------|------------------------------------|\n\ | `BPN` | `EQUALS`, `STARTS_WITH` | any string \ - \ |\n| `policyId` | `EQUALS`, `STARTS_WITH` | any\ - \ string |\n| `action` | `EQUALS` \ - \ | `use` or `access` |\n| `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE`\ - \ | `yyyy-MM-dd` |\n| `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE`\ - \ | `yyyy-MM-dd` |\n\n### Sorting\n`sort=[BPN|policyId|action|createdOn|validUntil],[asc|desc]`.\n\ + \ |\n| `policyId` | `EQUALS`, `STARTS_WITH` \ + \ | any string |\n| `action` | `EQUALS`\ + \ | `use` or `access` |\n\ + | `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` or\ + \ ISO date with time |\n| `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE`\ + \ | `yyyy-MM-dd` or ISO date with time |\n\n### Sorting\n`sort=[BPN|policyId|action|createdOn|validUntil],[asc|desc]`.\n\ \ \nExample: `sort=BPN,asc&sort=policyId,desc`. _(default: `BPN,asc`)_\n\n\ ### Paging\n \nExample: `page=1&size=20` _(default page size: 10, maximal\ \ page size: 1000)_\n" diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java index 11f5c8eb4..4f9f13b6b 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java @@ -23,25 +23,41 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; /** * Date utilities. */ +@Slf4j public final class DateUtils { + public static final String SIMPLE_DATE_WITHOUT_TIME = "yyyy-MM-dd"; + private DateUtils() { // private constructor (utility class) } public static boolean isDateBefore(final OffsetDateTime dateTime, final String referenceDateString) { - return dateTime.isBefore(toOffsetDateTimeAtStartOfDay(referenceDateString)); + if (isDateWithoutTime(referenceDateString)) { + return dateTime.isBefore(toOffsetDateTimeAtStartOfDay(referenceDateString)); + } else { + return dateTime.isBefore(OffsetDateTime.parse(referenceDateString)); + } } public static boolean isDateAfter(final OffsetDateTime dateTime, final String referenceDateString) { - return dateTime.isAfter(toOffsetDateTimeAtEndOfDay(referenceDateString)); + if (StringUtils.isBlank(referenceDateString)) { + throw new IllegalArgumentException("Invalid date: must not be blank!"); + } + if (isDateWithoutTime(referenceDateString)) { + return dateTime.isAfter(toOffsetDateTimeAtEndOfDay(referenceDateString)); + } else { + return dateTime.isAfter(OffsetDateTime.parse(referenceDateString)); + } } public static OffsetDateTime toOffsetDateTimeAtStartOfDay(final String dateString) { @@ -62,4 +78,31 @@ private static LocalDate parseDate(final String dateString) { throw new IllegalArgumentException("Invalid date format (please refer to the documentation)", e); } } + + @SuppressWarnings("PMD.PreserveStackTrace") // this is intended here as we try to parse with different formats + public static boolean isDateWithoutTime(final String referenceDateString) { + try { + DateTimeFormatter.ofPattern(SIMPLE_DATE_WITHOUT_TIME).parse(referenceDateString); + return true; + } catch (DateTimeParseException e) { + // ignore, trying next format below + log.trace(e.getMessage(), e); + } + + try { + OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE); + return true; + } catch (DateTimeParseException e) { + // ignore, trying next format below + log.trace(e.getMessage(), e); + } + + try { + OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE_TIME); + return false; + } catch (DateTimeParseException e) { + log.trace(e.getMessage(), e); + throw new IllegalArgumentException("Invalid date format: " + referenceDateString, e); + } + } } diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java index beb077c6a..a62bc75fd 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java @@ -196,7 +196,8 @@ public Map> getPolicies(// @RequestParam(required = false) // @ValidListOfBusinessPartnerNumbers(allowDefault = true) // @Parameter(description = "List of business partner numbers. " - + "This may also contain the value \"default\" in order to query the default policies.") // + + "This may also contain the value \"default\" in order to query the default policies.") + // final List businessPartnerNumbers // ) { @@ -263,7 +264,7 @@ public List autocomplete( @GetMapping("/policies/paged") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Find registered policies that should be accepted in EDC negotiation " - + "(with filtering, sorting and paging).", // + + "(with filtering, sorting and paging).", // description = """ Fetch a page of policies with options to filter and sort. \s @@ -278,13 +279,13 @@ public List autocomplete( \s Example: `search=BPN,STARTS_WITH,BPNL12&search=policyId,STARTS_WITH,policy2`. \s - | Field | Supported Operations | Value Format | - |--------------|------------------------------------------|----------------------| - | `BPN` | `EQUALS`, `STARTS_WITH` | any string | - | `policyId` | `EQUALS`, `STARTS_WITH` | any string | - | `action` | `EQUALS` | `use` or `access` | - | `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` | - | `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` | + | Field | Supported Operations | Value Format | + |--------------|------------------------------------------|------------------------------------| + | `BPN` | `EQUALS`, `STARTS_WITH` | any string | + | `policyId` | `EQUALS`, `STARTS_WITH` | any string | + | `action` | `EQUALS` | `use` or `access` | + | `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` or ISO date with time | + | `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` or ISO date with time | ### Sorting `sort=[BPN|policyId|action|createdOn|validUntil],[asc|desc]`. @@ -326,7 +327,8 @@ public Page getPoliciesPaged(// @RequestParam(required = false) // @ValidListOfBusinessPartnerNumbers(allowDefault = true) // @Parameter(name = "businessPartnerNumbers", description = "List of business partner numbers. " - + "This may also contain the value \"default\" in order to query the default policies.") // + + "This may also contain the value \"default\" in order to query the default policies.") + // final List businessPartnerNumbers) { if (pageable.getPageSize() > MAX_PAGE_SIZE) { diff --git a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java index 3d2ebd8f4..2a68d3de4 100644 --- a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java +++ b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java @@ -30,6 +30,7 @@ import java.util.stream.Stream; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -45,11 +46,25 @@ void testIsDateBefore(final OffsetDateTime dateTime, final String referenceDateS } static Stream provideDatesForIsDateBefore() { - final OffsetDateTime referenceDateTime = LocalDate.parse("2024-07-05").atStartOfDay().atOffset(ZoneOffset.UTC); return Stream.of( // - Arguments.of(referenceDateTime, "2024-07-04", false), - Arguments.of(referenceDateTime, "2024-07-05", false), - Arguments.of(referenceDateTime, "2024-07-06", true)); + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-04", false), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-05", false), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-06", true), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:30:00Z", false), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:30:01Z", true), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:45:00+05:30", false), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:46:01+05:30", true), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:15:30-04:00", false), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:16:01-04:00", true), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.123Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.124Z", true), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-29T00:00Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-30T00:01Z", true) // + ); + } + + private static OffsetDateTime atStartOfDay(final String date) { + return LocalDate.parse(date).atStartOfDay().atOffset(ZoneOffset.UTC); } @ParameterizedTest @@ -59,19 +74,50 @@ void testIsDateAfter(final OffsetDateTime dateTime, final String dateString, fin } static Stream provideDatesForIsDateAfter() { - final OffsetDateTime referenceDateTime = LocalDate.parse("2023-07-05") - .atTime(LocalTime.MAX) - .atOffset(ZoneOffset.UTC); return Stream.of( // - Arguments.of(referenceDateTime, "2023-07-04", true), - Arguments.of(referenceDateTime, "2023-07-05", false), - Arguments.of(referenceDateTime, "2023-07-06", false)); + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-04", true), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-05", false), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-06", false), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:30:00Z", false), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:29:59Z", true), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:45:00+05:30", false), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:44:59+05:30", true), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:15:30-04:00", false), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:15:29-04:00", true), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.123Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.122Z", true), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-29T00:00Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-28T00:01Z", true) // + ); + } + + private static OffsetDateTime atEndOfDay(final String dateStr) { + return LocalDate.parse(dateStr).atTime(LocalTime.MAX).atOffset(ZoneOffset.UTC); + } + + @ParameterizedTest + @MethodSource("provideDatesForIsDateWithoutTime") + public void isDateWithoutTime(final String dateString, final boolean expected) { + assertThat(DateUtils.isDateWithoutTime(dateString)).isEqualTo(expected); + } + + static Stream provideDatesForIsDateWithoutTime() { + return Stream.of( // + Arguments.of("2023-07-23", true), // + Arguments.of("2023-07-23T10:15:30+01:00", false), // + Arguments.of("2023-07-23T10:15:30Z", false) // + ); + } + + @Test + public void testIsDateWithoutTimeWithInvalidDate() { + assertThatThrownBy(() -> DateUtils.isDateWithoutTime("invalid-date")).isInstanceOf( + IllegalArgumentException.class).hasMessageContaining("Invalid date format: invalid-date"); } @ParameterizedTest - @ValueSource(strings = { "3333-11-11T11:11:11.111Z", - "3333-11-", + @ValueSource(strings = { "3333-11-", "2222", "asdf" }) @@ -79,7 +125,6 @@ void testInvalidDate(final String referenceDateStr) { final ThrowingCallable call = () -> DateUtils.isDateAfter(OffsetDateTime.now(), referenceDateStr); assertThatThrownBy(call).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid date") - .hasMessageContaining("refer to the documentation") .hasCauseInstanceOf(DateTimeParseException.class); }