001package ca.uhn.fhir.model.primitive;
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 */
022import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.DAY;
023import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.MILLI;
024import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.MONTH;
025import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.SECOND;
026import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.YEAR;
027
028import java.text.ParseException;
029import java.util.ArrayList;
030import java.util.Calendar;
031import java.util.Collections;
032import java.util.Date;
033import java.util.GregorianCalendar;
034import java.util.List;
035import java.util.TimeZone;
036import java.util.regex.Pattern;
037
038import org.apache.commons.lang3.Validate;
039import org.apache.commons.lang3.time.DateUtils;
040import org.apache.commons.lang3.time.FastDateFormat;
041
042import ca.uhn.fhir.model.api.BasePrimitive;
043import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
044import ca.uhn.fhir.parser.DataFormatException;
045
046public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
047
048        /*
049         * Add any new formatters to the static block below!!
050         */
051        private static final List<FastDateFormat> ourFormatters;
052
053        private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}");
054        private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}");
055        private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy");
056        private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd");
057        private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd");
058        private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}");
059        private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss");
060        private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");
061        private static final FastDateFormat ourYearMonthDayTimeMilliUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC"));
062        private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
063        private static final FastDateFormat ourYearMonthDayTimeUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"));
064        private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZZ");
065        private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM");
066        private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM");
067        private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
068        private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
069        private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}");
070        private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}");
071        private static final FastDateFormat ourYearMonthDayTimeMinsFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm");
072        private static final FastDateFormat ourYearMonthDayTimeMinsZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mmZZ");
073
074        static {
075                ArrayList<FastDateFormat> formatters = new ArrayList<FastDateFormat>();
076                formatters.add(ourYearFormat);
077                formatters.add(ourYearMonthDayFormat);
078                formatters.add(ourYearMonthDayNoDashesFormat);
079                formatters.add(ourYearMonthDayTimeFormat);
080                formatters.add(ourYearMonthDayTimeMilliFormat);
081                formatters.add(ourYearMonthDayTimeUTCZFormat);
082                formatters.add(ourYearMonthDayTimeMilliUTCZFormat);
083                formatters.add(ourYearMonthDayTimeMilliZoneFormat);
084                formatters.add(ourYearMonthDayTimeZoneFormat);
085                formatters.add(ourYearMonthFormat);
086                formatters.add(ourYearMonthNoDashesFormat);
087                ourFormatters = Collections.unmodifiableList(formatters);
088        }
089
090        private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND;
091
092        private TimeZone myTimeZone;
093        private boolean myTimeZoneZulu = false;
094        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseDateTimeDt.class);
095
096        /**
097         * Returns a human readable version of this date/time using the system local format.
098         * <p>
099         * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value. For example, if this date object contains the value "2012-01-05T12:00:00-08:00",
100         * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a different time zone. If this behaviour is not what you want, use
101         * {@link #toHumanDisplayLocalTimezone()} instead.
102         * </p>
103         */
104        public String toHumanDisplay() {
105                TimeZone tz = getTimeZone();
106                Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance();
107                value.setTime(getValue());
108
109                switch (getPrecision()) {
110                case YEAR:
111                case MONTH:
112                case DAY:
113                        return ourHumanDateFormat.format(value);
114                case MILLI:
115                case SECOND:
116                default:
117                        return ourHumanDateTimeFormat.format(value);
118                }
119        }
120
121        /**
122         * Returns a human readable version of this date/time using the system local format, converted to the local timezone if neccesary.
123         * 
124         * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
125         */
126        public String toHumanDisplayLocalTimezone() {
127                switch (getPrecision()) {
128                case YEAR:
129                case MONTH:
130                case DAY:
131                        return ourHumanDateFormat.format(getValue());
132                case MILLI:
133                case SECOND:
134                default:
135                        return ourHumanDateTimeFormat.format(getValue());
136                }
137        }
138
139        /**
140         * Constructor
141         */
142        public BaseDateTimeDt() {
143                // nothing
144        }
145
146        /**
147         * Constructor
148         * 
149         * @throws DataFormatException
150         *            If the specified precision is not allowed for this type
151         */
152        public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision) {
153                setValue(theDate, thePrecision);
154                if (isPrecisionAllowed(thePrecision) == false) {
155                        throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
156                }
157        }
158
159        /**
160         * Constructor
161         * 
162         * @throws DataFormatException
163         *            If the specified precision is not allowed for this type
164         */
165        public BaseDateTimeDt(String theString) {
166                setValueAsString(theString);
167                if (isPrecisionAllowed(getPrecision()) == false) {
168                        throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString);
169                }
170        }
171
172        /**
173         * Constructor
174         */
175        public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
176                this(theDate, thePrecision);
177                setTimeZone(theTimeZone);
178        }
179
180        private void clearTimeZone() {
181                myTimeZone = null;
182                myTimeZoneZulu = false;
183        }
184
185        @Override
186        protected String encode(Date theValue) {
187                if (theValue == null) {
188                        return null;
189                } else {
190                        GregorianCalendar cal;
191                        if (myTimeZoneZulu) {
192                                cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
193                        } else if (myTimeZone != null) {
194                                cal = new GregorianCalendar(myTimeZone);
195                        } else {
196                                cal = new GregorianCalendar();
197                        }
198                        cal.setTime(theValue);
199
200                        switch (myPrecision) {
201                        case DAY:
202                                return ourYearMonthDayFormat.format(cal);
203                        case MONTH:
204                                return ourYearMonthFormat.format(cal);
205                        case YEAR:
206                                return ourYearFormat.format(cal);
207                        case MINUTE:
208                                if (myTimeZoneZulu) {
209                                        return ourYearMonthDayTimeMinsFormat.format(cal) + "Z";
210                                } else {
211                                        return ourYearMonthDayTimeMinsZoneFormat.format(cal);
212                                }
213                        case SECOND:
214                                if (myTimeZoneZulu) {
215                                        return ourYearMonthDayTimeFormat.format(cal) + "Z";
216                                } else {
217                                        return ourYearMonthDayTimeZoneFormat.format(cal);
218                                }
219                        case MILLI:
220                                if (myTimeZoneZulu) {
221                                        return ourYearMonthDayTimeMilliFormat.format(cal) + "Z";
222                                } else {
223                                        return ourYearMonthDayTimeMilliZoneFormat.format(cal);
224                                }
225                        }
226                        throw new IllegalStateException("Invalid precision (this is a HAPI bug, shouldn't happen): " + myPrecision);
227                }
228        }
229
230        /**
231         * Returns the default precision for the given datatype
232         */
233        protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
234
235        /**
236         * Gets the precision for this datatype (using the default for the given type if not set)
237         * 
238         * @see #setPrecision(TemporalPrecisionEnum)
239         */
240        public TemporalPrecisionEnum getPrecision() {
241                if (myPrecision == null) {
242                        return getDefaultPrecisionForDatatype();
243                }
244                return myPrecision;
245        }
246
247        /**
248         * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was supplied.
249         */
250        public TimeZone getTimeZone() {
251                return myTimeZone;
252        }
253
254        private boolean hasOffset(String theValue) {
255                boolean inTime = false;
256                for (int i = 0; i < theValue.length(); i++) {
257                        switch (theValue.charAt(i)) {
258                        case 'T':
259                                inTime = true;
260                                break;
261                        case '+':
262                        case '-':
263                                if (inTime) {
264                                        return true;
265                                }
266                                break;
267                        }
268                }
269                return false;
270        }
271
272        /**
273         * To be implemented by subclasses to indicate whether the given precision is allowed by this type
274         */
275        abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
276
277        public boolean isTimeZoneZulu() {
278                return myTimeZoneZulu;
279        }
280
281        /**
282         * Returns <code>true</code> if this object represents a date that is today's date
283         * 
284         * @throws NullPointerException
285         *            if {@link #getValue()} returns <code>null</code>
286         */
287        public boolean isToday() {
288                Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
289                return DateUtils.isSameDay(new Date(), getValue());
290        }
291
292        @Override
293        protected Date parse(String theValue) throws DataFormatException {
294                try {
295                        if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) {
296                                if (!isPrecisionAllowed(YEAR)) {
297                                        ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support YEAR precision): " + theValue);
298                                }
299                                setPrecision(YEAR);
300                                clearTimeZone();
301                                return ((ourYearFormat).parse(theValue));
302                        } else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) {
303                                // Eg. 198401 (allow this just to be lenient)
304                                if (!isPrecisionAllowed(MONTH)) {
305                                        ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue);
306                                }
307                                setPrecision(MONTH);
308                                clearTimeZone();
309                                return ((ourYearMonthNoDashesFormat).parse(theValue));
310                        } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) {
311                                // E.g. 1984-01 (this is valid according to the spec)
312                                if (!isPrecisionAllowed(MONTH)) {
313                                        ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support MONTH precision): " + theValue);
314                                }
315                                setPrecision(MONTH);
316                                clearTimeZone();
317                                return ((ourYearMonthFormat).parse(theValue));
318                        } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) {
319                                // Eg. 19840101 (allow this just to be lenient)
320                                if (!isPrecisionAllowed(DAY)) {
321                                        ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue);
322                                }
323                                setPrecision(DAY);
324                                clearTimeZone();
325                                return ((ourYearMonthDayNoDashesFormat).parse(theValue));
326                        } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) {
327                                // E.g. 1984-01-01 (this is valid according to the spec)
328                                if (!isPrecisionAllowed(DAY)) {
329                                        ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue);
330                                }
331                                setPrecision(DAY);
332                                clearTimeZone();
333                                return ((ourYearMonthDayFormat).parse(theValue));
334                        } else if (theValue.length() >= 18) { // date and time with possible time zone
335                                int dotIndex = theValue.indexOf('.', 18);
336                                boolean hasMillis = dotIndex > -1;
337
338                                if (!hasMillis && !isPrecisionAllowed(SECOND)) {
339                                        ourLog.debug("Invalid date/time string (data type does not support SECONDS precision): " + theValue);
340                                } else if (hasMillis && !isPrecisionAllowed(MILLI)) {
341                                        ourLog.debug("Invalid date/time string (data type " + getClass().getSimpleName() + " does not support MILLIS precision):" + theValue);
342                                }
343
344                                Date retVal;
345                                if (hasMillis) {
346                                        try {
347                                                if (hasOffset(theValue)) {
348                                                        retVal = ourYearMonthDayTimeMilliZoneFormat.parse(theValue);
349                                                } else if (theValue.endsWith("Z")) {
350                                                        retVal = ourYearMonthDayTimeMilliUTCZFormat.parse(theValue);
351                                                } else {
352                                                        retVal = ourYearMonthDayTimeMilliFormat.parse(theValue);
353                                                }
354                                        } catch (ParseException p2) {
355                                                throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
356                                        }
357                                        setTimeZone(theValue, hasMillis);
358                                        setPrecision(TemporalPrecisionEnum.MILLI);
359                                } else {
360                                        try {
361                                                if (hasOffset(theValue)) {
362                                                        retVal = ourYearMonthDayTimeZoneFormat.parse(theValue);
363                                                } else if (theValue.endsWith("Z")) {
364                                                        retVal = ourYearMonthDayTimeUTCZFormat.parse(theValue);
365                                                } else {
366                                                        retVal = ourYearMonthDayTimeFormat.parse(theValue);
367                                                }
368                                        } catch (ParseException p2) {
369                                                throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
370                                        }
371
372                                        setTimeZone(theValue, hasMillis);
373                                        setPrecision(TemporalPrecisionEnum.SECOND);
374                                }
375
376                                return retVal;
377                        } else {
378                                throw new DataFormatException("Invalid date/time string (invalid length): " + theValue);
379                        }
380                } catch (ParseException e) {
381                        throw new DataFormatException("Invalid date string (" + e.getMessage() + "): " + theValue);
382                }
383        }
384
385        /**
386         * Sets the precision for this datatype
387         * 
388         * @throws DataFormatException
389         */
390        public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
391                if (thePrecision == null) {
392                        throw new NullPointerException("Precision may not be null");
393                }
394                myPrecision = thePrecision;
395                updateStringValue();
396        }
397
398        private BaseDateTimeDt setTimeZone(String theValueString, boolean hasMillis) {
399                clearTimeZone();
400                int timeZoneStart = 19;
401                if (hasMillis)
402                        timeZoneStart += 4;
403                if (theValueString.endsWith("Z")) {
404                        setTimeZoneZulu(true);
405                } else if (theValueString.indexOf("GMT", timeZoneStart) != -1) {
406                        setTimeZone(TimeZone.getTimeZone(theValueString.substring(timeZoneStart)));
407                } else if (theValueString.indexOf('+', timeZoneStart) != -1 || theValueString.indexOf('-', timeZoneStart) != -1) {
408                        setTimeZone(TimeZone.getTimeZone("GMT" + theValueString.substring(timeZoneStart)));
409                }
410                return this;
411        }
412
413        public BaseDateTimeDt setTimeZone(TimeZone theTimeZone) {
414                myTimeZone = theTimeZone;
415                updateStringValue();
416                return this;
417        }
418
419        public BaseDateTimeDt setTimeZoneZulu(boolean theTimeZoneZulu) {
420                myTimeZoneZulu = theTimeZoneZulu;
421                updateStringValue();
422                return this;
423        }
424
425        /**
426         * Sets the value for this type using the given Java Date object as the time, and using the default precision for this datatype, as well as the local timezone as determined by the local operating
427         * system. Both of these properties may be modified in subsequent calls if neccesary.
428         */
429        @Override
430        public BaseDateTimeDt setValue(Date theValue) {
431                setValue(theValue, getDefaultPrecisionForDatatype());
432                return this;
433        }
434
435        /**
436         * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as well as the local timezone as determined by the local operating system. Both of
437         * these properties may be modified in subsequent calls if neccesary.
438         * 
439         * @param theValue
440         *           The date value
441         * @param thePrecision
442         *           The precision
443         * @throws DataFormatException
444         */
445        public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
446                setTimeZone(TimeZone.getDefault());
447                myPrecision = thePrecision;
448                super.setValue(theValue);
449        }
450
451        @Override
452        public void setValueAsString(String theValue) throws DataFormatException {
453                clearTimeZone();
454                super.setValueAsString(theValue);
455        }
456
457        /**
458         * For unit tests only
459         */
460        static List<FastDateFormat> getFormatters() {
461                return ourFormatters;
462        }
463
464}