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