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                                        validateAndSet(theDateParam, null);
116                                        break;
117                                case ENDS_BEFORE:
118                                case LESSTHAN:
119                                case LESSTHAN_OR_EQUALS:
120                                        if (theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) {
121                                                theDateParam.setValueAsString(DateUtils.getCompletedDate(theDateParam.getValueAsString())
122                                                                .getLeft());
123                                        }
124                                        validateAndSet(null, theDateParam);
125                                        break;
126                                default:
127                                        // Should not happen
128                                        throw new InvalidRequestException(Msg.code(1921) + "Invalid comparator for date range parameter:"
129                                                        + theDateParam.getPrefix() + ". This is a bug.");
130                        }
131                }
132        }
133
134        /**
135         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
136         *
137         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
138         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
139         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
140         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
141         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
142         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
143         */
144        public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) {
145                this();
146                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
147        }
148
149        /**
150         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
151         *
152         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
153         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
154         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
155         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
156         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
157         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
158         */
159        public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
160                this();
161                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
162        }
163
164        /**
165         * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
166         *
167         * @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g.
168         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
169         *                      one may be null, but it is not valid for both to be null.
170         * @param theUpperBound An unqualified date param representing the upper date bound (optionally may include time), e.g.
171         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
172         *                      one may be null, but it is not valid for both to be null.
173         */
174        public DateRangeParam(String theLowerBound, String theUpperBound) {
175                this();
176                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
177        }
178
179        private void addParam(DateParam theParsed) throws InvalidRequestException {
180                if (theParsed.getPrefix() == null) {
181                        theParsed.setPrefix(EQUAL);
182                }
183
184                switch (theParsed.getPrefix()) {
185                        case NOT_EQUAL:
186                        case EQUAL:
187                                if (myLowerBound != null || myUpperBound != null) {
188                                        throw new InvalidRequestException(Msg.code(1922)
189                                                        + "Can not have multiple date range parameters for the same param without a qualifier");
190                                }
191                                if (theParsed.getMissing() != null) {
192                                        myLowerBound = theParsed;
193                                        myUpperBound = theParsed;
194                                } else {
195                                        myLowerBound = new DateParam(theParsed.getPrefix(), theParsed.getValueAsString());
196                                        myUpperBound = new DateParam(theParsed.getPrefix(), theParsed.getValueAsString());
197                                }
198                                break;
199                        case GREATERTHAN:
200                        case GREATERTHAN_OR_EQUALS:
201                        case STARTS_AFTER:
202                                if (myLowerBound != null) {
203                                        throw new InvalidRequestException(
204                                                        Msg.code(1923)
205                                                                        + "Can not have multiple date range parameters for the same param that specify a lower bound");
206                                }
207                                myLowerBound = theParsed;
208                                break;
209                        case LESSTHAN:
210                        case LESSTHAN_OR_EQUALS:
211                        case ENDS_BEFORE:
212                                if (myUpperBound != null) {
213                                        throw new InvalidRequestException(
214                                                        Msg.code(1924)
215                                                                        + "Can not have multiple date range parameters for the same param that specify an upper bound");
216                                }
217                                myUpperBound = theParsed;
218                                break;
219                        default:
220                                throw new InvalidRequestException(Msg.code(1925) + "Unknown comparator: " + theParsed.getPrefix());
221                }
222        }
223
224        @Override
225        public boolean equals(Object obj) {
226                if (obj == this) {
227                        return true;
228                }
229                if (!(obj instanceof DateRangeParam)) {
230                        return false;
231                }
232                DateRangeParam other = (DateRangeParam) obj;
233                return Objects.equals(myLowerBound, other.myLowerBound) && Objects.equals(myUpperBound, other.myUpperBound);
234        }
235
236        public DateParam getLowerBound() {
237                return myLowerBound;
238        }
239
240        public DateRangeParam setLowerBound(DateParam theLowerBound) {
241                validateAndSet(theLowerBound, myUpperBound);
242                return this;
243        }
244
245        /**
246         * Sets the lower bound using a string that is compliant with
247         * FHIR dateTime format (ISO-8601).
248         * <p>
249         * This lower bound is assumed to have a <code>ge</code>
250         * (greater than or equals) modifier.
251         * </p>
252         * <p>
253         * Note: An operation can take a DateRangeParam. If only a single date is provided,
254         * it will still result in a DateRangeParam where the lower and upper bounds
255         * are the same value. As such, even though the prefixes for the lower and
256         * upper bounds default to <code>ge</code> and <code>le</code> respectively,
257         * the resulting prefix is effectively <code>eq</code> where only a single
258         * date is provided - as required by the FHIR specification (i.e. "If no
259         * prefix is present, the prefix <code>eq</code> is assumed").
260         * </p>
261         */
262        public DateRangeParam setLowerBound(String theLowerBound) {
263                setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound));
264                return this;
265        }
266
267        /**
268         * Sets the lower bound to be greaterthan or equal to the given date
269         */
270        public DateRangeParam setLowerBoundInclusive(Date theLowerBound) {
271                validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, theLowerBound), myUpperBound);
272                return this;
273        }
274
275        /**
276         * Sets the upper bound to be greaterthan or equal to the given date
277         */
278        public DateRangeParam setUpperBoundInclusive(Date theUpperBound) {
279                validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, theUpperBound));
280                return this;
281        }
282
283        /**
284         * Sets the lower bound to be greaterthan to the given date
285         */
286        public DateRangeParam setLowerBoundExclusive(Date theLowerBound) {
287                validateAndSet(new DateParam(ParamPrefixEnum.GREATERTHAN, theLowerBound), myUpperBound);
288                return this;
289        }
290
291        /**
292         * Sets the upper bound to be greaterthan to the given date
293         */
294        public DateRangeParam setUpperBoundExclusive(Date theUpperBound) {
295                validateAndSet(myLowerBound, new DateParam(ParamPrefixEnum.LESSTHAN, theUpperBound));
296                return this;
297        }
298
299        /**
300         * Return the current lower bound as an integer representative of the date.
301         *
302         * e.g. 2019-02-22T04:22:00-0500 -> 20120922
303         */
304        public Integer getLowerBoundAsDateInteger() {
305                if (myLowerBound == null || myLowerBound.getValue() == null) {
306                        return null;
307                }
308                int retVal = DateUtils.convertDateToDayInteger(myLowerBound.getValue());
309
310                if (myLowerBound.getPrefix() != null) {
311                        switch (myLowerBound.getPrefix()) {
312                                case GREATERTHAN:
313                                case STARTS_AFTER:
314                                        retVal += 1;
315                                        break;
316                                case EQUAL:
317                                case GREATERTHAN_OR_EQUALS:
318                                case NOT_EQUAL:
319                                        break;
320                                case LESSTHAN:
321                                case APPROXIMATE:
322                                case LESSTHAN_OR_EQUALS:
323                                case ENDS_BEFORE:
324                                        throw new IllegalStateException(
325                                                        Msg.code(1926) + "Invalid lower bound comparator: " + myLowerBound.getPrefix());
326                        }
327                }
328                return retVal;
329        }
330
331        /**
332         * Return the current upper bound as an integer representative of the date
333         *
334         * e.g. 2019-02-22T04:22:00-0500 -> 2019122
335         */
336        public Integer getUpperBoundAsDateInteger() {
337                if (myUpperBound == null || myUpperBound.getValue() == null) {
338                        return null;
339                }
340                int retVal = DateUtils.convertDateToDayInteger(myUpperBound.getValue());
341                if (myUpperBound.getPrefix() != null) {
342                        switch (myUpperBound.getPrefix()) {
343                                case LESSTHAN:
344                                case ENDS_BEFORE:
345                                        retVal -= 1;
346                                        break;
347                                case EQUAL:
348                                case LESSTHAN_OR_EQUALS:
349                                case NOT_EQUAL:
350                                        break;
351                                case GREATERTHAN_OR_EQUALS:
352                                case GREATERTHAN:
353                                case APPROXIMATE:
354                                case STARTS_AFTER:
355                                        throw new IllegalStateException(
356                                                        Msg.code(1927) + "Invalid upper bound comparator: " + myUpperBound.getPrefix());
357                        }
358                }
359                return retVal;
360        }
361
362        public Date getLowerBoundAsInstant() {
363                if (myLowerBound == null || myLowerBound.getValue() == null) {
364                        return null;
365                }
366                return getLowerBoundAsInstant(myLowerBound);
367        }
368
369        @Nonnull
370        private static Date getLowerBoundAsInstant(@Nonnull DateParam theLowerBound) {
371                Date retVal = theLowerBound.getValue();
372                if (theLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
373                        retVal = DateUtils.getLowestInstantFromDate(retVal);
374                }
375
376                if (theLowerBound.getPrefix() != null) {
377                        switch (theLowerBound.getPrefix()) {
378                                case GREATERTHAN:
379                                case STARTS_AFTER:
380                                        retVal = theLowerBound.getPrecision().add(retVal, 1);
381                                        break;
382                                case EQUAL:
383                                case NOT_EQUAL:
384                                case GREATERTHAN_OR_EQUALS:
385                                        break;
386                                case LESSTHAN:
387                                case APPROXIMATE:
388                                case LESSTHAN_OR_EQUALS:
389                                case ENDS_BEFORE:
390                                        throw new IllegalStateException(
391                                                        Msg.code(1928) + "Invalid lower bound comparator: " + theLowerBound.getPrefix());
392                        }
393                }
394                return retVal;
395        }
396
397        public DateParam getUpperBound() {
398                return myUpperBound;
399        }
400
401        /**
402         * Sets the upper bound using a string that is compliant with
403         * FHIR dateTime format (ISO-8601).
404         * <p>
405         * This upper bound is assumed to have a <code>le</code>
406         * (less than or equals) modifier.
407         * </p>
408         * <p>
409         * Note: An operation can take a DateRangeParam. If only a single date is provided,
410         * it will still result in a DateRangeParam where the lower and upper bounds
411         * are the same value. As such, even though the prefixes for the lower and
412         * upper bounds default to <code>ge</code> and <code>le</code> respectively,
413         * the resulting prefix is effectively <code>eq</code> where only a single
414         * date is provided - as required by the FHIR specificiation (i.e. "If no
415         * prefix is present, the prefix <code>eq</code> is assumed").
416         * </p>
417         */
418        public DateRangeParam setUpperBound(String theUpperBound) {
419                setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound));
420                return this;
421        }
422
423        public DateRangeParam setUpperBound(DateParam theUpperBound) {
424                validateAndSet(myLowerBound, theUpperBound);
425                return this;
426        }
427
428        public Date getUpperBoundAsInstant() {
429                if (myUpperBound == null || myUpperBound.getValue() == null) {
430                        return null;
431                }
432
433                return getUpperBoundAsInstant(myUpperBound);
434        }
435
436        @Nonnull
437        private static Date getUpperBoundAsInstant(@Nonnull DateParam theUpperBound) {
438                Date retVal = theUpperBound.getValue();
439
440                if (theUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
441                        retVal = DateUtils.getHighestInstantFromDate(retVal);
442                }
443
444                if (theUpperBound.getPrefix() != null) {
445                        switch (theUpperBound.getPrefix()) {
446                                case LESSTHAN:
447                                case ENDS_BEFORE:
448                                        retVal = new Date(retVal.getTime() - 1L);
449                                        break;
450                                case EQUAL:
451                                case NOT_EQUAL:
452                                case LESSTHAN_OR_EQUALS:
453                                        retVal = theUpperBound.getPrecision().add(retVal, 1);
454                                        retVal = new Date(retVal.getTime() - 1L);
455                                        break;
456                                case GREATERTHAN_OR_EQUALS:
457                                case GREATERTHAN:
458                                case APPROXIMATE:
459                                case STARTS_AFTER:
460                                        throw new IllegalStateException(
461                                                        Msg.code(1929) + "Invalid upper bound comparator: " + theUpperBound.getPrefix());
462                        }
463                }
464                return retVal;
465        }
466
467        @Override
468        public List<DateParam> getValuesAsQueryTokens() {
469                ArrayList<DateParam> retVal = new ArrayList<>();
470                if (myLowerBound != null && myLowerBound.getMissing() != null) {
471                        retVal.add((myLowerBound));
472                } else {
473                        if (myLowerBound != null && !myLowerBound.isEmpty()) {
474                                retVal.add((myLowerBound));
475                        }
476                        if (myUpperBound != null && !myUpperBound.isEmpty()) {
477                                retVal.add((myUpperBound));
478                        }
479                }
480                return retVal;
481        }
482
483        private boolean hasBound(DateParam bound) {
484                return bound != null && !bound.isEmpty();
485        }
486
487        @Override
488        public int hashCode() {
489                return Objects.hash(myLowerBound, myUpperBound);
490        }
491
492        public boolean isEmpty() {
493                return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
494        }
495
496        /**
497         * Sets the range from a pair of dates, inclusive on both ends
498         *
499         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
500         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
501         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
502         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
503         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
504         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
505         */
506        public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
507                DateParam lowerBound = theLowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
508                DateParam upperBound = theUpperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
509                validateAndSet(lowerBound, upperBound);
510        }
511
512        /**
513         * Sets the range from a pair of dates, inclusive on both ends
514         *
515         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
516         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
517         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
518         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
519         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
520         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
521         */
522        public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) {
523                validateAndSet(theLowerBound, theUpperBound);
524        }
525
526        /**
527         * Sets the range from a pair of dates, inclusive on both ends. Note that if
528         * theLowerBound is after theUpperBound, thie method will automatically reverse
529         * the order of the arguments in order to create an inclusive range.
530         *
531         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
532         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
533         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
534         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
535         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
536         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
537         */
538        public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
539                IPrimitiveType<Date> lowerBound = theLowerBound;
540                IPrimitiveType<Date> upperBound = theUpperBound;
541                if (lowerBound != null
542                                && lowerBound.getValue() != null
543                                && upperBound != null
544                                && upperBound.getValue() != null) {
545                        if (lowerBound.getValue().after(upperBound.getValue())) {
546                                IPrimitiveType<Date> temp = lowerBound;
547                                lowerBound = upperBound;
548                                upperBound = temp;
549                        }
550                }
551                validateAndSet(
552                                lowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, lowerBound) : null,
553                                upperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, upperBound) : null);
554        }
555
556        /**
557         * Sets the range from a pair of dates, inclusive on both ends
558         *
559         * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
560         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
561         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
562         * @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
563         *                      "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
564         *                      theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
565         */
566        public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
567                DateParam lowerBound = theLowerBound != null ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
568                DateParam upperBound = theUpperBound != null ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
569                if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) {
570                        lowerBound.setPrefix(EQUAL);
571                        upperBound.setPrefix(EQUAL);
572                }
573                validateAndSet(lowerBound, upperBound);
574        }
575
576        @Override
577        public void setValuesAsQueryTokens(
578                        FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters)
579                        throws InvalidRequestException {
580
581                boolean haveHadUnqualifiedParameter = false;
582                for (QualifiedParamList paramList : theParameters) {
583                        if (paramList.size() == 0) {
584                                continue;
585                        }
586                        if (paramList.size() > 1) {
587                                throw new InvalidRequestException(Msg.code(1930) + "DateRange parameter does not support OR queries");
588                        }
589                        String param = paramList.get(0);
590
591                        /*
592                         * Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not
593                         * escaped theirs
594                         */
595                        param = param.replace(' ', '+');
596
597                        DateParam parsed = new DateParam();
598                        parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param);
599                        addParam(parsed);
600
601                        if (parsed.getPrefix() == null) {
602                                if (haveHadUnqualifiedParameter) {
603                                        throw new InvalidRequestException(
604                                                        Msg.code(1931)
605                                                                        + "Multiple date parameters with the same name and no qualifier (>, <, etc.) is not supported");
606                                }
607                                haveHadUnqualifiedParameter = true;
608                        }
609                }
610        }
611
612        @Override
613        public String toString() {
614                StringBuilder b = new StringBuilder();
615                b.append(getClass().getSimpleName());
616                b.append("[");
617                if (hasBound(myLowerBound)) {
618                        if (myLowerBound.getPrefix() != null) {
619                                b.append(myLowerBound.getPrefix().getValue());
620                        }
621                        b.append(myLowerBound.getValueAsString());
622                }
623                if (hasBound(myUpperBound)) {
624                        if (hasBound(myLowerBound)) {
625                                b.append(" ");
626                        }
627                        if (myUpperBound.getPrefix() != null) {
628                                b.append(myUpperBound.getPrefix().getValue());
629                        }
630                        b.append(myUpperBound.getValueAsString());
631                } else {
632                        if (!hasBound(myLowerBound)) {
633                                b.append("empty");
634                        }
635                }
636                b.append("]");
637                return b.toString();
638        }
639
640        /**
641         * Note: An operation can take a DateRangeParam. If only a single date is provided,
642         * it will still result in a DateRangeParam where the lower and upper bounds
643         * are the same value. As such, even though the prefixes for the lower and
644         * upper bounds default to <code>ge</code> and <code>le</code> respectively,
645         * the resulting prefix is effectively <code>eq</code> where only a single
646         * date is provided - as required by the FHIR specification (i.e. "If no
647         * prefix is present, the prefix <code>eq</code> is assumed").
648         */
649        private void validateAndSet(DateParam lowerBound, DateParam upperBound) {
650                if (hasBound(lowerBound) && hasBound(upperBound)) {
651                        Date lowerBoundAsInstant = getLowerBoundAsInstant(lowerBound);
652                        Date upperBoundAsInstant = getUpperBoundAsInstant(upperBound);
653                        if (lowerBoundAsInstant.after(upperBoundAsInstant)) {
654                                throw new DataFormatException(Msg.code(1932)
655                                                + format(
656                                                                "Lower bound of %s is after upper bound of %s",
657                                                                lowerBound.getValueAsString(), upperBound.getValueAsString()));
658                        }
659                }
660
661                if (hasBound(lowerBound)) {
662                        if (lowerBound.getPrefix() == null) {
663                                lowerBound.setPrefix(GREATERTHAN_OR_EQUALS);
664                        }
665                        switch (lowerBound.getPrefix()) {
666                                case GREATERTHAN:
667                                case GREATERTHAN_OR_EQUALS:
668                                default:
669                                        break;
670                                case LESSTHAN:
671                                case LESSTHAN_OR_EQUALS:
672                                        throw new DataFormatException(Msg.code(1933) + "Lower bound comparator must be > or >=, can not be "
673                                                        + lowerBound.getPrefix().getValue());
674                        }
675                }
676
677                if (hasBound(upperBound)) {
678                        if (upperBound.getPrefix() == null) {
679                                upperBound.setPrefix(LESSTHAN_OR_EQUALS);
680                        }
681                        switch (upperBound.getPrefix()) {
682                                case LESSTHAN:
683                                case LESSTHAN_OR_EQUALS:
684                                default:
685                                        break;
686                                case GREATERTHAN:
687                                case GREATERTHAN_OR_EQUALS:
688                                        throw new DataFormatException(Msg.code(1934) + "Upper bound comparator must be < or <=, can not be "
689                                                        + upperBound.getPrefix().getValue());
690                        }
691                }
692
693                myLowerBound = lowerBound;
694                myUpperBound = upperBound;
695        }
696}