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