Add request timezone, validation, and period start and end manipulation logic to $evaluate-measure.
This commit is contained in:
parent
1aef189069
commit
7fbdf30b72
|
@ -20,6 +20,8 @@
|
|||
package ca.uhn.fhir.util;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import com.google.common.base.Preconditions;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
@ -28,11 +30,21 @@ import java.lang.ref.SoftReference;
|
|||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.time.temporal.TemporalField;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
|
@ -93,6 +105,66 @@ public final class DateUtils {
|
|||
*/
|
||||
private DateUtils() {}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static String formatWithZoneAndOptionalDelta(
|
||||
LocalDateTime localDateTime,
|
||||
ZoneId theZoneId,
|
||||
DateTimeFormatter theDateTimeFormatter,
|
||||
@Nullable TemporalAmount theDelta) {
|
||||
final ZonedDateTime zonedDateTime = localDateTime.atZone(theZoneId);
|
||||
|
||||
final ZonedDateTime zonedDateTimeToFormat =
|
||||
Optional.ofNullable(theDelta).map(zonedDateTime::plus).orElse(zonedDateTime);
|
||||
|
||||
return zonedDateTimeToFormat.format(theDateTimeFormatter);
|
||||
}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static Optional<LocalDateTime> extractLocalDateTimeIfValid(TemporalAccessor theTemporalAccessor) {
|
||||
if (theTemporalAccessor.isSupported(ChronoField.YEAR)) {
|
||||
final int year = theTemporalAccessor.get(ChronoField.YEAR);
|
||||
final int month = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MONTH_OF_YEAR, 1);
|
||||
final int day = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.DAY_OF_MONTH, 1);
|
||||
final int hour = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.HOUR_OF_DAY, 0);
|
||||
final int minute = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MINUTE_OF_HOUR, 0);
|
||||
final int seconds = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.SECOND_OF_MINUTE, 0);
|
||||
|
||||
return Optional.of(LocalDateTime.of(year, month, day, hour, minute, seconds));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static Optional<TemporalAccessor> parseDateTimeStringIfValid(
|
||||
String theDateTimeString, DateTimeFormatter theSupportedDateTimeFormatter) {
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(theDateTimeString));
|
||||
|
||||
try {
|
||||
return Optional.of(theSupportedDateTimeFormatter.parse(theDateTimeString));
|
||||
} catch (Exception exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static Optional<ZoneOffset> getZoneOffsetIfSupported(TemporalAccessor theTemporalAccessor) {
|
||||
if (theTemporalAccessor.isSupported(ChronoField.OFFSET_SECONDS)) {
|
||||
return Optional.of(ZoneOffset.from(theTemporalAccessor));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static int getTimeUnitIfSupported(
|
||||
TemporalAccessor theTemporalAccessor, TemporalField theTemporalField, int theDefaultValue) {
|
||||
if (theTemporalAccessor.isSupported(theTemporalField)) {
|
||||
return theTemporalAccessor.get(theTemporalField);
|
||||
}
|
||||
|
||||
return theDefaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory for {@link SimpleDateFormat}s. The instances are stored in a
|
||||
* threadlocal way because SimpleDateFormat is not thread safe as noted in
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
package ca.uhn.fhir.util;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Month;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.time.temporal.TemporalField;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DateUtilsTest {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(DateUtilsTest.class);
|
||||
|
||||
private static final ZoneId TIMEZONE_NEWFOUNDLAND = ZoneId.of("America/St_Johns");
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
private static final Duration MINUS_ONE_SECOND = Duration.of(-1, ChronoUnit.SECONDS);
|
||||
|
||||
|
||||
private static Stream<Arguments> extractLocalDateTimeIfValidParams() {
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024),
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2023, 2),
|
||||
LocalDateTime.of(2023, Month.FEBRUARY, 1, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2022, 9),
|
||||
LocalDateTime.of(2022, Month.SEPTEMBER, 1, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2021, 3, 24),
|
||||
LocalDateTime.of(2021, Month.MARCH, 24, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 10, 23),
|
||||
LocalDateTime.of(2024, Month.OCTOBER, 23, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 8, 24, 12),
|
||||
LocalDateTime.of(2024, Month.AUGUST, 24, 12, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 11, 24, 12, 35),
|
||||
LocalDateTime.of(2024, Month.NOVEMBER, 24, 12, 35, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 9, 24, 12, 35, 47),
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> formatWithZoneAndOptionalDeltaParams() {
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-01-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-02-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-08-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-03-24T12:35:47Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-09-24T12:35:47Z"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31T23:59:59Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31T23:59:59Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31T23:59:59Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24T12:35:46Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24T12:35:46Z"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-01-01T00:00:00-03:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-02-01T00:00:00-03:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-08-01T00:00:00-02:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-03-24T12:35:47-02:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-09-24T12:35:47-02:30"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31T23:59:59-03:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31T23:59:59-03:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31T23:59:59-02:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24T12:35:46-02:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24T12:35:46-02:30"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-01-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-02-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-08-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-03-24T12:35:47Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-09-24T12:35:47Z"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-01-01"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-02-01"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-08-01"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-03-24"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-09-24"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("extractLocalDateTimeIfValidParams")
|
||||
void extractLocalDateTimeIfValid (
|
||||
TemporalAccessor theTemporalAccessor,
|
||||
@Nullable LocalDateTime theExpectedResult) {
|
||||
assertThat(DateUtils.extractLocalDateTimeIfValid(theTemporalAccessor))
|
||||
.isEqualTo(Optional.ofNullable(theExpectedResult));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("formatWithZoneAndOptionalDeltaParams")
|
||||
void formatWithZoneAndOptionalDelta (
|
||||
LocalDateTime theLocalDateTime,
|
||||
ZoneId theZoneId,
|
||||
DateTimeFormatter theDateTimeFormatter,
|
||||
@Nullable TemporalAmount theDelta,
|
||||
String theExpectedResult) {
|
||||
|
||||
assertThat(DateUtils.formatWithZoneAndOptionalDelta(theLocalDateTime, theZoneId, theDateTimeFormatter, theDelta))
|
||||
.isEqualTo(theExpectedResult);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear) {
|
||||
return getTemporalAccessor(theYear, null, null, null, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth) {
|
||||
return getTemporalAccessor(theYear, theMonth, null, null, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth, int theDay) {
|
||||
return getTemporalAccessor(theYear, theMonth, theDay, null, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth, int theDay, int theHour) {
|
||||
return getTemporalAccessor(theYear, theMonth, theDay, theHour, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth, int theDay, int theHour, int theMinute) {
|
||||
return getTemporalAccessor(theYear, theMonth, theDay, theHour, theMinute, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(
|
||||
int year,
|
||||
@Nullable Integer month,
|
||||
@Nullable Integer day,
|
||||
@Nullable Integer hour,
|
||||
@Nullable Integer minute,
|
||||
@Nullable Integer second) {
|
||||
final TemporalAccessor temporalAccessor = mock(TemporalAccessor.class);
|
||||
|
||||
mockAccessor(temporalAccessor, ChronoField.YEAR, year);
|
||||
mockAccessor(temporalAccessor, ChronoField.MONTH_OF_YEAR, month);
|
||||
mockAccessor(temporalAccessor, ChronoField.DAY_OF_MONTH, day);
|
||||
mockAccessor(temporalAccessor, ChronoField.HOUR_OF_DAY, hour);
|
||||
mockAccessor(temporalAccessor, ChronoField.MINUTE_OF_HOUR, minute);
|
||||
mockAccessor(temporalAccessor, ChronoField.SECOND_OF_MINUTE, second);
|
||||
|
||||
return temporalAccessor;
|
||||
}
|
||||
|
||||
private static void mockAccessor(TemporalAccessor theTemporalAccessor, TemporalField theTemporalField, @Nullable Integer theValue) {
|
||||
if (theValue != null) {
|
||||
when(theTemporalAccessor.isSupported(theTemporalField)).thenReturn(true);
|
||||
when(theTemporalAccessor.get(theTemporalField)).thenReturn(theValue);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ import ca.uhn.fhir.cr.r4.measure.CareGapsOperationProvider;
|
|||
import ca.uhn.fhir.cr.r4.measure.CollectDataOperationProvider;
|
||||
import ca.uhn.fhir.cr.r4.measure.DataRequirementsOperationProvider;
|
||||
import ca.uhn.fhir.cr.r4.measure.MeasureOperationsProvider;
|
||||
import ca.uhn.fhir.cr.r4.measure.MeasureReportPeriodRequestProcessingService;
|
||||
import ca.uhn.fhir.cr.r4.measure.SubmitDataProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import org.opencds.cqf.fhir.cql.EvaluationSettings;
|
||||
|
@ -57,6 +58,7 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
@ -145,8 +147,11 @@ public class CrR4Config {
|
|||
}
|
||||
|
||||
@Bean
|
||||
MeasureOperationsProvider r4MeasureOperationsProvider() {
|
||||
return new MeasureOperationsProvider();
|
||||
MeasureOperationsProvider r4MeasureOperationsProvider(
|
||||
IMeasureServiceFactory theR4MeasureServiceFactory,
|
||||
MeasureReportPeriodRequestProcessingService theMeasureReportPeriodRequestProcessingService) {
|
||||
return new MeasureOperationsProvider(
|
||||
theR4MeasureServiceFactory, theMeasureReportPeriodRequestProcessingService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -168,4 +173,9 @@ public class CrR4Config {
|
|||
|
||||
return new ProviderLoader(theRestfulServer, theApplicationContext, selector);
|
||||
}
|
||||
|
||||
@Bean
|
||||
MeasureReportPeriodRequestProcessingService measureReportPeriodService() {
|
||||
return new MeasureReportPeriodRequestProcessingService(ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,11 +34,17 @@ import org.hl7.fhir.r4.model.Measure;
|
|||
import org.hl7.fhir.r4.model.MeasureReport;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.opencds.cqf.fhir.utility.monad.Eithers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public class MeasureOperationsProvider {
|
||||
@Autowired
|
||||
IMeasureServiceFactory myR4MeasureServiceFactory;
|
||||
private final IMeasureServiceFactory myR4MeasureServiceFactory;
|
||||
private final MeasureReportPeriodRequestProcessingService myMeasureReportPeriodRequestProcessingService;
|
||||
|
||||
public MeasureOperationsProvider(
|
||||
IMeasureServiceFactory theR4MeasureServiceFactory,
|
||||
MeasureReportPeriodRequestProcessingService theMeasureReportPeriodRequestProcessingService) {
|
||||
myR4MeasureServiceFactory = theR4MeasureServiceFactory;
|
||||
myMeasureReportPeriodRequestProcessingService = theMeasureReportPeriodRequestProcessingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the <a href=
|
||||
|
@ -78,12 +84,17 @@ public class MeasureOperationsProvider {
|
|||
@OperationParam(name = "parameters") Parameters theParameters,
|
||||
RequestDetails theRequestDetails)
|
||||
throws InternalErrorException, FHIRException {
|
||||
|
||||
final MeasurePeriodForEvaluation measurePeriodForEvaluation =
|
||||
myMeasureReportPeriodRequestProcessingService.validateAndProcessTimezone(
|
||||
theRequestDetails, thePeriodStart, thePeriodEnd);
|
||||
|
||||
return myR4MeasureServiceFactory
|
||||
.create(theRequestDetails)
|
||||
.evaluate(
|
||||
Eithers.forMiddle3(theId),
|
||||
thePeriodStart,
|
||||
thePeriodEnd,
|
||||
measurePeriodForEvaluation.getPeriodStart(),
|
||||
measurePeriodForEvaluation.getPeriodEnd(),
|
||||
theReportType,
|
||||
theSubject,
|
||||
theLastReceivedOn,
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Clinical Reasoning
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.cr.r4.measure;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
// LUKETODO: javadoc
|
||||
// TODO: LD: make this a record when hapi-fhir supports JDK 17
|
||||
public class MeasurePeriodForEvaluation {
|
||||
private final String myPeriodStart;
|
||||
private final String myPeriodEnd;
|
||||
|
||||
public MeasurePeriodForEvaluation(String thePeriodStart, String thePeriodEnd) {
|
||||
myPeriodStart = thePeriodStart;
|
||||
myPeriodEnd = thePeriodEnd;
|
||||
}
|
||||
|
||||
public String getPeriodStart() {
|
||||
return myPeriodStart;
|
||||
}
|
||||
|
||||
public String getPeriodEnd() {
|
||||
return myPeriodEnd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object theO) {
|
||||
if (this == theO) {
|
||||
return true;
|
||||
}
|
||||
if (theO == null || getClass() != theO.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MeasurePeriodForEvaluation that = (MeasurePeriodForEvaluation) theO;
|
||||
return Objects.equals(myPeriodStart, that.myPeriodStart) && Objects.equals(myPeriodEnd, that.myPeriodEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(myPeriodStart, myPeriodEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", MeasurePeriodForEvaluation.class.getSimpleName() + "[", "]")
|
||||
.add("myPeriodStart='" + myPeriodStart + "'")
|
||||
.add("myPeriodEnd='" + myPeriodEnd + "'")
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Clinical Reasoning
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.cr.r4.measure;
|
||||
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.DateUtils;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
// LUKETODO: copyright header
|
||||
// LUKETODO: javadoc
|
||||
// LUKETODO: unit test
|
||||
/*
|
||||
according to the docs: periodStart and periodEnd support Dates (YYYY, YYYY-MM, or YYYY-MM-DD) and DateTimes (YYYY-MM-DDThh:mm:ss+zz:zz)
|
||||
if the user passes in:
|
||||
Header: America/Toronto
|
||||
periodStart “2024-09-25T12:00:00-06:00”
|
||||
which timezone do we respect, or do we convert the periodStart to EDT?
|
||||
also, what happens if they pass in start and end in different formats?
|
||||
periodStart “2024-09-25T12:00:00-06:00"
|
||||
periodEnd: “2024-10-01”
|
||||
also, if we don’t already, I’ll return a 400 if they pass a start that’s after an end
|
||||
*/
|
||||
|
||||
/*
|
||||
| request input | request timezone | delta | converted input to CR |
|
||||
| --------------| ---------------- | ------ | ------------------------- |
|
||||
| 2024-09-24 | null | null | 2024-09-24T00:00:00Z |
|
||||
| 2024-09-24 | UTC | null | 2024-09-24T00:00:00Z |
|
||||
| 2024-09-24 | Newfoundland | null | 2024-09-24T00:00:00-02:30 |
|
||||
| 2024-09-24 | Eastern | null | 2024-09-24T00:00:00-04:00 |
|
||||
| 2024-09-24 | Mountain | null | 2024-09-24T00:00:00-06:00 |
|
||||
| 2024-02-24 | null | null | 2024-02-24T00:00:00Z |
|
||||
| 2024-02-24 | UTC | null | 2024-02-24T00:00:00Z |
|
||||
| 2024-02-24 | Newfoundland | null | 2024-02-24T00:00:00-03:30 |
|
||||
| 2024-02-24 | Eastern | null | 2024-02-24T00:00:00-05:00 |
|
||||
| 2024-02-24 | Mountain | null | 2024-02-24T00:00:00-07:00 |
|
||||
| 2024-09-24 | null | -1 sec | 2024-09-24T23:00:59Z |
|
||||
| 2024-09-24 | UTC | -1 sec | 2024-09-24T23:59:59Z |
|
||||
| 2024-09-24 | Newfoundland | -1 sec | 2024-09-24T23:59:59-02:30 |
|
||||
| 2024-09-24 | Eastern | -1 sec | 2024-09-24T23:59:59-04:00 |
|
||||
| 2024-09-24 | Mountain | -1 sec | 2024-09-24T23:59:59-06:00 |
|
||||
| 2024-02-24 | null | -1 sec | 2024-02-24T23:59:59Z |
|
||||
| 2024-02-24 | UTC | -1 sec | 2024-02-24T23:59:59Z |
|
||||
| 2024-02-24 | Newfoundland | -1 sec | 2024-02-24T23:59:59-03:30 |
|
||||
| 2024-02-24 | Eastern | -1 sec | 2024-02-24T23:59:59-05:00 |
|
||||
| 2024-02-24 | Mountain | -1 sec | 2024-02-24T23:59:59-07:00 |
|
||||
*/
|
||||
public class MeasureReportPeriodRequestProcessingService {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(MeasureReportPeriodRequestProcessingService.class);
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_INPUT = DateTimeFormatter.ofPattern("yyyy");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_INPUT = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_INPUT = DateTimeFormatter.ISO_DATE;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT =
|
||||
DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
private static final Duration MINUS_ONE_SECOND = Duration.of(-1, ChronoUnit.SECONDS);
|
||||
|
||||
private static final Map<Integer, DateTimeFormatter> VALID_DATE_TIME_FORMATTERS_BY_FORMAT_LENGTH = Map.of(
|
||||
4, DATE_TIME_FORMATTER_YYYY_INPUT,
|
||||
7, DATE_TIME_FORMATTER_YYYY_MM_INPUT,
|
||||
10, DATE_TIME_FORMATTER_YYYY_MM_DD_INPUT,
|
||||
20, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT,
|
||||
25, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT);
|
||||
|
||||
private final ZoneId myFallbackTimezone;
|
||||
|
||||
public MeasureReportPeriodRequestProcessingService(ZoneId theFallbackTimezone) {
|
||||
myFallbackTimezone = theFallbackTimezone;
|
||||
}
|
||||
|
||||
public MeasurePeriodForEvaluation validateAndProcessTimezone(
|
||||
RequestDetails theRequestDetails, String thePeriodStart, String thePeriodEnd) {
|
||||
final ZoneId clientTimezone = getClientTimezoneOrInvalidRequest(theRequestDetails);
|
||||
|
||||
return validateInputDates(thePeriodStart, thePeriodEnd, clientTimezone);
|
||||
}
|
||||
|
||||
private MeasurePeriodForEvaluation validateInputDates(
|
||||
String thePeriodStart, String thePeriodEnd, ZoneId theZoneId) {
|
||||
if (Strings.isBlank(thePeriodStart) || Strings.isBlank(thePeriodEnd)) {
|
||||
throw new InvalidRequestException(
|
||||
String.format("Either start: [%s] or end: [%s] or both are blank", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
if (thePeriodStart.length() != thePeriodEnd.length()) {
|
||||
throw new InvalidRequestException(String.format(
|
||||
"Period start: %s and end: %s are not the same date/time formats", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
final DateTimeFormatter dateTimeFormatterStart =
|
||||
VALID_DATE_TIME_FORMATTERS_BY_FORMAT_LENGTH.get(thePeriodStart.length());
|
||||
|
||||
if (dateTimeFormatterStart == null) {
|
||||
throw new InvalidRequestException(String.format(
|
||||
"Either start: %s or end: %s or both have an supported date/time format",
|
||||
thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
final Optional<TemporalAccessor> optTemporalAccessorStart =
|
||||
DateUtils.parseDateTimeStringIfValid(thePeriodStart, dateTimeFormatterStart);
|
||||
final Optional<TemporalAccessor> optTemporalAccessorEnd =
|
||||
DateUtils.parseDateTimeStringIfValid(thePeriodEnd, dateTimeFormatterStart);
|
||||
|
||||
if (optTemporalAccessorStart.isEmpty() || optTemporalAccessorEnd.isEmpty()) {
|
||||
throw new InvalidRequestException("Either start or end period have an unsupported format");
|
||||
}
|
||||
|
||||
final Optional<LocalDateTime> optLocalDateTimeStart =
|
||||
DateUtils.extractLocalDateTimeIfValid(optTemporalAccessorStart.get());
|
||||
final Optional<LocalDateTime> optLocalDateTimeEnd =
|
||||
DateUtils.extractLocalDateTimeIfValid(optTemporalAccessorEnd.get());
|
||||
|
||||
final Optional<ZoneOffset> optZoneOffsetStart =
|
||||
DateUtils.getZoneOffsetIfSupported(optTemporalAccessorStart.get());
|
||||
final Optional<ZoneOffset> optZoneOffsetEnd = DateUtils.getZoneOffsetIfSupported(optTemporalAccessorEnd.get());
|
||||
|
||||
if (optLocalDateTimeStart.isEmpty() || optLocalDateTimeEnd.isEmpty()) {
|
||||
throw new InvalidRequestException("Either start or end period have an unsupported format");
|
||||
}
|
||||
|
||||
final LocalDateTime localDateTimeStart = optLocalDateTimeStart.get();
|
||||
final LocalDateTime localDateTimeEnd = optLocalDateTimeEnd.get();
|
||||
|
||||
if (localDateTimeStart.isEqual(localDateTimeEnd)) {
|
||||
throw new InvalidRequestException(
|
||||
String.format("Start date: %s is the same as end date: %s", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
if (localDateTimeStart.isAfter(localDateTimeEnd)) {
|
||||
throw new InvalidRequestException(
|
||||
String.format("Start date: %s is after end date: %s", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
if (!optZoneOffsetStart.equals(optZoneOffsetEnd)) {
|
||||
throw new InvalidRequestException(String.format(
|
||||
"Zone offsets do not match for period start: %s and end: %s", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
// Either the input Strings have no offset
|
||||
|
||||
// Or the request timezone is different from the default
|
||||
if (optZoneOffsetStart.isEmpty() || !myFallbackTimezone.getRules().equals(theZoneId.getRules())) {
|
||||
|
||||
// Preserve backwards compatibility
|
||||
if (optZoneOffsetStart.isPresent()) {
|
||||
ourLog.warn(
|
||||
"Start offset is not the same as the timezone header. Ignoring both start and stop offsets for start: {} amd: {}",
|
||||
thePeriodStart,
|
||||
thePeriodEnd);
|
||||
}
|
||||
|
||||
return new MeasurePeriodForEvaluation(
|
||||
formatWithZonePeriodStart(localDateTimeStart, theZoneId),
|
||||
formatWithZonePeriodEnd(localDateTimeEnd, theZoneId));
|
||||
}
|
||||
|
||||
return new MeasurePeriodForEvaluation(
|
||||
formatWithZonePeriodStart(localDateTimeStart, optZoneOffsetStart.get()),
|
||||
formatWithZonePeriodEnd(localDateTimeEnd, optZoneOffsetEnd.get()));
|
||||
}
|
||||
|
||||
private String formatWithZonePeriodStart(LocalDateTime theLocalDateTime, ZoneId theZoneId) {
|
||||
return formatWithZoneAndOptionalDelta(theLocalDateTime, theZoneId, null);
|
||||
}
|
||||
|
||||
private String formatWithZonePeriodEnd(LocalDateTime theLocalDateTime, ZoneId theZoneId) {
|
||||
return formatWithZoneAndOptionalDelta(theLocalDateTime, theZoneId, MINUS_ONE_SECOND);
|
||||
}
|
||||
|
||||
private String formatWithZoneAndOptionalDelta(
|
||||
LocalDateTime theLocalDateTime, ZoneId theZoneId, @Nullable TemporalAmount theDelta) {
|
||||
return DateUtils.formatWithZoneAndOptionalDelta(
|
||||
theLocalDateTime, theZoneId, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT, theDelta);
|
||||
}
|
||||
|
||||
private ZoneId getClientTimezoneOrInvalidRequest(RequestDetails theRequestDetails) {
|
||||
final String clientTimezoneString = theRequestDetails.getHeader(Constants.HEADER_CLIENT_TIMEZONE);
|
||||
|
||||
if (Strings.isNotBlank(clientTimezoneString)) {
|
||||
try {
|
||||
return ZoneId.of(clientTimezoneString);
|
||||
} catch (Exception exception) {
|
||||
throw new InvalidRequestException("Invalid value for Timezone header: " + clientTimezoneString);
|
||||
}
|
||||
}
|
||||
|
||||
return myFallbackTimezone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package ca.uhn.fhir.cr.r4.measure;
|
||||
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class MeasureReportPeriodRequestProcessingServiceTest {
|
||||
|
||||
private final MeasureReportPeriodRequestProcessingService myTestSubject = new MeasureReportPeriodRequestProcessingService(ZoneOffset.UTC);
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource( nullValues = {"null"},
|
||||
value={
|
||||
"null, 2020, 2021, 2020-01-01T00:00:00Z, 2020-12-31T23:59:59Z",
|
||||
"UTC, 2020, 2021, 2020-01-01T00:00:00Z, 2020-12-31T23:59:59Z",
|
||||
"America/St_Johns, 2020, 2021, 2020-01-01T00:00:00-03:30, 2020-12-31T23:59:59-03:30",
|
||||
"America/Toronto, 2020, 2021, 2020-01-01T00:00:00-05:00, 2020-12-31T23:59:59-05:00",
|
||||
"America/Denver, 2020, 2021, 2020-01-01T00:00:00-07:00, 2020-12-31T23:59:59-07:00",
|
||||
|
||||
"null, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-07-31T23:59:59Z",
|
||||
"UTC, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-07-31T23:59:59Z",
|
||||
"America/St_Johns, 2022-02, 2022-08, 2022-02-01T00:00:00-03:30, 2022-07-31T23:59:59-02:30",
|
||||
"America/Toronto, 2022-02, 2022-08, 2022-02-01T00:00:00-05:00, 2022-07-31T23:59:59-04:00",
|
||||
"America/Denver, 2022-02, 2022-08, 2022-02-01T00:00:00-07:00, 2022-07-31T23:59:59-06:00",
|
||||
|
||||
"null, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-25T23:59:59Z",
|
||||
"UTC, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-25T23:59:59Z",
|
||||
"America/St_Johns, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-03:30, 2024-02-25T23:59:59-03:30",
|
||||
"America/Toronto, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-05:00, 2024-02-25T23:59:59-05:00",
|
||||
"America/Denver, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-07:00, 2024-02-25T23:59:59-07:00",
|
||||
|
||||
"null, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-25T23:59:59Z",
|
||||
"UTC, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-25T23:59:59Z",
|
||||
"America/St_Johns, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-02:30, 2024-09-25T23:59:59-02:30",
|
||||
"America/Toronto, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-04:00, 2024-09-25T23:59:59-04:00",
|
||||
"America/Denver, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-06:00, 2024-09-25T23:59:59-06:00",
|
||||
|
||||
"null, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-06:00, 2024-09-26T11:59:59-06:00",
|
||||
"UTC, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-06:00, 2024-09-26T11:59:59-06:00",
|
||||
"America/St_Johns, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-02:30, 2024-09-26T11:59:59-02:30",
|
||||
"America/Toronto, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-04:00, 2024-09-26T11:59:59-04:00",
|
||||
"America/Denver, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-06:00, 2024-09-26T11:59:59-06:00",
|
||||
}
|
||||
)
|
||||
void validateAndProcessTimezone_happyPath(@Nullable String theTimezone, String theInputPeriodStart, String theInputPeriodEnd, String theOutputPeriodStart, String theOutputPeriodEnd) {
|
||||
|
||||
final MeasurePeriodForEvaluation actualResult =
|
||||
myTestSubject.validateAndProcessTimezone(getRequestDetails(theTimezone), theInputPeriodStart, theInputPeriodEnd);
|
||||
|
||||
final MeasurePeriodForEvaluation expectedResult = new MeasurePeriodForEvaluation(theOutputPeriodStart, theOutputPeriodEnd);
|
||||
assertThat(actualResult).isEqualTo(expectedResult);
|
||||
}
|
||||
|
||||
public static Stream<Arguments> errorParams() {
|
||||
return Stream.of(
|
||||
Arguments.of(null, null, null, new InvalidRequestException("Either start: [null] or end: [null] or both are blank")),
|
||||
Arguments.of(null, "", "", new InvalidRequestException("Either start: [] or end: [] or both are blank")),
|
||||
Arguments.of(null, "2024", "2024-01", new InvalidRequestException("Period start: 2024 and end: 2024-01 are not the same date/time formats")),
|
||||
Arguments.of(null, "2024-01-01T12", "2024-01-01T12", new InvalidRequestException("Either start: 2024-01-01T12 or end: 2024-01-01T12 or both have an supported date/time format")),
|
||||
Arguments.of(null, "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Zone offsets do not match for period start: 2024-01-01T12:00:00-02:30 and end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of(null, "2024-01-01", "2024-01-01", new InvalidRequestException("Start date: 2024-01-01 is the same as end date: 2024-01-01")),
|
||||
Arguments.of(null, "2024-01-02", "2024-01-01", new InvalidRequestException("Start date: 2024-01-02 is after end date: 2024-01-01")),
|
||||
Arguments.of("Middle-Earth/Combe", "2024-01-02", "2024-01-03", new InvalidRequestException("Invalid value for Timezone header: Middle-Earth/Combe"))
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("errorParams")
|
||||
void validateAndProcessTimezone_errorPaths(@Nullable String theTimezone, @Nullable String theInputPeriodStart, @Nullable String theInputPeriodEnd, InvalidRequestException theExpectedResult) {
|
||||
assertThatThrownBy(() -> myTestSubject.validateAndProcessTimezone(getRequestDetails(theTimezone), theInputPeriodStart, theInputPeriodEnd))
|
||||
.hasMessage(theExpectedResult.getMessage())
|
||||
.isInstanceOf(theExpectedResult.getClass());
|
||||
}
|
||||
|
||||
private static RequestDetails getRequestDetails(@Nullable String theTimezone) {
|
||||
final SystemRequestDetails systemRequestDetails = new SystemRequestDetails();
|
||||
Optional.ofNullable(theTimezone)
|
||||
.ifPresent(nonNullTimezone -> systemRequestDetails .addHeader(Constants.HEADER_CLIENT_TIMEZONE, nonNullTimezone));
|
||||
return systemRequestDetails;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue