View Javadoc
1   package ca.uhn.fhir.rest.param;
2   
3   import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL;
4   import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
5   import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
6   import static java.lang.String.format;
7   import static org.apache.commons.lang3.StringUtils.isNotBlank;
8   
9   /*
10   * #%L
11   * HAPI FHIR - Core Library
12   * %%
13   * Copyright (C) 2014 - 2018 University Health Network
14   * %%
15   * Licensed under the Apache License, Version 2.0 (the "License");
16   * you may not use this file except in compliance with the License.
17   * You may obtain a copy of the License at
18   * 
19   *      http://www.apache.org/licenses/LICENSE-2.0
20   * 
21   * Unless required by applicable law or agreed to in writing, software
22   * distributed under the License is distributed on an "AS IS" BASIS,
23   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24   * See the License for the specific language governing permissions and
25   * limitations under the License.
26   * #L%
27   */
28  import java.util.*;
29  
30  import org.hl7.fhir.instance.model.api.IPrimitiveType;
31  
32  import ca.uhn.fhir.context.FhirContext;
33  import ca.uhn.fhir.model.api.IQueryParameterAnd;
34  import ca.uhn.fhir.parser.DataFormatException;
35  import ca.uhn.fhir.rest.api.QualifiedParamList;
36  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
37  
38  public class DateRangeParam implements IQueryParameterAnd<DateParam> {
39  
40  	private static final long serialVersionUID = 1L;
41  	
42  	private DateParam myLowerBound;
43  	private DateParam myUpperBound;
44  
45  	/**
46  	 * Basic constructor. Values must be supplied by calling {@link #setLowerBound(DateParam)} and
47  	 * {@link #setUpperBound(DateParam)}
48  	 */
49  	public DateRangeParam() {
50  		super();
51  	}
52  
53  	/**
54  	 * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
55  	 * 
56  	 * @param theLowerBound
57  	 *           A qualified date param representing the lower date bound (optionally may include time), e.g.
58  	 *           "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
59  	 *           theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
60  	 * @param theUpperBound
61  	 *           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
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 }