001package ca.uhn.fhir.rest.param; 002 003import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL; 004import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; 005import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; 006import static java.lang.String.format; 007import static org.apache.commons.lang3.StringUtils.isNotBlank; 008 009/* 010 * #%L 011 * HAPI FHIR - Core Library 012 * %% 013 * Copyright (C) 2014 - 2018 University Health Network 014 * %% 015 * Licensed under the Apache License, Version 2.0 (the "License"); 016 * you may not use this file except in compliance with the License. 017 * You may obtain a copy of the License at 018 * 019 * http://www.apache.org/licenses/LICENSE-2.0 020 * 021 * Unless required by applicable law or agreed to in writing, software 022 * distributed under the License is distributed on an "AS IS" BASIS, 023 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 024 * See the License for the specific language governing permissions and 025 * limitations under the License. 026 * #L% 027 */ 028import java.util.*; 029 030import org.hl7.fhir.instance.model.api.IPrimitiveType; 031 032import ca.uhn.fhir.context.FhirContext; 033import ca.uhn.fhir.model.api.IQueryParameterAnd; 034import ca.uhn.fhir.parser.DataFormatException; 035import ca.uhn.fhir.rest.api.QualifiedParamList; 036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 037 038public class DateRangeParam implements IQueryParameterAnd<DateParam> { 039 040 private static final long serialVersionUID = 1L; 041 042 private DateParam myLowerBound; 043 private DateParam myUpperBound; 044 045 /** 046 * Basic constructor. Values must be supplied by calling {@link #setLowerBound(DateParam)} and 047 * {@link #setUpperBound(DateParam)} 048 */ 049 public DateRangeParam() { 050 super(); 051 } 052 053 /** 054 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) 055 * 056 * @param theLowerBound 057 * A qualified date param representing the lower date bound (optionally may include time), e.g. 058 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 059 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 060 * @param theUpperBound 061 * A qualified date param representing the upper date bound (optionally may include time), e.g. 062 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 063 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 064 */ 065 public DateRangeParam(Date theLowerBound, Date theUpperBound) { 066 this(); 067 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 068 } 069 070 /** 071 * Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound 072 * (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the lower 073 * or upper bound, with no opposite bound. 074 */ 075 public DateRangeParam(DateParam theDateParam) { 076 this(); 077 if (theDateParam == null) { 078 throw new NullPointerException("theDateParam can not be null"); 079 } 080 if (theDateParam.isEmpty()) { 081 throw new IllegalArgumentException("theDateParam can not be empty"); 082 } 083 if (theDateParam.getPrefix() == null) { 084 setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString()); 085 } else { 086 switch (theDateParam.getPrefix()) { 087 case EQUAL: 088 setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString()); 089 break; 090 case STARTS_AFTER: 091 case GREATERTHAN: 092 case GREATERTHAN_OR_EQUALS: 093 validateAndSet(theDateParam, null); 094 break; 095 case ENDS_BEFORE: 096 case LESSTHAN: 097 case LESSTHAN_OR_EQUALS: 098 validateAndSet(null, theDateParam); 099 break; 100 default: 101 // Should not happen 102 throw new InvalidRequestException("Invalid comparator for date range parameter:" + theDateParam.getPrefix() + ". This is a bug."); 103 } 104 } 105 } 106 107 /** 108 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) 109 * 110 * @param theLowerBound 111 * A qualified date param representing the lower date bound (optionally may include time), e.g. 112 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 113 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 114 * @param theUpperBound 115 * A qualified date param representing the upper date bound (optionally may include time), e.g. 116 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 117 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 118 */ 119 public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) { 120 this(); 121 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 122 } 123 124 /** 125 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) 126 * 127 * @param theLowerBound 128 * A qualified date param representing the lower date bound (optionally may include time), e.g. 129 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 130 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 131 * @param theUpperBound 132 * A qualified date param representing the upper date bound (optionally may include time), e.g. 133 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 134 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 135 */ 136 public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) { 137 this(); 138 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 139 } 140 141 /** 142 * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends) 143 * 144 * @param theLowerBound 145 * An unqualified date param representing the lower date bound (optionally may include time), e.g. 146 * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or 147 * one may be null, but it is not valid for both to be null. 148 * @param theUpperBound 149 * An unqualified date param representing the upper date bound (optionally may include time), e.g. 150 * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or 151 * one may be null, but it is not valid for both to be null. 152 */ 153 public DateRangeParam(String theLowerBound, String theUpperBound) { 154 this(); 155 setRangeFromDatesInclusive(theLowerBound, theUpperBound); 156 } 157 158 private void addParam(DateParam theParsed) throws InvalidRequestException { 159 if (theParsed.getPrefix() == null || theParsed.getPrefix() == EQUAL) { 160 if (myLowerBound != null || myUpperBound != null) { 161 throw new InvalidRequestException("Can not have multiple date range parameters for the same param without a qualifier"); 162 } 163 164 if (theParsed.getMissing() != null) { 165 myLowerBound = theParsed; 166 myUpperBound = theParsed; 167 } else { 168 myLowerBound = new DateParam(EQUAL, theParsed.getValueAsString()); 169 myUpperBound = new DateParam(EQUAL, theParsed.getValueAsString()); 170 } 171 172 } else { 173 174 switch (theParsed.getPrefix()) { 175 case GREATERTHAN: 176 case GREATERTHAN_OR_EQUALS: 177 if (myLowerBound != null) { 178 throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound"); 179 } 180 myLowerBound = theParsed; 181 break; 182 case LESSTHAN: 183 case LESSTHAN_OR_EQUALS: 184 if (myUpperBound != null) { 185 throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound"); 186 } 187 myUpperBound = theParsed; 188 break; 189 default: 190 throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix()); 191 } 192 193 } 194 } 195 196 public DateParam getLowerBound() { 197 return myLowerBound; 198 } 199 200 public Date getLowerBoundAsInstant() { 201 if (myLowerBound == null) { 202 return null; 203 } 204 Date retVal = myLowerBound.getValue(); 205 if (myLowerBound.getPrefix() != null) { 206 switch (myLowerBound.getPrefix()) { 207 case GREATERTHAN: 208 case STARTS_AFTER: 209 retVal = myLowerBound.getPrecision().add(retVal, 1); 210 break; 211 case EQUAL: 212 case GREATERTHAN_OR_EQUALS: 213 break; 214 case LESSTHAN: 215 case APPROXIMATE: 216 case LESSTHAN_OR_EQUALS: 217 case ENDS_BEFORE: 218 case NOT_EQUAL: 219 throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix()); 220 } 221 } 222 return retVal; 223 } 224 225 public DateParam getUpperBound() { 226 return myUpperBound; 227 } 228 229 public Date getUpperBoundAsInstant() { 230 if (myUpperBound == null) { 231 return null; 232 } 233 Date retVal = myUpperBound.getValue(); 234 if (myUpperBound.getPrefix() != null) { 235 switch (myUpperBound.getPrefix()) { 236 case LESSTHAN: 237 case ENDS_BEFORE: 238 retVal = new Date(retVal.getTime() - 1L); 239 break; 240 case EQUAL: 241 case LESSTHAN_OR_EQUALS: 242 retVal = myUpperBound.getPrecision().add(retVal, 1); 243 retVal = new Date(retVal.getTime() - 1L); 244 break; 245 case GREATERTHAN_OR_EQUALS: 246 case GREATERTHAN: 247 case APPROXIMATE: 248 case NOT_EQUAL: 249 case STARTS_AFTER: 250 throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix()); 251 } 252 } 253 return retVal; 254 } 255 256 @Override 257 public List<DateParam> getValuesAsQueryTokens() { 258 ArrayList<DateParam> retVal = new ArrayList<>(); 259 if (myLowerBound != null && myLowerBound.getMissing() != null) { 260 retVal.add((myLowerBound)); 261 } else { 262 if (myLowerBound != null && !myLowerBound.isEmpty()) { 263 retVal.add((myLowerBound)); 264 } 265 if (myUpperBound != null && !myUpperBound.isEmpty()) { 266 retVal.add((myUpperBound)); 267 } 268 } 269 return retVal; 270 } 271 272 private boolean hasBound(DateParam bound) { 273 return bound != null && !bound.isEmpty(); 274 } 275 276 public boolean isEmpty() { 277 return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null); 278 } 279 280 public DateRangeParam setLowerBound(DateParam theLowerBound) { 281 validateAndSet(theLowerBound, myUpperBound); 282 return this; 283 } 284 285 /** 286 * Sets the range from a pair of dates, inclusive on both ends 287 * 288 * @param theLowerBound 289 * A qualified date param representing the lower date bound (optionally may include time), e.g. 290 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 291 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 292 * @param theUpperBound 293 * A qualified date param representing the upper date bound (optionally may include time), e.g. 294 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 295 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 296 */ 297 public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) { 298 DateParam lowerBound = theLowerBound != null 299 ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null; 300 DateParam upperBound = theUpperBound != null 301 ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null; 302 validateAndSet(lowerBound, upperBound); 303 } 304 305 /** 306 * Sets the range from a pair of dates, inclusive on both ends 307 * 308 * @param theLowerBound 309 * A qualified date param representing the lower date bound (optionally may include time), e.g. 310 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 311 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 312 * @param theUpperBound 313 * A qualified date param representing the upper date bound (optionally may include time), e.g. 314 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 315 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 316 */ 317 public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) { 318 validateAndSet(theLowerBound, theUpperBound); 319 } 320 321 /** 322 * Sets the range from a pair of dates, inclusive on both ends. Note that if 323 * theLowerBound is after theUpperBound, thie method will automatically reverse 324 * the order of the arguments in order to create an inclusive range. 325 * 326 * @param theLowerBound 327 * A qualified date param representing the lower date bound (optionally may include time), e.g. 328 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 329 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 330 * @param theUpperBound 331 * A qualified date param representing the upper date bound (optionally may include time), e.g. 332 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 333 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 334 */ 335 public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) { 336 IPrimitiveType<Date> lowerBound = theLowerBound; 337 IPrimitiveType<Date> upperBound = theUpperBound; 338 if (lowerBound != null && lowerBound.getValue() != null && upperBound != null && upperBound.getValue() != null) { 339 if (lowerBound.getValue().after(upperBound.getValue())) { 340 IPrimitiveType<Date> temp = lowerBound; 341 lowerBound = upperBound; 342 upperBound = temp; 343 } 344 } 345 validateAndSet( 346 lowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, lowerBound) : null, 347 upperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, upperBound) : null); 348 } 349 350 /** 351 * Sets the range from a pair of dates, inclusive on both ends 352 * 353 * @param theLowerBound 354 * A qualified date param representing the lower date bound (optionally may include time), e.g. 355 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 356 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 357 * @param theUpperBound 358 * A qualified date param representing the upper date bound (optionally may include time), e.g. 359 * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or 360 * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. 361 */ 362 public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) { 363 DateParam lowerBound = theLowerBound != null 364 ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) 365 : null; 366 DateParam upperBound = theUpperBound != null 367 ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) 368 : null; 369 if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) { 370 lowerBound.setPrefix(EQUAL); 371 upperBound.setPrefix(EQUAL); 372 } 373 validateAndSet(lowerBound, upperBound); 374 } 375 376 public DateRangeParam setUpperBound(DateParam theUpperBound) { 377 validateAndSet(myLowerBound, theUpperBound); 378 return this; 379 } 380 381 @Override 382 public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters) 383 throws InvalidRequestException { 384 385 boolean haveHadUnqualifiedParameter = false; 386 for (QualifiedParamList paramList : theParameters) { 387 if (paramList.size() == 0) { 388 continue; 389 } 390 if (paramList.size() > 1) { 391 throw new InvalidRequestException("DateRange parameter does not suppport OR queries"); 392 } 393 String param = paramList.get(0); 394 395 /* 396 * Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not 397 * escaped theirs 398 */ 399 param = param.replace(' ', '+'); 400 401 DateParam parsed = new DateParam(); 402 parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param); 403 addParam(parsed); 404 405 if (parsed.getPrefix() == null) { 406 if (haveHadUnqualifiedParameter) { 407 throw new InvalidRequestException("Multiple date parameters with the same name and no qualifier (>, <, etc.) is not supported"); 408 } 409 haveHadUnqualifiedParameter = true; 410 } 411 412 } 413 414 } 415 416 @Override 417 public boolean equals(Object obj) { 418 if (obj == this) { 419 return true; 420 } 421 if (!(obj instanceof DateRangeParam)) { 422 return false; 423 } 424 DateRangeParam other = (DateRangeParam) obj; 425 return Objects.equals(myLowerBound, other.myLowerBound) && 426 Objects.equals(myUpperBound, other.myUpperBound); 427 } 428 429 @Override 430 public int hashCode() { 431 return Objects.hash(myLowerBound, myUpperBound); 432 } 433 434 @Override 435 public String toString() { 436 StringBuilder b = new StringBuilder(); 437 b.append(getClass().getSimpleName()); 438 b.append("["); 439 if (hasBound(myLowerBound)) { 440 if (myLowerBound.getPrefix() != null) { 441 b.append(myLowerBound.getPrefix().getValue()); 442 } 443 b.append(myLowerBound.getValueAsString()); 444 } 445 if (hasBound(myUpperBound)) { 446 if (hasBound(myLowerBound)) { 447 b.append(" "); 448 } 449 if (myUpperBound.getPrefix() != null) { 450 b.append(myUpperBound.getPrefix().getValue()); 451 } 452 b.append(myUpperBound.getValueAsString()); 453 } else { 454 if (!hasBound(myLowerBound)) { 455 b.append("empty"); 456 } 457 } 458 b.append("]"); 459 return b.toString(); 460 } 461 462 private void validateAndSet(DateParam lowerBound, DateParam upperBound) { 463 if (hasBound(lowerBound) && hasBound(upperBound)) { 464 if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) { 465 throw new DataFormatException(format( 466 "Lower bound of %s is after upper bound of %s", 467 lowerBound.getValueAsString(), upperBound.getValueAsString())); 468 } 469 } 470 471 if (hasBound(lowerBound)) { 472 if (lowerBound.getPrefix() == null) { 473 lowerBound.setPrefix(GREATERTHAN_OR_EQUALS); 474 } 475 switch (lowerBound.getPrefix()) { 476 case GREATERTHAN: 477 case GREATERTHAN_OR_EQUALS: 478 default: 479 break; 480 case LESSTHAN: 481 case LESSTHAN_OR_EQUALS: 482 throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue()); 483 } 484 } 485 486 if (hasBound(upperBound)) { 487 if (upperBound.getPrefix() == null) { 488 upperBound.setPrefix(LESSTHAN_OR_EQUALS); 489 } 490 switch (upperBound.getPrefix()) { 491 case LESSTHAN: 492 case LESSTHAN_OR_EQUALS: 493 default: 494 break; 495 case GREATERTHAN: 496 case GREATERTHAN_OR_EQUALS: 497 throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue()); 498 } 499 } 500 501 myLowerBound = lowerBound; 502 myUpperBound = upperBound; 503 } 504}