View Javadoc
1   package ca.uhn.fhir.rest.param;
2   
3   import ca.uhn.fhir.context.FhirContext;
4   import ca.uhn.fhir.model.api.IQueryParameterAnd;
5   import ca.uhn.fhir.parser.DataFormatException;
6   import ca.uhn.fhir.rest.api.QualifiedParamList;
7   import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
8   import org.hl7.fhir.instance.model.api.IPrimitiveType;
9   
10  import java.util.ArrayList;
11  import java.util.Date;
12  import java.util.List;
13  import java.util.Objects;
14  
15  import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*;
16  import static java.lang.String.format;
17  import static org.apache.commons.lang3.StringUtils.isNotBlank;
18  
19  /*
20   * #%L
21   * HAPI FHIR - Core Library
22   * %%
23   * Copyright (C) 2014 - 2018 University Health Network
24   * %%
25   * Licensed under the Apache License, Version 2.0 (the "License");
26   * you may not use this file except in compliance with the License.
27   * You may obtain a copy of the License at
28   * 
29   *      http://www.apache.org/licenses/LICENSE-2.0
30   * 
31   * Unless required by applicable law or agreed to in writing, software
32   * distributed under the License is distributed on an "AS IS" BASIS,
33   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34   * See the License for the specific language governing permissions and
35   * limitations under the License.
36   * #L%
37   */
38  
39  @SuppressWarnings("UnusedReturnValue")
40  public class DateRangeParam implements IQueryParameterAnd<DateParam> {
41  
42  	private static final long serialVersionUID = 1L;
43  
44  	private DateParam myLowerBound;
45  	private DateParam myUpperBound;
46  
47  	/**
48  	 * Basic constructor. Values must be supplied by calling {@link #setLowerBound(DateParam)} and
49  	 * {@link #setUpperBound(DateParam)}
50  	 */
51  	public DateRangeParam() {
52  		super();
53  	}
54  
55  	/**
56  	 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
57  	 *
58  	 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
59  	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
60  	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
61  	 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
62  	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
63  	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
64  	 */
65  	public DateRangeParam(Date theLowerBound, Date theUpperBound) {
66  		this();
67  		setRangeFromDatesInclusive(theLowerBound, theUpperBound);
68  	}
69  
70  	/**
71  	 * Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound
72  	 * (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the lower
73  	 * or upper bound, with no opposite bound.
74  	 */
75  	public DateRangeParam(DateParam theDateParam) {
76  		this();
77  		if (theDateParam == null) {
78  			throw new NullPointerException("theDateParam can not be null");
79  		}
80  		if (theDateParam.isEmpty()) {
81  			throw new IllegalArgumentException("theDateParam can not be empty");
82  		}
83  		if (theDateParam.getPrefix() == null) {
84  			setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
85  		} else {
86  			switch (theDateParam.getPrefix()) {
87  				case EQUAL:
88  					setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
89  					break;
90  				case STARTS_AFTER:
91  				case GREATERTHAN:
92  				case GREATERTHAN_OR_EQUALS:
93  					validateAndSet(theDateParam, null);
94  					break;
95  				case ENDS_BEFORE:
96  				case LESSTHAN:
97  				case LESSTHAN_OR_EQUALS:
98  					validateAndSet(null, theDateParam);
99  					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 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 A qualified date param representing the upper 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 	 */
117 	public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) {
118 		this();
119 		setRangeFromDatesInclusive(theLowerBound, theUpperBound);
120 	}
121 
122 	/**
123 	 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
124 	 *
125 	 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
126 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
127 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
128 	 * @param theUpperBound A qualified date param representing the upper 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 	 */
132 	public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
133 		this();
134 		setRangeFromDatesInclusive(theLowerBound, theUpperBound);
135 	}
136 
137 	/**
138 	 * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
139 	 *
140 	 * @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g.
141 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
142 	 *                      one may be null, but it is not valid for both to be null.
143 	 * @param theUpperBound An unqualified date param representing the upper date bound (optionally may include time), e.g.
144 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
145 	 *                      one may be null, but it is not valid for both to be null.
146 	 */
147 	public DateRangeParam(String theLowerBound, String theUpperBound) {
148 		this();
149 		setRangeFromDatesInclusive(theLowerBound, theUpperBound);
150 	}
151 
152 	private void addParam(DateParam theParsed) throws InvalidRequestException {
153 		if (theParsed.getPrefix() == null || theParsed.getPrefix() == EQUAL) {
154 			if (myLowerBound != null || myUpperBound != null) {
155 				throw new InvalidRequestException("Can not have multiple date range parameters for the same param without a qualifier");
156 			}
157 
158 			if (theParsed.getMissing() != null) {
159 				myLowerBound = theParsed;
160 				myUpperBound = theParsed;
161 			} else {
162 				myLowerBound = new DateParam(EQUAL, theParsed.getValueAsString());
163 				myUpperBound = new DateParam(EQUAL, theParsed.getValueAsString());
164 			}
165 
166 		} else {
167 
168 			switch (theParsed.getPrefix()) {
169 				case GREATERTHAN:
170 				case GREATERTHAN_OR_EQUALS:
171 					if (myLowerBound != null) {
172 						throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
173 					}
174 					myLowerBound = theParsed;
175 					break;
176 				case LESSTHAN:
177 				case LESSTHAN_OR_EQUALS:
178 					if (myUpperBound != null) {
179 						throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
180 					}
181 					myUpperBound = theParsed;
182 					break;
183 				default:
184 					throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix());
185 			}
186 
187 		}
188 	}
189 
190 	@Override
191 	public boolean equals(Object obj) {
192 		if (obj == this) {
193 			return true;
194 		}
195 		if (!(obj instanceof DateRangeParam)) {
196 			return false;
197 		}
198 		DateRangeParam other = (DateRangeParam) obj;
199 		return Objects.equals(myLowerBound, other.myLowerBound) &&
200 			Objects.equals(myUpperBound, other.myUpperBound);
201 	}
202 
203 	public DateParam getLowerBound() {
204 		return myLowerBound;
205 	}
206 
207 	public DateRangeParam setLowerBound(DateParam theLowerBound) {
208 		validateAndSet(theLowerBound, myUpperBound);
209 		return this;
210 	}
211 
212 	/**
213 	 * Sets the lower bound using a string that is compliant with
214 	 * FHIR dateTime format (ISO-8601).
215 	 * <p>
216 	 * This lower bound is assumed to have a <code>ge</code>
217 	 * (greater than or equals) modifier.
218 	 * </p>
219 	 */
220 	public DateRangeParam setLowerBound(String theLowerBound) {
221 		setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound));
222 		return this;
223 	}
224 
225 	/**
226 	 * Sets the lower bound to be greaterthan or equal to the given date
227 	 */
228 	public DateRangeParam setLowerBoundInclusive(Date theLowerBound) {
229 		validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, theLowerBound), myUpperBound);
230 		return this;
231 	}
232 
233 	/**
234 	 * Sets the upper bound to be greaterthan or equal to the given date
235 	 */
236 	public DateRangeParam setUpperBoundInclusive(Date theUpperBound) {
237 		validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, theUpperBound));
238 		return this;
239 	}
240 
241 
242 	/**
243 	 * Sets the lower bound to be greaterthan to the given date
244 	 */
245 	public DateRangeParam setLowerBoundExclusive(Date theLowerBound) {
246 		validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN, theLowerBound), myUpperBound);
247 		return this;
248 	}
249 
250 	/**
251 	 * Sets the upper bound to be greaterthan to the given date
252 	 */
253 	public DateRangeParam setUpperBoundExclusive(Date theUpperBound) {
254 		validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN, theUpperBound));
255 		return this;
256 	}
257 
258 	public Date getLowerBoundAsInstant() {
259 		if (myLowerBound == null) {
260 			return null;
261 		}
262 		Date retVal = myLowerBound.getValue();
263 		if (myLowerBound.getPrefix() != null) {
264 			switch (myLowerBound.getPrefix()) {
265 				case GREATERTHAN:
266 				case STARTS_AFTER:
267 					retVal = myLowerBound.getPrecision().add(retVal, 1);
268 					break;
269 				case EQUAL:
270 				case GREATERTHAN_OR_EQUALS:
271 					break;
272 				case LESSTHAN:
273 				case APPROXIMATE:
274 				case LESSTHAN_OR_EQUALS:
275 				case ENDS_BEFORE:
276 				case NOT_EQUAL:
277 					throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix());
278 			}
279 		}
280 		return retVal;
281 	}
282 
283 	public DateParam getUpperBound() {
284 		return myUpperBound;
285 	}
286 
287 	/**
288 	 * Sets the upper bound using a string that is compliant with
289 	 * FHIR dateTime format (ISO-8601).
290 	 * <p>
291 	 * This upper bound is assumed to have a <code>le</code>
292 	 * (less than or equals) modifier.
293 	 * </p>
294 	 */
295 	public DateRangeParam setUpperBound(String theUpperBound) {
296 		setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound));
297 		return this;
298 	}
299 
300 	public DateRangeParam setUpperBound(DateParam theUpperBound) {
301 		validateAndSet(myLowerBound, theUpperBound);
302 		return this;
303 	}
304 
305 	public Date getUpperBoundAsInstant() {
306 		if (myUpperBound == null) {
307 			return null;
308 		}
309 		Date retVal = myUpperBound.getValue();
310 		if (myUpperBound.getPrefix() != null) {
311 			switch (myUpperBound.getPrefix()) {
312 				case LESSTHAN:
313 				case ENDS_BEFORE:
314 					retVal = new Date(retVal.getTime() - 1L);
315 					break;
316 				case EQUAL:
317 				case LESSTHAN_OR_EQUALS:
318 					retVal = myUpperBound.getPrecision().add(retVal, 1);
319 					retVal = new Date(retVal.getTime() - 1L);
320 					break;
321 				case GREATERTHAN_OR_EQUALS:
322 				case GREATERTHAN:
323 				case APPROXIMATE:
324 				case NOT_EQUAL:
325 				case STARTS_AFTER:
326 					throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix());
327 			}
328 		}
329 		return retVal;
330 	}
331 
332 	@Override
333 	public List<DateParam> getValuesAsQueryTokens() {
334 		ArrayList<DateParam> retVal = new ArrayList<>();
335 		if (myLowerBound != null && myLowerBound.getMissing() != null) {
336 			retVal.add((myLowerBound));
337 		} else {
338 			if (myLowerBound != null && !myLowerBound.isEmpty()) {
339 				retVal.add((myLowerBound));
340 			}
341 			if (myUpperBound != null && !myUpperBound.isEmpty()) {
342 				retVal.add((myUpperBound));
343 			}
344 		}
345 		return retVal;
346 	}
347 
348 	private boolean hasBound(DateParam bound) {
349 		return bound != null && !bound.isEmpty();
350 	}
351 
352 	@Override
353 	public int hashCode() {
354 		return Objects.hash(myLowerBound, myUpperBound);
355 	}
356 
357 	public boolean isEmpty() {
358 		return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
359 	}
360 
361 	/**
362 	 * Sets the range from a pair of dates, inclusive on both ends
363 	 *
364 	 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
365 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
366 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
367 	 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
368 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
369 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
370 	 */
371 	public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
372 		DateParam lowerBound = theLowerBound != null
373 			? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
374 		DateParam upperBound = theUpperBound != null
375 			? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
376 		validateAndSet(lowerBound, upperBound);
377 	}
378 
379 	/**
380 	 * Sets the range from a pair of dates, inclusive on both ends
381 	 *
382 	 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
383 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
384 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
385 	 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
386 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
387 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
388 	 */
389 	public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) {
390 		validateAndSet(theLowerBound, theUpperBound);
391 	}
392 
393 	/**
394 	 * Sets the range from a pair of dates, inclusive on both ends. Note that if
395 	 * theLowerBound is after theUpperBound, thie method will automatically reverse
396 	 * the order of the arguments in order to create an inclusive range.
397 	 *
398 	 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
399 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
400 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
401 	 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
402 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
403 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
404 	 */
405 	public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
406 		IPrimitiveType<Date> lowerBound = theLowerBound;
407 		IPrimitiveType<Date> upperBound = theUpperBound;
408 		if (lowerBound != null && lowerBound.getValue() != null && upperBound != null && upperBound.getValue() != null) {
409 			if (lowerBound.getValue().after(upperBound.getValue())) {
410 				IPrimitiveType<Date> temp = lowerBound;
411 				lowerBound = upperBound;
412 				upperBound = temp;
413 			}
414 		}
415 		validateAndSet(
416 			lowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, lowerBound) : null,
417 			upperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, upperBound) : null);
418 	}
419 
420 	/**
421 	 * Sets the range from a pair of dates, inclusive on both ends
422 	 *
423 	 * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
424 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
425 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
426 	 * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
427 	 *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
428 	 *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
429 	 */
430 	public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
431 		DateParam lowerBound = theLowerBound != null
432 			? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)
433 			: null;
434 		DateParam upperBound = theUpperBound != null
435 			? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)
436 			: null;
437 		if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) {
438 			lowerBound.setPrefix(EQUAL);
439 			upperBound.setPrefix(EQUAL);
440 		}
441 		validateAndSet(lowerBound, upperBound);
442 	}
443 
444 	@Override
445 	public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters)
446 		throws InvalidRequestException {
447 
448 		boolean haveHadUnqualifiedParameter = false;
449 		for (QualifiedParamList paramList : theParameters) {
450 			if (paramList.size() == 0) {
451 				continue;
452 			}
453 			if (paramList.size() > 1) {
454 				throw new InvalidRequestException("DateRange parameter does not suppport OR queries");
455 			}
456 			String param = paramList.get(0);
457 
458 			/*
459 			 * Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not
460 			 * escaped theirs
461 			 */
462 			param = param.replace(' ', '+');
463 
464 			DateParam parsed = new DateParam();
465 			parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param);
466 			addParam(parsed);
467 
468 			if (parsed.getPrefix() == null) {
469 				if (haveHadUnqualifiedParameter) {
470 					throw new InvalidRequestException("Multiple date parameters with the same name and no qualifier (>, <, etc.) is not supported");
471 				}
472 				haveHadUnqualifiedParameter = true;
473 			}
474 
475 		}
476 
477 	}
478 
479 	@Override
480 	public String toString() {
481 		StringBuilder b = new StringBuilder();
482 		b.append(getClass().getSimpleName());
483 		b.append("[");
484 		if (hasBound(myLowerBound)) {
485 			if (myLowerBound.getPrefix() != null) {
486 				b.append(myLowerBound.getPrefix().getValue());
487 			}
488 			b.append(myLowerBound.getValueAsString());
489 		}
490 		if (hasBound(myUpperBound)) {
491 			if (hasBound(myLowerBound)) {
492 				b.append(" ");
493 			}
494 			if (myUpperBound.getPrefix() != null) {
495 				b.append(myUpperBound.getPrefix().getValue());
496 			}
497 			b.append(myUpperBound.getValueAsString());
498 		} else {
499 			if (!hasBound(myLowerBound)) {
500 				b.append("empty");
501 			}
502 		}
503 		b.append("]");
504 		return b.toString();
505 	}
506 
507 	private void validateAndSet(DateParam lowerBound, DateParam upperBound) {
508 		if (hasBound(lowerBound) && hasBound(upperBound)) {
509 			if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) {
510 				throw new DataFormatException(format(
511 					"Lower bound of %s is after upper bound of %s",
512 					lowerBound.getValueAsString(), upperBound.getValueAsString()));
513 			}
514 		}
515 
516 		if (hasBound(lowerBound)) {
517 			if (lowerBound.getPrefix() == null) {
518 				lowerBound.setPrefix(GREATERTHAN_OR_EQUALS);
519 			}
520 			switch (lowerBound.getPrefix()) {
521 				case GREATERTHAN:
522 				case GREATERTHAN_OR_EQUALS:
523 				default:
524 					break;
525 				case LESSTHAN:
526 				case LESSTHAN_OR_EQUALS:
527 					throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue());
528 			}
529 		}
530 
531 		if (hasBound(upperBound)) {
532 			if (upperBound.getPrefix() == null) {
533 				upperBound.setPrefix(LESSTHAN_OR_EQUALS);
534 			}
535 			switch (upperBound.getPrefix()) {
536 				case LESSTHAN:
537 				case LESSTHAN_OR_EQUALS:
538 				default:
539 					break;
540 				case GREATERTHAN:
541 				case GREATERTHAN_OR_EQUALS:
542 					throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue());
543 			}
544 		}
545 
546 		myLowerBound = lowerBound;
547 		myUpperBound = upperBound;
548 	}
549 
550 }