001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.param;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.model.api.IQueryParameterAnd;
025import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
026import ca.uhn.fhir.parser.DataFormatException;
027import ca.uhn.fhir.rest.api.QualifiedParamList;
028import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
029import ca.uhn.fhir.util.DateUtils;
030import jakarta.annotation.Nonnull;
031import org.apache.commons.lang3.Validate;
032import org.hl7.fhir.instance.model.api.IPrimitiveType;
033
034import java.util.ArrayList;
035import java.util.Date;
036import java.util.List;
037import java.util.Objects;
038
039import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL;
040import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
041import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
042import static java.lang.String.format;
043import static org.apache.commons.lang3.StringUtils.isNotBlank;
044
045@SuppressWarnings("UnusedReturnValue")
046public class DateRangeParam implements IQueryParameterAnd<DateParam> {
047
048        private static final long serialVersionUID = 1L;
049
050        private DateParam myLowerBound;
051        private DateParam myUpperBound;
052
053        /**
054         * Basic constructor. Values must be supplied by calling {@link #setLowerBound(DateParam)} and
055         * {@link #setUpperBound(DateParam)}
056         */
057        public DateRangeParam() {
058                super();
059        }
060
061        /**
062         * Copy constructor.
063         */
064        @SuppressWarnings("CopyConstructorMissesField")
065        public DateRangeParam(DateRangeParam theDateRangeParam) {
066                super();
067                Validate.notNull(theDateRangeParam);
068                setLowerBound(theDateRangeParam.getLowerBound());
069                setUpperBound(theDateRangeParam.getUpperBound());
070        }
071
072        /**
073         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
074         *
075         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
076         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
077         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
078         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
079         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
080         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
081         */
082        public DateRangeParam(Date theLowerBound, Date theUpperBound) {
083                this();
084                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
085        }
086
087        /**
088         * Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound
089         * (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the lower
090         * or upper bound, with no opposite bound.
091         */
092        public DateRangeParam(DateParam theDateParam) {
093                this();
094                if (theDateParam == null) {
095                        throw new NullPointerException(Msg.code(1919) + "theDateParam can not be null");
096                }
097                if (theDateParam.isEmpty()) {
098                        throw new IllegalArgumentException(Msg.code(1920) + "theDateParam can not be empty");
099                }
100                if (theDateParam.getPrefix() == null) {
101                        setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
102                } else {
103                        switch (theDateParam.getPrefix()) {
104                                case NOT_EQUAL:
105                                case EQUAL:
106                                        setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
107                                        break;
108                                case STARTS_AFTER:
109                                case GREATERTHAN:
110                                case GREATERTHAN_OR_EQUALS:
111                                        if (theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) {
112                                                theDateParam.setValueAsString(DateUtils.getCompletedDate(theDateParam.getValueAsString())
113                                                                .getRight());
114                                        }
115                                        // there is only one value; we will set it as the lower bound
116                                        // as a >= operation
117                                        validateAndSet(theDateParam, null);
118                                        break;
119                                case ENDS_BEFORE:
120                                case LESSTHAN:
121                                case LESSTHAN_OR_EQUALS:
122                                        if (theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) {
123                                                theDateParam.setValueAsString(DateUtils.getCompletedDate(theDateParam.getValueAsString())
124                                                                .getLeft());
125                                        }
126
127                                        // there is only one value; we will set it as the upper bound
128                                        // as a <= operation
129                                        validateAndSet(null, theDateParam);
130                                        break;
131                                default:
132                                        // Should not happen
133                                        throw new InvalidRequestException(Msg.code(1921) + "Invalid comparator for date range parameter:"
134                                                        + theDateParam.getPrefix() + ". This is a bug.");
135                        }
136                }
137        }
138
139        /**
140         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
141         *
142         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
143         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
144         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
145         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
146         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
147         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
148         */
149        public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) {
150                this();
151                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
152        }
153
154        /**
155         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
156         *
157         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
158         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
159         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
160         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
161         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
162         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
163         */
164        public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
165                this();
166                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
167        }
168
169        /**
170         * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
171         *
172         * @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g.
173         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
174         *                      one may be null, but it is not valid for both to be null.
175         * @param theUpperBound An unqualified date param representing the upper date bound (optionally may include time), e.g.
176         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
177         *                      one may be null, but it is not valid for both to be null.
178         */
179        public DateRangeParam(String theLowerBound, String theUpperBound) {
180                this();
181                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
182        }
183
184        private void addParam(DateParam theParsed) throws InvalidRequestException {
185                if (theParsed.getPrefix() == null) {
186                        theParsed.setPrefix(EQUAL);
187                }
188
189                switch (theParsed.getPrefix()) {
190                        case NOT_EQUAL:
191                        case EQUAL:
192                                if (myLowerBound != null || myUpperBound != null) {
193                                        throw new InvalidRequestException(Msg.code(1922)
194                                                        + "Can not have multiple date range parameters for the same param without a qualifier");
195                                }
196                                if (theParsed.getMissing() != null) {
197                                        myLowerBound = theParsed;
198                                        myUpperBound = theParsed;
199                                } else {
200                                        myLowerBound = new DateParam(theParsed.getPrefix(), theParsed.getValueAsString());
201                                        myUpperBound = new DateParam(theParsed.getPrefix(), theParsed.getValueAsString());
202                                }
203                                break;
204                        case GREATERTHAN:
205                        case GREATERTHAN_OR_EQUALS:
206                        case STARTS_AFTER:
207                                if (myLowerBound != null) {
208                                        throw new InvalidRequestException(
209                                                        Msg.code(1923)
210                                                                        + "Can not have multiple date range parameters for the same param that specify a lower bound");
211                                }
212                                myLowerBound = theParsed;
213                                break;
214                        case LESSTHAN:
215                        case LESSTHAN_OR_EQUALS:
216                        case ENDS_BEFORE:
217                                if (myUpperBound != null) {
218                                        throw new InvalidRequestException(
219                                                        Msg.code(1924)
220                                                                        + "Can not have multiple date range parameters for the same param that specify an upper bound");
221                                }
222                                myUpperBound = theParsed;
223                                break;
224                        default:
225                                throw new InvalidRequestException(Msg.code(1925) + "Unknown comparator: " + theParsed.getPrefix());
226                }
227        }
228
229        @Override
230        public boolean equals(Object obj) {
231                if (obj == this) {
232                        return true;
233                }
234                if (!(obj instanceof DateRangeParam)) {
235                        return false;
236                }
237                DateRangeParam other = (DateRangeParam) obj;
238                return Objects.equals(myLowerBound, other.myLowerBound) && Objects.equals(myUpperBound, other.myUpperBound);
239        }
240
241        public DateParam getLowerBound() {
242                return myLowerBound;
243        }
244
245        public DateRangeParam setLowerBound(DateParam theLowerBound) {
246                validateAndSet(theLowerBound, myUpperBound);
247                return this;
248        }
249
250        /**
251         * Sets the lower bound using a string that is compliant with
252         * FHIR dateTime format (ISO-8601).
253         * <p>
254         * This lower bound is assumed to have a <code>ge</code>
255         * (greater than or equals) modifier.
256         * </p>
257         * <p>
258         * Note: An operation can take a DateRangeParam. If only a single date is provided,
259         * it will still result in a DateRangeParam where the lower and upper bounds
260         * are the same value. As such, even though the prefixes for the lower and
261         * upper bounds default to <code>ge</code> and <code>le</code> respectively,
262         * the resulting prefix is effectively <code>eq</code> where only a single
263         * date is provided - as required by the FHIR specification (i.e. "If no
264         * prefix is present, the prefix <code>eq</code> is assumed").
265         * </p>
266         */
267        public DateRangeParam setLowerBound(String theLowerBound) {
268                setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound));
269                return this;
270        }
271
272        /**
273         * Sets the lower bound to be greaterthan or equal to the given date
274         */
275        public DateRangeParam setLowerBoundInclusive(Date theLowerBound) {
276                validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, theLowerBound), myUpperBound);
277                return this;
278        }
279
280        /**
281         * Sets the upper bound to be greaterthan or equal to the given date
282         */
283        public DateRangeParam setUpperBoundInclusive(Date theUpperBound) {
284                validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, theUpperBound));
285                return this;
286        }
287
288        /**
289         * Sets the lower bound to be greaterthan to the given date
290         */
291        public DateRangeParam setLowerBoundExclusive(Date theLowerBound) {
292                validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN, theLowerBound), myUpperBound);
293                return this;
294        }
295
296        /**
297         * Sets the upper bound to be greaterthan to the given date
298         */
299        public DateRangeParam setUpperBoundExclusive(Date theUpperBound) {
300                validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN, theUpperBound));
301                return this;
302        }
303
304        /**
305         * Return the current lower bound as an integer representative of the date.
306         *
307         * e.g. 2019-02-22T04:22:00-0500 -> 20120922
308         */
309        public Integer getLowerBoundAsDateInteger() {
310                if (myLowerBound == null || myLowerBound.getValue() == null) {
311                        return null;
312                }
313                int retVal = DateUtils.convertDateToDayInteger(myLowerBound.getValue());
314
315                if (myLowerBound.getPrefix() != null) {
316                        switch (myLowerBound.getPrefix()) {
317                                case GREATERTHAN:
318                                case STARTS_AFTER:
319                                        retVal += 1;
320                                        break;
321                                case EQUAL:
322                                case GREATERTHAN_OR_EQUALS:
323                                case NOT_EQUAL:
324                                        break;
325                                case LESSTHAN:
326                                case LESSTHAN_OR_EQUALS:
327                                case APPROXIMATE:
328                                case ENDS_BEFORE:
329                                        throw new IllegalStateException(
330                                                        Msg.code(1926) + "Invalid lower bound comparator: " + myLowerBound.getPrefix());
331                        }
332                }
333                return retVal;
334        }
335
336        /**
337         * Return the current upper bound as an integer representative of the date
338         *
339         * e.g. 2019-02-22T04:22:00-0500 -> 2019122
340         */
341        public Integer getUpperBoundAsDateInteger() {
342                if (myUpperBound == null || myUpperBound.getValue() == null) {
343                        return null;
344                }
345                int retVal = DateUtils.convertDateToDayInteger(myUpperBound.getValue());
346                if (myUpperBound.getPrefix() != null) {
347                        switch (myUpperBound.getPrefix()) {
348                                case LESSTHAN:
349                                case ENDS_BEFORE:
350                                        retVal -= 1;
351                                        break;
352                                case EQUAL:
353                                case LESSTHAN_OR_EQUALS:
354                                case NOT_EQUAL:
355                                        break;
356                                case GREATERTHAN_OR_EQUALS:
357                                case GREATERTHAN:
358                                case APPROXIMATE:
359                                case STARTS_AFTER:
360                                        throw new IllegalStateException(
361                                                        Msg.code(1927) + "Invalid upper bound comparator: " + myUpperBound.getPrefix());
362                        }
363                }
364                return retVal;
365        }
366
367        public Date getLowerBoundAsInstant() {
368                if (myLowerBound == null || myLowerBound.getValue() == null) {
369                        return null;
370                }
371                return getLowerBoundAsInstant(myLowerBound);
372        }
373
374        @Nonnull
375        private static Date getLowerBoundAsInstant(@Nonnull DateParam theLowerBound) {
376                Date retVal = theLowerBound.getValue();
377                if (theLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
378                        retVal = DateUtils.getLowestInstantFromDate(retVal);
379                }
380
381                if (theLowerBound.getPrefix() != null) {
382                        switch (theLowerBound.getPrefix()) {
383                                case GREATERTHAN:
384                                case STARTS_AFTER:
385                                        retVal = theLowerBound.getPrecision().add(retVal, 1);
386                                        break;
387                                case EQUAL:
388                                case NOT_EQUAL:
389                                case GREATERTHAN_OR_EQUALS:
390                                        break;
391                                case LESSTHAN_OR_EQUALS:
392                                case LESSTHAN:
393                                case APPROXIMATE:
394                                case ENDS_BEFORE:
395                                        throw new IllegalStateException(
396                                                        Msg.code(1928) + "Invalid lower bound comparator: " + theLowerBound.getPrefix());
397                        }
398                }
399                return retVal;
400        }
401
402        public DateParam getUpperBound() {
403                return myUpperBound;
404        }
405
406        /**
407         * Sets the upper bound using a string that is compliant with
408         * FHIR dateTime format (ISO-8601).
409         * <p>
410         * This upper bound is assumed to have a <code>le</code>
411         * (less than or equals) modifier.
412         * </p>
413         * <p>
414         * Note: An operation can take a DateRangeParam. If only a single date is provided,
415         * it will still result in a DateRangeParam where the lower and upper bounds
416         * are the same value. As such, even though the prefixes for the lower and
417         * upper bounds default to <code>ge</code> and <code>le</code> respectively,
418         * the resulting prefix is effectively <code>eq</code> where only a single
419         * date is provided - as required by the FHIR specificiation (i.e. "If no
420         * prefix is present, the prefix <code>eq</code> is assumed").
421         * </p>
422         */
423        public DateRangeParam setUpperBound(String theUpperBound) {
424                setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound));
425                return this;
426        }
427
428        public DateRangeParam setUpperBound(DateParam theUpperBound) {
429                validateAndSet(myLowerBound, theUpperBound);
430                return this;
431        }
432
433        public Date getUpperBoundAsInstant() {
434                if (myUpperBound == null || myUpperBound.getValue() == null) {
435                        return null;
436                }
437
438                return getUpperBoundAsInstant(myUpperBound);
439        }
440
441        @Nonnull
442        private static Date getUpperBoundAsInstant(@Nonnull DateParam theUpperBound) {
443                Date retVal = theUpperBound.getValue();
444
445                if (theUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
446                        retVal = DateUtils.getHighestInstantFromDate(retVal);
447                }
448
449                if (theUpperBound.getPrefix() != null) {
450                        switch (theUpperBound.getPrefix()) {
451                                case LESSTHAN:
452                                case ENDS_BEFORE:
453                                        retVal = new Date(retVal.getTime() - 1L);
454                                        break;
455                                case EQUAL:
456                                case NOT_EQUAL:
457                                case LESSTHAN_OR_EQUALS:
458                                        retVal = theUpperBound.getPrecision().add(retVal, 1);
459                                        retVal = new Date(retVal.getTime() - 1L);
460                                        break;
461                                case GREATERTHAN_OR_EQUALS:
462                                case GREATERTHAN:
463                                case APPROXIMATE:
464                                case STARTS_AFTER:
465                                        throw new IllegalStateException(
466                                                        Msg.code(1929) + "Invalid upper bound comparator: " + theUpperBound.getPrefix());
467                        }
468                }
469                return retVal;
470        }
471
472        @Override
473        public List<DateParam> getValuesAsQueryTokens() {
474                ArrayList<DateParam> retVal = new ArrayList<>();
475                if (myLowerBound != null && myLowerBound.getMissing() != null) {
476                        retVal.add((myLowerBound));
477                } else {
478                        boolean hasLowerBound = myLowerBound != null && !myLowerBound.isEmpty();
479                        boolean hasUpperBound = myUpperBound != null && !myUpperBound.isEmpty();
480
481                        if (hasLowerBound) {
482                                retVal.add((myLowerBound));
483                        }
484                        if (hasUpperBound) {
485                                retVal.add((myUpperBound));
486                        }
487                }
488                return retVal;
489        }
490
491        private boolean hasBound(DateParam bound) {
492                return bound != null && !bound.isEmpty();
493        }
494
495        @Override
496        public int hashCode() {
497                return Objects.hash(myLowerBound, myUpperBound);
498        }
499
500        public boolean isEmpty() {
501                return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
502        }
503
504        /**
505         * Sets the range from a pair of dates, inclusive on both ends
506         *
507         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
508         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
509         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
510         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
511         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
512         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
513         */
514        public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
515                DateParam lowerBound = theLowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
516                DateParam upperBound = theUpperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
517                validateAndSet(lowerBound, upperBound);
518        }
519
520        /**
521         * Sets the range from a pair of dates, inclusive on both ends
522         *
523         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
524         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
525         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
526         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
527         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
528         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
529         */
530        public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) {
531                validateAndSet(theLowerBound, theUpperBound);
532        }
533
534        /**
535         * Sets the range from a pair of dates, inclusive on both ends. Note that if
536         * theLowerBound is after theUpperBound, thie method will automatically reverse
537         * the order of the arguments in order to create an inclusive range.
538         *
539         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
540         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
541         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
542         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
543         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
544         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
545         */
546        public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
547                IPrimitiveType<Date> lowerBound = theLowerBound;
548                IPrimitiveType<Date> upperBound = theUpperBound;
549                if (lowerBound != null
550                                && lowerBound.getValue() != null
551                                && upperBound != null
552                                && upperBound.getValue() != null) {
553                        if (lowerBound.getValue().after(upperBound.getValue())) {
554                                IPrimitiveType<Date> temp = lowerBound;
555                                lowerBound = upperBound;
556                                upperBound = temp;
557                        }
558                }
559                validateAndSet(
560                                lowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, lowerBound) : null,
561                                upperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, upperBound) : null);
562        }
563
564        /**
565         * Sets the range from a pair of dates, inclusive on both ends
566         *
567         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
568         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
569         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
570         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
571         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
572         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
573         */
574        public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
575                DateParam lowerBound = theLowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
576                DateParam upperBound = theUpperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
577                if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) {
578                        lowerBound.setPrefix(EQUAL);
579                        upperBound.setPrefix(EQUAL);
580                }
581                validateAndSet(lowerBound, upperBound);
582        }
583
584        @Override
585        public void setValuesAsQueryTokens(
586                        FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters)
587                        throws InvalidRequestException {
588
589                boolean haveHadUnqualifiedParameter = false;
590                for (QualifiedParamList paramList : theParameters) {
591                        if (paramList.size() == 0) {
592                                continue;
593                        }
594                        if (paramList.size() > 1) {
595                                throw new InvalidRequestException(Msg.code(1930) + "DateRange parameter does not support OR queries");
596                        }
597                        String param = paramList.get(0);
598
599                        /*
600                         * Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not
601                         * escaped theirs
602                         */
603                        param = param.replace(' ', '+');
604
605                        DateParam parsed = new DateParam();
606                        parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param);
607                        addParam(parsed);
608
609                        if (parsed.getPrefix() == null) {
610                                if (haveHadUnqualifiedParameter) {
611                                        throw new InvalidRequestException(
612                                                        Msg.code(1931)
613                                                                        + "Multiple date parameters with the same name and no qualifier (>, <, etc.) is not supported");
614                                }
615                                haveHadUnqualifiedParameter = true;
616                        }
617                }
618        }
619
620        @Override
621        public String toString() {
622                StringBuilder b = new StringBuilder();
623                b.append(getClass().getSimpleName());
624                b.append("[");
625                if (hasBound(myLowerBound)) {
626                        if (myLowerBound.getPrefix() != null) {
627                                b.append(myLowerBound.getPrefix().getValue());
628                        }
629                        b.append(myLowerBound.getValueAsString());
630                }
631                if (hasBound(myUpperBound)) {
632                        if (hasBound(myLowerBound)) {
633                                b.append(" ");
634                        }
635                        if (myUpperBound.getPrefix() != null) {
636                                b.append(myUpperBound.getPrefix().getValue());
637                        }
638                        b.append(myUpperBound.getValueAsString());
639                } else {
640                        if (!hasBound(myLowerBound)) {
641                                b.append("empty");
642                        }
643                }
644                b.append("]");
645                return b.toString();
646        }
647
648        /**
649         * Note: An operation can take a DateRangeParam. If only a single date is provided,
650         * it will still result in a DateRangeParam where the lower and upper bounds
651         * are the same value. As such, even though the prefixes for the lower and
652         * upper bounds default to <code>ge</code> and <code>le</code> respectively,
653         * the resulting prefix is effectively <code>eq</code> where only a single
654         * date is provided - as required by the FHIR specification (i.e. "If no
655         * prefix is present, the prefix <code>eq</code> is assumed").
656         */
657        private void validateAndSet(DateParam lowerBound, DateParam upperBound) {
658                if (hasBound(lowerBound) && hasBound(upperBound)) {
659                        Date lowerBoundAsInstant = getLowerBoundAsInstant(lowerBound);
660                        Date upperBoundAsInstant = getUpperBoundAsInstant(upperBound);
661                        if (lowerBoundAsInstant.after(upperBoundAsInstant)) {
662                                throw new DataFormatException(Msg.code(1932)
663                                                + format(
664                                                                "Lower bound of %s is after upper bound of %s",
665                                                                lowerBound.getValueAsString(), upperBound.getValueAsString()));
666                        }
667                }
668
669                if (hasBound(lowerBound)) {
670                        if (lowerBound.getPrefix() == null) {
671                                lowerBound.setPrefix(GREATERTHAN_OR_EQUALS);
672                        }
673                        switch (lowerBound.getPrefix()) {
674                                case GREATERTHAN:
675                                case GREATERTHAN_OR_EQUALS:
676                                default:
677                                        break;
678                                case LESSTHAN:
679                                case LESSTHAN_OR_EQUALS:
680                                        throw new DataFormatException(Msg.code(1933) + "Lower bound comparator must be > or >=, can not be "
681                                                        + lowerBound.getPrefix().getValue());
682                        }
683                }
684
685                if (hasBound(upperBound)) {
686                        if (upperBound.getPrefix() == null) {
687                                upperBound.setPrefix(LESSTHAN_OR_EQUALS);
688                        }
689                        switch (upperBound.getPrefix()) {
690                                case LESSTHAN:
691                                case LESSTHAN_OR_EQUALS:
692                                default:
693                                        break;
694                                case GREATERTHAN:
695                                case GREATERTHAN_OR_EQUALS:
696                                        throw new DataFormatException(Msg.code(1934) + "Upper bound comparator must be < or <=, can not be "
697                                                        + upperBound.getPrefix().getValue());
698                        }
699                }
700
701                myLowerBound = lowerBound;
702                myUpperBound = upperBound;
703        }
704}