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