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