diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java index 8c164c37510..aa20383f423 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java @@ -215,6 +215,15 @@ public class DateRangeParam implements IQueryParameterAnd { * This lower bound is assumed to have a ge * (greater than or equals) modifier. *

+ *

+ * Note: An operation can take a DateRangeParam. If only a single date is provided, + * it will still result in a DateRangeParam where the lower and upper bounds + * are the same value. As such, even though the prefixes for the lower and + * upper bounds default to ge and le respectively, + * the resulting prefix is effectively eq where only a single + * date is provided - as required by the FHIR specificiation (i.e. "If no + * prefix is present, the prefix eq is assumed"). + *

*/ public DateRangeParam setLowerBound(String theLowerBound) { setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)); @@ -281,7 +290,7 @@ public class DateRangeParam implements IQueryParameterAnd { case LESSTHAN_OR_EQUALS: case ENDS_BEFORE: case NOT_EQUAL: - throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix()); + throw new IllegalStateException("Invalid lower bound comparator: " + myLowerBound.getPrefix()); } } return retVal; @@ -298,6 +307,15 @@ public class DateRangeParam implements IQueryParameterAnd { * This upper bound is assumed to have a le * (less than or equals) modifier. *

+ *

+ * Note: An operation can take a DateRangeParam. If only a single date is provided, + * it will still result in a DateRangeParam where the lower and upper bounds + * are the same value. As such, even though the prefixes for the lower and + * upper bounds default to ge and le respectively, + * the resulting prefix is effectively eq where only a single + * date is provided - as required by the FHIR specificiation (i.e. "If no + * prefix is present, the prefix eq is assumed"). + *

*/ public DateRangeParam setUpperBound(String theUpperBound) { setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)); @@ -339,7 +357,7 @@ public class DateRangeParam implements IQueryParameterAnd { case APPROXIMATE: case NOT_EQUAL: case STARTS_AFTER: - throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix()); + throw new IllegalStateException("Invalid upper bound comparator: " + myUpperBound.getPrefix()); } } return retVal; @@ -370,6 +388,104 @@ public class DateRangeParam implements IQueryParameterAnd { return Objects.hash(myLowerBound, myUpperBound); } + public boolean isDateWithinRange(Date theDate) { + boolean retVal = false; + + if (theDate == null) { + throw new NullPointerException("theDate can not be null"); + } + + boolean hasLowerBound = hasBound(myLowerBound); + boolean hasUpperBound = hasBound(myUpperBound); + boolean hasLowerAndUpperBounds = hasLowerBound && hasUpperBound && (myLowerBound.getValue().getTime() != myUpperBound.getValue().getTime()); + + if (hasLowerAndUpperBounds) { + retVal = isDateWithinLowerAndUpperBounds(theDate); + } else if (hasLowerBound) { + retVal = isDateWithinLowerBound(theDate); + } else if (hasUpperBound) { + retVal = isDateWithinUpperBound(theDate); + } + + return retVal; + } + + private boolean isDateWithinLowerAndUpperBounds(Date theDate) { + return isDateWithinLowerBound(theDate, true) && isDateWithinUpperBound(theDate, true); + } + + public boolean isDateWithinLowerBound(Date theDate) { + return isDateWithinLowerBound(theDate, false); + } + + private boolean isDateWithinLowerBound(Date theDate, boolean theIsRange) { + boolean retVal = false; + + if (theDate == null) { + throw new NullPointerException("theDate can not be null"); + } + + if (hasBound(myLowerBound)) { + long lowerBound = myLowerBound.getValue().getTime(); + switch (myLowerBound.getPrefix()) { + case GREATERTHAN: + case STARTS_AFTER: + retVal = theDate.getTime() > lowerBound; + break; + case EQUAL: + if (theIsRange) { + retVal = theDate.getTime() >= lowerBound; + } else { + retVal = theDate.getTime() == lowerBound; + } + break; + case GREATERTHAN_OR_EQUALS: + retVal = theDate.getTime() >= lowerBound; + break; + default: + throw new IllegalStateException("Invalid lower bound comparator: " + myLowerBound.getPrefix()); + } + } + + return retVal; + } + + public boolean isDateWithinUpperBound(Date theDate) { + return isDateWithinUpperBound(theDate, false); + } + + private boolean isDateWithinUpperBound(Date theDate, boolean theIsRange) { + boolean retVal = false; + + if (theDate == null) { + throw new NullPointerException("theDate can not be null"); + } + + if (hasBound(myUpperBound)) { + long upperBound = myUpperBound.getValue().getTime(); + switch (myUpperBound.getPrefix()) { + case LESSTHAN: + case ENDS_BEFORE: + retVal = theDate.getTime() < upperBound; + break; + case EQUAL: + if (theIsRange) { + retVal = theDate.getTime() <= upperBound; + } else { + retVal = theDate.getTime() == upperBound; + } + break; + case LESSTHAN_OR_EQUALS: + retVal = theDate.getTime() <= upperBound; + break; + default: + throw new IllegalStateException("Invalid upper bound comparator: " + myUpperBound.getPrefix()); + } + } + + return retVal; + } + public boolean isEmpty() { return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null); } @@ -520,6 +636,15 @@ public class DateRangeParam implements IQueryParameterAnd { return b.toString(); } + /** + * Note: An operation can take a DateRangeParam. If only a single date is provided, + * it will still result in a DateRangeParam where the lower and upper bounds + * are the same value. As such, even though the prefixes for the lower and + * upper bounds default to ge and le respectively, + * the resulting prefix is effectively eq where only a single + * date is provided - as required by the FHIR specificiation (i.e. "If no + * prefix is present, the prefix eq is assumed"). + */ private void validateAndSet(DateParam lowerBound, DateParam upperBound) { if (hasBound(lowerBound) && hasBound(upperBound)) { if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java index 72b03f5b4c9..6cfd86d3ad2 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java @@ -1,28 +1,209 @@ package ca.uhn.fhir.rest.param; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.List; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.QualifiedParamList; - import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + @RunWith(JUnit4.class) public class DateRangeParamTest { private FhirContext fhirContext; + private Date myBefore; + private Date myLower; + private Date myBetween; + private Date myUpper; + private Date myAfter; + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); @Before public void initMockContext() { fhirContext = Mockito.mock(FhirContext.class); } + @Before + public void initDates() throws InterruptedException { + myBefore = new Date(); + Thread.sleep(1L); + myLower = new Date(); + Thread.sleep(1L); + myBetween = new Date(); + Thread.sleep(1L); + myUpper = new Date(); + Thread.sleep(1L); + myAfter = new Date(); + } + + @Test + public void testIsDateWithinRangeExclusive() { + DateParam lowerBound = new DateParam(GREATERTHAN, myLower); + DateParam upperBound = new DateParam(LESSTHAN, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(lowerBound, upperBound); + + assertFalse(dateRangeParam.isDateWithinRange(myBefore)); + assertFalse(dateRangeParam.isDateWithinRange(myLower)); + assertTrue(dateRangeParam.isDateWithinRange(myBetween)); + assertFalse(dateRangeParam.isDateWithinRange(myUpper)); + assertFalse(dateRangeParam.isDateWithinRange(myAfter)); + } + + @Test + public void testIsDateWithinRangeInclusive() { + DateParam lowerBound = new DateParam(EQUAL, myLower); + DateParam upperBound = new DateParam(EQUAL, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(lowerBound, upperBound); + + assertFalse(dateRangeParam.isDateWithinRange(myBefore)); + assertTrue(dateRangeParam.isDateWithinRange(myLower)); + assertTrue(dateRangeParam.isDateWithinRange(myBetween)); + assertTrue(dateRangeParam.isDateWithinRange(myUpper)); + assertFalse(dateRangeParam.isDateWithinRange(myAfter)); + } + + @Test + public void testIsDateWithinRangeOnlyLower() { + DateParam lowerBound = new DateParam(EQUAL, myLower); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setLowerBound(lowerBound); + + assertFalse(dateRangeParam.isDateWithinRange(myBefore)); + assertTrue(dateRangeParam.isDateWithinRange(myLower)); + assertFalse(dateRangeParam.isDateWithinRange(myBetween)); + assertFalse(dateRangeParam.isDateWithinRange(myUpper)); + assertFalse(dateRangeParam.isDateWithinRange(myAfter)); + } + + @Test + public void testIsDateWithinRangeOnlyUpper() { + DateParam upperBound = new DateParam(EQUAL, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setUpperBound(upperBound); + + assertFalse(dateRangeParam.isDateWithinRange(myBefore)); + assertFalse(dateRangeParam.isDateWithinRange(myLower)); + assertFalse(dateRangeParam.isDateWithinRange(myBetween)); + assertTrue(dateRangeParam.isDateWithinRange(myUpper)); + assertFalse(dateRangeParam.isDateWithinRange(myAfter)); + } + + @Test + public void testIsDateWithinLowerBoundGreaterThan() { + DateParam lowerBound = new DateParam(GREATERTHAN, myLower); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setLowerBound(lowerBound); + + assertFalse(dateRangeParam.isDateWithinLowerBound(myBefore)); + assertFalse(dateRangeParam.isDateWithinLowerBound(myLower)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myBetween)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myUpper)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myAfter)); + } + + @Test + public void testIsDateWithinLowerBoundStartsAfter() { + DateParam lowerBound = new DateParam(STARTS_AFTER, myLower); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setLowerBound(lowerBound); + + assertFalse(dateRangeParam.isDateWithinLowerBound(myBefore)); + assertFalse(dateRangeParam.isDateWithinLowerBound(myLower)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myBetween)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myUpper)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myAfter)); + } + + @Test + public void testIsDateWithinLowerBoundEqual() { + DateParam lowerBound = new DateParam(EQUAL, myLower); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setLowerBound(lowerBound); + + assertFalse(dateRangeParam.isDateWithinLowerBound(myBefore)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myLower)); + assertFalse(dateRangeParam.isDateWithinLowerBound(myBetween)); + assertFalse(dateRangeParam.isDateWithinLowerBound(myUpper)); + assertFalse(dateRangeParam.isDateWithinLowerBound(myAfter)); + } + + @Test + public void testIsDateWithinLowerBoundGreaterThanOrEquals() { + DateParam lowerBound = new DateParam(GREATERTHAN_OR_EQUALS, myLower); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setLowerBound(lowerBound); + + assertFalse(dateRangeParam.isDateWithinLowerBound(myBefore)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myLower)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myBetween)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myUpper)); + assertTrue(dateRangeParam.isDateWithinLowerBound(myAfter)); + } + + @Test + public void testIsDateWithinUpperBoundLessThan() { + DateParam upperBound = new DateParam(LESSTHAN, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setUpperBound(upperBound); + + assertTrue(dateRangeParam.isDateWithinUpperBound(myBefore)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myLower)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myBetween)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myUpper)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myAfter)); + } + + @Test + public void testIsDateWithinUpperBoundEndsBefore() { + DateParam upperBound = new DateParam(ENDS_BEFORE, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setUpperBound(upperBound); + + assertTrue(dateRangeParam.isDateWithinUpperBound(myBefore)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myLower)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myBetween)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myUpper)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myAfter)); + } + + @Test + public void testIsDateWithinUpperBoundEqual() { + DateParam upperBound = new DateParam(EQUAL, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setUpperBound(upperBound); + + assertFalse(dateRangeParam.isDateWithinUpperBound(myBefore)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myLower)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myBetween)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myUpper)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myAfter)); + } + + @Test + public void testIsDateWithinUpperBoundLessThanOrEquals() { + DateParam upperBound = new DateParam(LESSTHAN_OR_EQUALS, myUpper); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setUpperBound(upperBound); + + assertTrue(dateRangeParam.isDateWithinUpperBound(myBefore)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myLower)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myBetween)); + assertTrue(dateRangeParam.isDateWithinUpperBound(myUpper)); + assertFalse(dateRangeParam.isDateWithinUpperBound(myAfter)); + } + /** Can happen e.g. when the query parameter for {@code _lastUpdated} is left empty. */ @Test public void testParamWithoutPrefixAndWithoutValue() { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_2_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_2_0/changes.yaml index e0c2bfd651c..751ab54a333 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_2_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_2_0/changes.yaml @@ -41,18 +41,27 @@ type: "fix" title: "A missing mandatory was added to the SNOMED CT CodeSystem that is uploaded when SCT is uploaded to the JPA server. Thanks to Anders Havn for the pull request!" - item: - issue: 1643 - type: fix + issue: "1643" + type: "fix" title: "When validating resources containing custom valuesets defined in PrePopulatedValidationSupport outside of the JPA server, sometimes code systems could not be found resulting in false negative errors." - item: - issue: 1588 - type: add + issue: "1588" + type: "add" title: "Support for several new operators has been added to the `_filter` support in the JPA server. Thanks to Anthony Sute for the Pull Request!" - item: - issue: 1650 - type: fix - title: Several misleading comments in documentation code snippets were fixed. Thanks to - Jafer Khan for the pull request! + issue: "1650" + type: "fix" + title: "Several misleading comments in documentation code snippets were fixed. Thanks to + Jafer Khan for the pull request!" +- item: + issue: "1645" + type: "add" + title: "The following utility methods have been added: +
    +
  • DateRangeParam#isDateWithinRange(Date theDate)
  • +
  • DateRangeParam#isDateWithinLowerBound(Date theDate)
  • +
  • DateRangeParam#isDateWithinUpperBound(Date theDate)
  • +
"