001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.param; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.model.api.IQueryParameterAnd; 025import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 026import ca.uhn.fhir.parser.DataFormatException; 027import ca.uhn.fhir.rest.api.QualifiedParamList; 028import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 029import ca.uhn.fhir.util.DateUtils; 030import jakarta.annotation.Nonnull; 031import org.apache.commons.lang3.ObjectUtils; 032import org.apache.commons.lang3.Validate; 033import org.hl7.fhir.instance.model.api.IPrimitiveType; 034 035import java.util.ArrayList; 036import java.util.Date; 037import java.util.List; 038import java.util.Objects; 039 040import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL; 041import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; 042import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; 043import static java.lang.String.format; 044import static org.apache.commons.lang3.StringUtils.isNotBlank; 045 046@SuppressWarnings("UnusedReturnValue") 047public class DateRangeParam implements IQueryParameterAnd<DateParam> { 048 049 private static final long serialVersionUID = 1L; 050 051 private DateParam myLowerBound; 052 private DateParam myUpperBound; 053 054 /** 055 * Basic constructor. Values must be supplied by calling {@link #setLowerBound(DateParam)} and 056 * {@link #setUpperBound(DateParam)} 057 */ 058 public DateRangeParam() { 059 super(); 060 } 061 062 /** 063 * Copy constructor. 064 */ 065 @SuppressWarnings("CopyConstructorMissesField") 066 public DateRangeParam(DateRangeParam theDateRangeParam) { 067 super(); 068 Validate.notNull(theDateRangeParam); 069 setLowerBound(theDateRangeParam.getLowerBound()); 070 setUpperBound(theDateRangeParam.getUpperBound()); 071 } 072 073 /** 074 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) 075 * 076 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 077 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 078 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 079 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 080 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 081 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 082 */ 083 public DateRangeParam(Date theLowerBound, Date theUpperBound) { 084 this(); 085 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 086 } 087 088 /** 089 * Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound 090 * (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the lower 091 * or upper bound, with no opposite bound. 092 */ 093 public DateRangeParam(DateParam theDateParam) { 094 this(); 095 if (theDateParam == null) { 096 throw new NullPointerException(Msg.code(1919) + "theDateParam can not be null"); 097 } 098 if (theDateParam.isEmpty()) { 099 throw new IllegalArgumentException(Msg.code(1920) + "theDateParam can not be empty"); 100 } 101 if (theDateParam.getPrefix() == null) { 102 setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString()); 103 } else { 104 switch (theDateParam.getPrefix()) { 105 case NOT_EQUAL: 106 case EQUAL: 107 setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString()); 108 break; 109 case STARTS_AFTER: 110 case GREATERTHAN: 111 case GREATERTHAN_OR_EQUALS: 112 if (theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 113 theDateParam.setValueAsString(DateUtils.getCompletedDate(theDateParam.getValueAsString()) 114 .getRight()); 115 } 116 // there is only one value; we will set it as the lower bound 117 // as a >= operation 118 validateAndSet(theDateParam, null); 119 break; 120 case ENDS_BEFORE: 121 case LESSTHAN: 122 case LESSTHAN_OR_EQUALS: 123 if (theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 124 theDateParam.setValueAsString(DateUtils.getCompletedDate(theDateParam.getValueAsString()) 125 .getLeft()); 126 } 127 128 // there is only one value; we will set it as the upper bound 129 // as a <= operation 130 validateAndSet(null, theDateParam); 131 break; 132 default: 133 // Should not happen 134 throw new InvalidRequestException(Msg.code(1921) + "Invalid comparator for date range parameter:" 135 + theDateParam.getPrefix() + ". This is a bug."); 136 } 137 } 138 } 139 140 /** 141 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) 142 * 143 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 144 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 145 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 146 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 147 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 148 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 149 */ 150 public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) { 151 this(); 152 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 153 } 154 155 /** 156 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) 157 * 158 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 159 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 160 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 161 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 162 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 163 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 164 */ 165 public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) { 166 this(); 167 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 168 } 169 170 /** 171 * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends) 172 * 173 * @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g. 174 * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or 175 * one may be null, but it is not valid for both to be null. 176 * @param theUpperBound An unqualified date param representing the upper date bound (optionally may include time), e.g. 177 * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or 178 * one may be null, but it is not valid for both to be null. 179 */ 180 public DateRangeParam(String theLowerBound, String theUpperBound) { 181 this(); 182 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 183 } 184 185 private void addParam(DateParam theParsed) throws InvalidRequestException { 186 ParamPrefixEnum prefix = getPrefixOrDefault(theParsed); 187 188 switch (prefix) { 189 case NOT_EQUAL: 190 case EQUAL: 191 if (myLowerBound != null || myUpperBound != null) { 192 throw new InvalidRequestException(Msg.code(1922) 193 + "Can not have multiple date range parameters for the same param without a qualifier"); 194 } 195 if (theParsed.getMissing() != null) { 196 myLowerBound = theParsed; 197 myUpperBound = theParsed; 198 } else { 199 myLowerBound = new DateParam(theParsed.getPrefix(), theParsed.getValueAsString()); 200 myUpperBound = new DateParam(theParsed.getPrefix(), theParsed.getValueAsString()); 201 } 202 break; 203 case GREATERTHAN: 204 case GREATERTHAN_OR_EQUALS: 205 case STARTS_AFTER: 206 if (myLowerBound != null) { 207 throw new InvalidRequestException( 208 Msg.code(1923) 209 + "Can not have multiple date range parameters for the same param that specify a lower bound"); 210 } 211 myLowerBound = theParsed; 212 break; 213 case LESSTHAN: 214 case LESSTHAN_OR_EQUALS: 215 case ENDS_BEFORE: 216 if (myUpperBound != null) { 217 throw new InvalidRequestException( 218 Msg.code(1924) 219 + "Can not have multiple date range parameters for the same param that specify an upper bound"); 220 } 221 myUpperBound = theParsed; 222 break; 223 default: 224 throw new InvalidRequestException(Msg.code(1925) + "Unknown comparator: " + theParsed.getPrefix()); 225 } 226 } 227 228 @Override 229 public boolean equals(Object obj) { 230 if (obj == this) { 231 return true; 232 } 233 if (!(obj instanceof DateRangeParam)) { 234 return false; 235 } 236 DateRangeParam other = (DateRangeParam) obj; 237 return Objects.equals(myLowerBound, other.myLowerBound) && Objects.equals(myUpperBound, other.myUpperBound); 238 } 239 240 public DateParam getLowerBound() { 241 return myLowerBound; 242 } 243 244 public DateRangeParam setLowerBound(DateParam theLowerBound) { 245 validateAndSet(theLowerBound, myUpperBound); 246 return this; 247 } 248 249 /** 250 * Sets the lower bound using a string that is compliant with 251 * FHIR dateTime format (ISO-8601). 252 * <p> 253 * This lower bound is assumed to have a <code>ge</code> 254 * (greater than or equals) modifier. 255 * </p> 256 * <p> 257 * Note: An operation can take a DateRangeParam. If only a single date is provided, 258 * it will still result in a DateRangeParam where the lower and upper bounds 259 * are the same value. As such, even though the prefixes for the lower and 260 * upper bounds default to <code>ge</code> and <code>le</code> respectively, 261 * the resulting prefix is effectively <code>eq</code> where only a single 262 * date is provided - as required by the FHIR specification (i.e. "If no 263 * prefix is present, the prefix <code>eq</code> is assumed"). 264 * </p> 265 */ 266 public DateRangeParam setLowerBound(String theLowerBound) { 267 setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)); 268 return this; 269 } 270 271 /** 272 * Sets the lower bound to be greaterthan or equal to the given date 273 */ 274 public DateRangeParam setLowerBoundInclusive(Date theLowerBound) { 275 validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, theLowerBound), myUpperBound); 276 return this; 277 } 278 279 /** 280 * Sets the upper bound to be greaterthan or equal to the given date 281 */ 282 public DateRangeParam setUpperBoundInclusive(Date theUpperBound) { 283 validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, theUpperBound)); 284 return this; 285 } 286 287 /** 288 * Sets the lower bound to be greaterthan to the given date 289 */ 290 public DateRangeParam setLowerBoundExclusive(Date theLowerBound) { 291 validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN, theLowerBound), myUpperBound); 292 return this; 293 } 294 295 /** 296 * Sets the upper bound to be greaterthan to the given date 297 */ 298 public DateRangeParam setUpperBoundExclusive(Date theUpperBound) { 299 validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN, theUpperBound)); 300 return this; 301 } 302 303 /** 304 * Return the current lower bound as an integer representative of the date. 305 * 306 * e.g. 2019-02-22T04:22:00-0500 -> 20120922 307 */ 308 public Integer getLowerBoundAsDateInteger() { 309 if (myLowerBound == null || myLowerBound.getValue() == null) { 310 return null; 311 } 312 int retVal = DateUtils.convertDateToDayInteger(myLowerBound.getValue()); 313 314 ParamPrefixEnum prefix = getPrefixOrDefault(myLowerBound); 315 316 switch (prefix) { 317 case GREATERTHAN: 318 case STARTS_AFTER: 319 retVal += 1; 320 break; 321 case EQUAL: 322 case GREATERTHAN_OR_EQUALS: 323 case NOT_EQUAL: 324 break; 325 case LESSTHAN: 326 case LESSTHAN_OR_EQUALS: 327 case APPROXIMATE: 328 case ENDS_BEFORE: 329 throw new IllegalStateException(Msg.code(1926) + "Invalid lower bound comparator: " + prefix); 330 } 331 332 return retVal; 333 } 334 335 /** 336 * Return the current upper bound as an integer representative of the date 337 * 338 * e.g. 2019-02-22T04:22:00-0500 -> 2019122 339 */ 340 public Integer getUpperBoundAsDateInteger() { 341 if (myUpperBound == null || myUpperBound.getValue() == null) { 342 return null; 343 } 344 int retVal = DateUtils.convertDateToDayInteger(myUpperBound.getValue()); 345 346 ParamPrefixEnum prefix = getPrefixOrDefault(myUpperBound); 347 348 switch (prefix) { 349 case LESSTHAN: 350 case ENDS_BEFORE: 351 retVal -= 1; 352 break; 353 case EQUAL: 354 case LESSTHAN_OR_EQUALS: 355 case NOT_EQUAL: 356 break; 357 case GREATERTHAN_OR_EQUALS: 358 case GREATERTHAN: 359 case APPROXIMATE: 360 case STARTS_AFTER: 361 throw new IllegalStateException(Msg.code(1927) + "Invalid upper bound comparator: " + prefix); 362 } 363 364 return retVal; 365 } 366 367 public Date getLowerBoundAsInstant() { 368 if (myLowerBound == null || myLowerBound.getValue() == null) { 369 return null; 370 } 371 return getLowerBoundAsInstant(myLowerBound); 372 } 373 374 @Nonnull 375 private static Date getLowerBoundAsInstant(@Nonnull DateParam theLowerBound) { 376 Date retVal = theLowerBound.getValue(); 377 378 if (theLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) { 379 retVal = DateUtils.getLowestInstantFromDate(retVal); 380 } 381 382 ParamPrefixEnum prefix = getPrefixOrDefault(theLowerBound); 383 384 switch (prefix) { 385 case GREATERTHAN: 386 case STARTS_AFTER: 387 retVal = theLowerBound.getPrecision().add(retVal, 1); 388 break; 389 case EQUAL: 390 case NOT_EQUAL: 391 case GREATERTHAN_OR_EQUALS: 392 break; 393 case LESSTHAN_OR_EQUALS: 394 case LESSTHAN: 395 case APPROXIMATE: 396 case ENDS_BEFORE: 397 throw new IllegalStateException(Msg.code(1928) + "Invalid lower bound comparator: " + prefix); 398 } 399 400 return retVal; 401 } 402 403 public DateParam getUpperBound() { 404 return myUpperBound; 405 } 406 407 /** 408 * Sets the upper bound using a string that is compliant with 409 * FHIR dateTime format (ISO-8601). 410 * <p> 411 * This upper bound is assumed to have a <code>le</code> 412 * (less than or equals) modifier. 413 * </p> 414 * <p> 415 * Note: An operation can take a DateRangeParam. If only a single date is provided, 416 * it will still result in a DateRangeParam where the lower and upper bounds 417 * are the same value. As such, even though the prefixes for the lower and 418 * upper bounds default to <code>ge</code> and <code>le</code> respectively, 419 * the resulting prefix is effectively <code>eq</code> where only a single 420 * date is provided - as required by the FHIR specificiation (i.e. "If no 421 * prefix is present, the prefix <code>eq</code> is assumed"). 422 * </p> 423 */ 424 public DateRangeParam setUpperBound(String theUpperBound) { 425 setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)); 426 return this; 427 } 428 429 public DateRangeParam setUpperBound(DateParam theUpperBound) { 430 validateAndSet(myLowerBound, theUpperBound); 431 return this; 432 } 433 434 public Date getUpperBoundAsInstant() { 435 if (myUpperBound == null || myUpperBound.getValue() == null) { 436 return null; 437 } 438 439 return getUpperBoundAsInstant(myUpperBound); 440 } 441 442 @Nonnull 443 private static Date getUpperBoundAsInstant(@Nonnull DateParam theUpperBound) { 444 Date retVal = theUpperBound.getValue(); 445 446 if (theUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) { 447 retVal = DateUtils.getHighestInstantFromDate(retVal); 448 } 449 450 ParamPrefixEnum prefix = getPrefixOrDefault(theUpperBound); 451 452 switch (prefix) { 453 case LESSTHAN: 454 case ENDS_BEFORE: 455 retVal = new Date(retVal.getTime() - 1L); 456 break; 457 case EQUAL: 458 case NOT_EQUAL: 459 case LESSTHAN_OR_EQUALS: 460 retVal = theUpperBound.getPrecision().add(retVal, 1); 461 retVal = new Date(retVal.getTime() - 1L); 462 break; 463 case GREATERTHAN_OR_EQUALS: 464 case GREATERTHAN: 465 case APPROXIMATE: 466 case STARTS_AFTER: 467 throw new IllegalStateException(Msg.code(1929) + "Invalid upper bound comparator: " + prefix); 468 } 469 470 return retVal; 471 } 472 473 @Override 474 public List<DateParam> getValuesAsQueryTokens() { 475 ArrayList<DateParam> retVal = new ArrayList<>(); 476 if (myLowerBound != null && myLowerBound.getMissing() != null) { 477 retVal.add((myLowerBound)); 478 } else { 479 boolean hasLowerBound = myLowerBound != null && !myLowerBound.isEmpty(); 480 boolean hasUpperBound = myUpperBound != null && !myUpperBound.isEmpty(); 481 482 if (hasLowerBound) { 483 retVal.add((myLowerBound)); 484 } 485 if (hasUpperBound) { 486 retVal.add((myUpperBound)); 487 } 488 } 489 return retVal; 490 } 491 492 private boolean hasBound(DateParam bound) { 493 return bound != null && !bound.isEmpty(); 494 } 495 496 @Override 497 public int hashCode() { 498 return Objects.hash(myLowerBound, myUpperBound); 499 } 500 501 public boolean isEmpty() { 502 return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null); 503 } 504 505 /** 506 * Sets the range from a pair of dates, inclusive on both ends 507 * 508 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 509 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 510 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 511 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 512 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 513 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 514 */ 515 public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) { 516 DateParam lowerBound = theLowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null; 517 DateParam upperBound = theUpperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null; 518 validateAndSet(lowerBound, upperBound); 519 } 520 521 /** 522 * Sets the range from a pair of dates, inclusive on both ends 523 * 524 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 525 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 526 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 527 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 528 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 529 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 530 */ 531 public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) { 532 validateAndSet(theLowerBound, theUpperBound); 533 } 534 535 /** 536 * Sets the range from a pair of dates, inclusive on both ends. Note that if 537 * theLowerBound is after theUpperBound, thie method will automatically reverse 538 * the order of the arguments in order to create an inclusive range. 539 * 540 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 541 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 542 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 543 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 544 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 545 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 546 */ 547 public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) { 548 IPrimitiveType<Date> lowerBound = theLowerBound; 549 IPrimitiveType<Date> upperBound = theUpperBound; 550 if (lowerBound != null 551 && lowerBound.getValue() != null 552 && upperBound != null 553 && upperBound.getValue() != null) { 554 if (lowerBound.getValue().after(upperBound.getValue())) { 555 IPrimitiveType<Date> temp = lowerBound; 556 lowerBound = upperBound; 557 upperBound = temp; 558 } 559 } 560 validateAndSet( 561 lowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, lowerBound) : null, 562 upperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, upperBound) : null); 563 } 564 565 /** 566 * Sets the range from a pair of dates, inclusive on both ends 567 * 568 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g. 569 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 570 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 571 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g. 572 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 573 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 574 */ 575 public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) { 576 DateParam lowerBound = theLowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null; 577 DateParam upperBound = theUpperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null; 578 if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) { 579 lowerBound.setPrefix(EQUAL); 580 upperBound.setPrefix(EQUAL); 581 } 582 validateAndSet(lowerBound, upperBound); 583 } 584 585 @Override 586 public void setValuesAsQueryTokens( 587 FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters) 588 throws InvalidRequestException { 589 590 // When we create and populate a DateRangeParam from a query parameter (?birthdate=2024-12-02 or 591 // ?birthdate=eq2024-12-02), we 592 // set the prefix only if it was specifically provided by the client as it is mandatory to retain the capability 593 // to make the differentiation. See {@link SearchBuilder#validateParamValuesAreValidForComboParam}. 594 // 595 // Since the FHIR specification says that "If no prefix is present, the prefix <code>eq</code> is assumed", 596 // we will do so by invoking method {@link DateRangeParam#getPrefixOrDefault} everytime computation is 597 // conditional on the 598 // prefix value. 599 600 boolean haveHadUnqualifiedParameter = false; 601 for (QualifiedParamList paramList : theParameters) { 602 if (paramList.size() == 0) { 603 continue; 604 } 605 if (paramList.size() > 1) { 606 throw new InvalidRequestException(Msg.code(1930) + "DateRange parameter does not support OR queries"); 607 } 608 String param = paramList.get(0); 609 610 /* 611 * Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not 612 * escaped theirs 613 */ 614 param = param.replace(' ', '+'); 615 616 DateParam parsed = new DateParam(); 617 parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param); 618 addParam(parsed); 619 620 if (parsed.getPrefix() == null) { 621 if (haveHadUnqualifiedParameter) { 622 throw new InvalidRequestException( 623 Msg.code(1931) 624 + "Multiple date parameters with the same name and no qualifier (>, <, etc.) is not supported"); 625 } 626 haveHadUnqualifiedParameter = true; 627 } 628 } 629 } 630 631 @Override 632 public String toString() { 633 StringBuilder b = new StringBuilder(); 634 b.append(getClass().getSimpleName()); 635 b.append("["); 636 if (hasBound(myLowerBound)) { 637 if (myLowerBound.getPrefix() != null) { 638 b.append(myLowerBound.getPrefix().getValue()); 639 } 640 b.append(myLowerBound.getValueAsString()); 641 } 642 if (hasBound(myUpperBound)) { 643 if (hasBound(myLowerBound)) { 644 b.append(" "); 645 } 646 if (myUpperBound.getPrefix() != null) { 647 b.append(myUpperBound.getPrefix().getValue()); 648 } 649 b.append(myUpperBound.getValueAsString()); 650 } else { 651 if (!hasBound(myLowerBound)) { 652 b.append("empty"); 653 } 654 } 655 b.append("]"); 656 return b.toString(); 657 } 658 659 /** 660 * Note: An operation can take a DateRangeParam. If only a single date is provided, 661 * it will still result in a DateRangeParam where the lower and upper bounds 662 * are the same value. As such, even though the prefixes for the lower and 663 * upper bounds default to <code>ge</code> and <code>le</code> respectively, 664 * the resulting prefix is effectively <code>eq</code> where only a single 665 * date is provided - as required by the FHIR specification (i.e. "If no 666 * prefix is present, the prefix <code>eq</code> is assumed"). 667 */ 668 private void validateAndSet(DateParam lowerBound, DateParam upperBound) { 669 if (hasBound(lowerBound) && hasBound(upperBound)) { 670 Date lowerBoundAsInstant = getLowerBoundAsInstant(lowerBound); 671 Date upperBoundAsInstant = getUpperBoundAsInstant(upperBound); 672 if (lowerBoundAsInstant.after(upperBoundAsInstant)) { 673 throw new DataFormatException(Msg.code(1932) 674 + format( 675 "Lower bound of %s is after upper bound of %s", 676 lowerBound.getValueAsString(), upperBound.getValueAsString())); 677 } 678 } 679 680 if (hasBound(lowerBound)) { 681 if (lowerBound.getPrefix() == null) { 682 lowerBound.setPrefix(GREATERTHAN_OR_EQUALS); 683 } 684 switch (lowerBound.getPrefix()) { 685 case GREATERTHAN: 686 case GREATERTHAN_OR_EQUALS: 687 default: 688 break; 689 case LESSTHAN: 690 case LESSTHAN_OR_EQUALS: 691 throw new DataFormatException(Msg.code(1933) + "Lower bound comparator must be > or >=, can not be " 692 + lowerBound.getPrefix().getValue()); 693 } 694 } 695 696 if (hasBound(upperBound)) { 697 if (upperBound.getPrefix() == null) { 698 upperBound.setPrefix(LESSTHAN_OR_EQUALS); 699 } 700 switch (upperBound.getPrefix()) { 701 case LESSTHAN: 702 case LESSTHAN_OR_EQUALS: 703 default: 704 break; 705 case GREATERTHAN: 706 case GREATERTHAN_OR_EQUALS: 707 throw new DataFormatException(Msg.code(1934) + "Upper bound comparator must be < or <=, can not be " 708 + upperBound.getPrefix().getValue()); 709 } 710 } 711 712 myLowerBound = lowerBound; 713 myUpperBound = upperBound; 714 } 715 716 /** 717 * 718 * This method should be used when performing computation conditional on the prefix value to ensure that a dateParam 719 * without prefix is treated as if it has one set to 'eq'. 720 */ 721 private static ParamPrefixEnum getPrefixOrDefault(DateParam theDateParam) { 722 return ObjectUtils.defaultIfNull(theDateParam.getPrefix(), EQUAL); 723 } 724}