Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/639 support any iso date #844

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 7 additions & 6 deletions docs/src/api/irs-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1047,13 +1047,14 @@ paths:
```\n \n### Filtering\n \n`search=<Field>,[EQUALS|STARTS_WITH|BEFORE_LOCAL_DATE|AFTER_LOCAL_DATE],<Value>`.\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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ public Map<String, List<PolicyResponse>> 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<String> businessPartnerNumbers //
) {

Expand Down Expand Up @@ -263,7 +264,7 @@ public List<String> 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
Expand All @@ -278,13 +279,13 @@ public List<String> 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]`.
Expand Down Expand Up @@ -326,7 +327,8 @@ public Page<PolicyResponse> 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<String> businessPartnerNumbers) {

if (pageable.getPageSize() > MAX_PAGE_SIZE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,11 +46,25 @@ void testIsDateBefore(final OffsetDateTime dateTime, final String referenceDateS
}

static Stream<Arguments> 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
Expand All @@ -59,27 +74,57 @@ void testIsDateAfter(final OffsetDateTime dateTime, final String dateString, fin
}

static Stream<Arguments> 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<Arguments> 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"
})
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);
}

Expand Down
Loading