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