001package ca.uhn.fhir.rest.param;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.util.ArrayList;
024import java.util.Date;
025import java.util.List;
026
027import org.hl7.fhir.instance.model.api.IPrimitiveType;
028
029import ca.uhn.fhir.model.api.IQueryParameterAnd;
030import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
031import ca.uhn.fhir.model.primitive.DateTimeDt;
032import ca.uhn.fhir.parser.DataFormatException;
033import ca.uhn.fhir.rest.method.QualifiedParamList;
034import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
035
036public class DateRangeParam implements IQueryParameterAnd<DateParam> {
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                // nothing
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 theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
055         * @param theUpperBound
056         *            A qualified date param representing the upper 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 theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
058         */
059        public DateRangeParam(Date theLowerBound, Date theUpperBound) {
060                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
061        }
062
063        /**
064         * Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound
065         * (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the
066         * lower or upper bound, with no opposite bound.
067         */
068        public DateRangeParam(DateParam theDateParam) {
069                if (theDateParam == null) {
070                        throw new NullPointerException("theDateParam can not be null");
071                }
072                if (theDateParam.isEmpty()) {
073                        throw new IllegalArgumentException("theDateParam can not be empty");
074                }
075                if (theDateParam.getComparator() == null) {
076                        setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
077                } else {
078                        switch (theDateParam.getComparator()) {
079                        case GREATERTHAN:
080                        case GREATERTHAN_OR_EQUALS:
081                                myLowerBound = theDateParam;
082                                myUpperBound = null;
083                                break;
084                        case LESSTHAN:
085                        case LESSTHAN_OR_EQUALS:
086                                myLowerBound = null;
087                                myUpperBound = theDateParam;
088                                break;
089                        default:
090                                // Should not happen
091                                throw new IllegalStateException("Unknown comparator:" + theDateParam.getComparator() + ". This is a bug.");
092                        }
093                }
094                validateAndThrowDataFormatExceptionIfInvalid();
095        }
096
097        /**
098         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
099         * 
100         * @param theLowerBound
101         *            A qualified date param representing the lower date bound (optionally may include time), e.g.
102         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
103         * @param theUpperBound
104         *            A qualified date param representing the upper date bound (optionally may include time), e.g.
105         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
106         */
107        public DateRangeParam(DateTimeDt theLowerBound, DateTimeDt theUpperBound) {
108                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
109        }
110
111        /**
112         * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
113         * 
114         * @param theLowerBound
115         *            A qualified date param representing the lower 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 theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
117         * @param theUpperBound
118         *            A qualified date param representing the upper date bound (optionally may include time), e.g.
119         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
120         */
121        public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
122                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
123        }
124
125        /**
126         * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
127         * 
128         * @param theLowerBound
129         *            An unqualified date param representing the lower date bound (optionally may include time), e.g.
130         *            "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
131         * @param theUpperBound
132         *            An unqualified date param representing the upper date bound (optionally may include time), e.g.
133         *            "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
134         */
135        public DateRangeParam(String theLowerBound, String theUpperBound) {
136                setRangeFromDatesInclusive(theLowerBound, theUpperBound);
137        }
138
139        private void addParam(DateParam theParsed) throws InvalidRequestException {
140                if (theParsed.getComparator() == null) {
141                        if (myLowerBound != null || myUpperBound != null) {
142                                throw new InvalidRequestException("Can not have multiple date range parameters for the same param without a qualifier");
143                        }
144
145                        myLowerBound = new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theParsed.getValueAsString());
146                        myUpperBound = new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theParsed.getValueAsString());
147                        
148                } else {
149
150                        switch (theParsed.getComparator()) {
151                        case GREATERTHAN:
152                        case GREATERTHAN_OR_EQUALS:
153                                if (myLowerBound != null) {
154                                        throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
155                                }
156                                myLowerBound = theParsed;
157                                break;
158                        case LESSTHAN:
159                        case LESSTHAN_OR_EQUALS:
160                                if (myUpperBound != null) {
161                                        throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
162                                }
163                                myUpperBound = theParsed;
164                                break;
165                        default:
166                                throw new InvalidRequestException("Unknown comparator: " + theParsed.getComparator());
167                        }
168
169                }
170        }
171
172        public DateParam getLowerBound() {
173                return myLowerBound;
174        }
175
176        public Date getLowerBoundAsInstant() {
177                if (myLowerBound == null) {
178                        return null;
179                }
180                Date retVal = myLowerBound.getValue();
181                if (myLowerBound.getComparator() != null) {
182                        switch (myLowerBound.getComparator()) {
183                        case GREATERTHAN:
184                                retVal = myLowerBound.getPrecision().add(retVal, 1);
185                                break;
186                        case GREATERTHAN_OR_EQUALS:
187                                break;
188                        case LESSTHAN:
189                        case LESSTHAN_OR_EQUALS:
190                                throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getComparator());
191                        }
192                }
193                return retVal;
194        }
195
196        public DateParam getUpperBound() {
197                return myUpperBound;
198        }
199
200        public Date getUpperBoundAsInstant() {
201                if (myUpperBound == null) {
202                        return null;
203                }
204                Date retVal = myUpperBound.getValue();
205                if (myUpperBound.getComparator() != null) {
206                        switch (myUpperBound.getComparator()) {
207                        case LESSTHAN:
208                                retVal = new Date(retVal.getTime() - 1L);
209                                break;
210                        case LESSTHAN_OR_EQUALS:
211                                retVal = myUpperBound.getPrecision().add(retVal, 1);
212                                retVal = new Date(retVal.getTime() - 1L);
213                                break;
214                        case GREATERTHAN_OR_EQUALS:
215                        case GREATERTHAN:
216                                throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getComparator());
217                        }
218                }
219                return retVal;
220        }
221
222        @Override
223        public List<DateParam> getValuesAsQueryTokens() {
224                ArrayList<DateParam> retVal = new ArrayList<DateParam>();
225                if (myLowerBound != null && !myLowerBound.isEmpty()) {
226                        retVal.add((myLowerBound));
227                }
228                if (myUpperBound != null && !myUpperBound.isEmpty()) {
229                        retVal.add((myUpperBound));
230                }
231                return retVal;
232        }
233
234        private boolean haveLowerBound() {
235                return myLowerBound != null && myLowerBound.isEmpty() == false;
236        }
237
238        private boolean haveUpperBound() {
239                return myUpperBound != null && myUpperBound.isEmpty() == false;
240        }
241
242        public boolean isEmpty() {
243                return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
244        }
245
246        public void setLowerBound(DateParam theLowerBound) {
247                myLowerBound = theLowerBound;
248                validateAndThrowDataFormatExceptionIfInvalid();
249        }
250
251        /**
252         * Sets the range from a pair of dates, inclusive on both ends
253         * 
254         * @param theLowerBound
255         *            A qualified date param representing the lower date bound (optionally may include time), e.g.
256         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
257         * @param theUpperBound
258         *            A qualified date param representing the upper date bound (optionally may include time), e.g.
259         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
260         */
261        public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
262                myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null;
263                myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null;
264                validateAndThrowDataFormatExceptionIfInvalid();
265        }
266
267        /**
268         * Sets the range from a pair of dates, inclusive on both ends
269         * 
270         * @param theLowerBound
271         *            A qualified date param representing the lower date bound (optionally may include time), e.g.
272         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
273         * @param theUpperBound
274         *            A qualified date param representing the upper date bound (optionally may include time), e.g.
275         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
276         */
277        public void setRangeFromDatesInclusive(DateTimeDt theLowerBound, DateTimeDt theUpperBound) {
278                if (theLowerBound instanceof DateParam) {
279                        myLowerBound = (DateParam) theLowerBound;
280                } else {
281                        myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null;
282                } 
283                if (theUpperBound instanceof DateParam) {
284                        myUpperBound = (DateParam) theUpperBound;
285                } else {
286                        myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null;
287                }
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 theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
297         * @param theUpperBound
298         *            A qualified date param representing the upper date bound (optionally may include time), e.g.
299         *            "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
300         */
301        public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
302                myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null;
303                myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null;
304                validateAndThrowDataFormatExceptionIfInvalid();
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 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 theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
315         */
316        public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
317                myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null;
318                myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null;
319                validateAndThrowDataFormatExceptionIfInvalid();
320        }
321
322        public void setUpperBound(DateParam theUpperBound) {
323                myUpperBound = theUpperBound;
324                validateAndThrowDataFormatExceptionIfInvalid();
325        }
326
327        @Override
328        public void setValuesAsQueryTokens(List<QualifiedParamList> theParameters) throws InvalidRequestException {
329                
330                boolean haveHadUnqualifiedParameter = false;
331                for (QualifiedParamList paramList : theParameters) {
332                        if (paramList.size() == 0) {
333                                continue;
334                        }
335                        if (paramList.size() > 1) {
336                                throw new InvalidRequestException("DateRange parameter does not suppport OR queries");
337                        }
338                        String param = paramList.get(0);
339                        DateParam parsed = new DateParam();
340                        parsed.setValueAsQueryToken(paramList.getQualifier(), param);
341                        addParam(parsed);
342                        
343                        if (parsed.getComparator() == null) {
344                                if (haveHadUnqualifiedParameter) {
345                                        throw new InvalidRequestException("Multiple date parameters with the same name and no qualifier (>, <, etc.) is not supported");                                        
346                                }
347                                haveHadUnqualifiedParameter=true;
348                        }
349                        
350                }
351                
352        }
353
354        @Override
355        public String toString() {
356                StringBuilder b = new StringBuilder();
357                b.append(getClass().getSimpleName());
358                b.append("[");
359                if (haveLowerBound()) {
360                        if (myLowerBound.getComparator() != null) {
361                                b.append(myLowerBound.getComparator().getCode());
362                        }
363                        b.append(myLowerBound.getValueAsString());
364                }
365                if (haveUpperBound()) {
366                        if(haveLowerBound()) {
367                                b.append(" ");
368                        }
369                        if (myUpperBound.getComparator() != null) {
370                                b.append(myUpperBound.getComparator().getCode());
371                        }
372                        b.append(myUpperBound.getValueAsString());
373                } else {
374                        if (!haveLowerBound()) {
375                                b.append("empty");
376                        }
377                }
378                b.append("]");
379                return b.toString();
380        }
381
382        private void validateAndThrowDataFormatExceptionIfInvalid() {
383                boolean haveLowerBound = haveLowerBound();
384                boolean haveUpperBound = haveUpperBound();
385                if (haveLowerBound && haveUpperBound) {
386                        if (myLowerBound.getValue().getTime() > myUpperBound.getValue().getTime()) {
387                                StringBuilder b = new StringBuilder();
388                                b.append("Lower bound of ");
389                                b.append(myLowerBound.getValueAsString());
390                                b.append(" is after upper bound of ");
391                                b.append(myUpperBound.getValueAsString());
392                                throw new DataFormatException(b.toString());
393                        }
394                }
395
396                if (haveLowerBound) {
397                        if (myLowerBound.getComparator() == null) {
398                                myLowerBound.setComparator(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS);
399                        }
400                        switch (myLowerBound.getComparator()) {
401                        case GREATERTHAN:
402                        case GREATERTHAN_OR_EQUALS:
403                        default:
404                                break;
405                        case LESSTHAN:
406                        case LESSTHAN_OR_EQUALS:
407                                throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + myLowerBound.getComparator().getCode());
408                        }
409                }
410
411                if (haveUpperBound) {
412                        if (myUpperBound.getComparator() == null) {
413                                myUpperBound.setComparator(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS);
414                        }
415                        switch (myUpperBound.getComparator()) {
416                        case LESSTHAN:
417                        case LESSTHAN_OR_EQUALS:
418                        default:
419                                break;
420                        case GREATERTHAN:
421                        case GREATERTHAN_OR_EQUALS:
422                                throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + myUpperBound.getComparator().getCode());
423                        }
424                }
425
426        }
427
428}