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