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