001package org.hl7.fhir.r4.model;
002
003/*-
004 * #%L
005 * org.hl7.fhir.r4
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
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 static org.apache.commons.lang3.StringUtils.isBlank;
024
025import java.util.Calendar;
026import java.util.Date;
027import java.util.GregorianCalendar;
028import java.util.TimeZone;
029
030import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.Validate;
033import org.apache.commons.lang3.time.DateUtils;
034import org.apache.commons.lang3.time.FastDateFormat;
035
036import ca.uhn.fhir.parser.DataFormatException;
037
038public abstract class BaseDateTimeType extends PrimitiveType<Date> {
039
040        static final long NANOS_PER_MILLIS = 1000000L;
041
042        static final long NANOS_PER_SECOND = 1000000000L;
043        private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
044
045        private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
046        private static final long serialVersionUID = 1L;
047
048        private String myFractionalSeconds;
049        private TemporalPrecisionEnum myPrecision = null;
050        private TimeZone myTimeZone;
051        private boolean myTimeZoneZulu = false;
052
053        /**
054         * Constructor
055         */
056        public BaseDateTimeType() {
057                // nothing
058        }
059
060        /**
061         * Constructor
062         *
063         * @throws IllegalArgumentException
064         *            If the specified precision is not allowed for this type
065         */
066        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) {
067                setValue(theDate, thePrecision);
068                if (isPrecisionAllowed(thePrecision) == false) {
069                        throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
070                }
071        }
072
073        /**
074         * Constructor
075         */
076        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
077                this(theDate, thePrecision);
078                setTimeZone(theTimeZone);
079        }
080
081        /**
082         * Constructor
083         *
084         * @throws IllegalArgumentException
085         *            If the specified precision is not allowed for this type
086         */
087        public BaseDateTimeType(String theString) {
088                setValueAsString(theString);
089                if (isPrecisionAllowed(getPrecision()) == false) {
090                        throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString);
091                }
092        }
093
094        /**
095         * Adds the given amount to the field specified by theField
096         *
097         * @param theField
098         *           The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR}
099         * @param theValue
100         *           The number to add (or subtract for a negative number)
101         */
102        public void add(int theField, int theValue) {
103                switch (theField) {
104                case Calendar.YEAR:
105                        setValue(DateUtils.addYears(getValue(), theValue), getPrecision());
106                        break;
107                case Calendar.MONTH:
108                        setValue(DateUtils.addMonths(getValue(), theValue), getPrecision());
109                        break;
110                case Calendar.DATE:
111                        setValue(DateUtils.addDays(getValue(), theValue), getPrecision());
112                        break;
113                case Calendar.HOUR:
114                        setValue(DateUtils.addHours(getValue(), theValue), getPrecision());
115                        break;
116                case Calendar.MINUTE:
117                        setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision());
118                        break;
119                case Calendar.SECOND:
120                        setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision());
121                        break;
122                case Calendar.MILLISECOND:
123                        setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision());
124                        break;
125                default:
126                        throw new DataFormatException("Unknown field constant: " + theField);
127                }
128        }
129
130        /**
131         * Returns <code>true</code> if the given object represents a date/time before <code>this</code> object
132         *
133         * @throws NullPointerException
134         *            If <code>this.getValue()</code> or <code>theDateTimeType.getValue()</code>
135         *            return <code>null</code>
136         */
137        public boolean after(DateTimeType theDateTimeType) {
138                validateBeforeOrAfter(theDateTimeType);
139                return getValue().after(theDateTimeType.getValue());
140        }
141
142        /**
143         * Returns <code>true</code> if the given object represents a date/time before <code>this</code> object
144         *
145         * @throws NullPointerException
146         *            If <code>this.getValue()</code> or <code>theDateTimeType.getValue()</code>
147         *            return <code>null</code>
148         */
149        public boolean before(DateTimeType theDateTimeType) {
150                validateBeforeOrAfter(theDateTimeType);
151                return getValue().before(theDateTimeType.getValue());
152        }
153
154        private void clearTimeZone() {
155                myTimeZone = null;
156                myTimeZoneZulu = false;
157        }
158
159        @Override
160        protected String encode(Date theValue) {
161                if (theValue == null) {
162                        return null;
163                } else {
164                        GregorianCalendar cal;
165                        if (myTimeZoneZulu) {
166                                cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
167                        } else if (myTimeZone != null) {
168                                cal = new GregorianCalendar(myTimeZone);
169                        } else {
170                                cal = new GregorianCalendar();
171                        }
172                        cal.setTime(theValue);
173
174                        StringBuilder b = new StringBuilder();
175                        leftPadWithZeros(cal.get(Calendar.YEAR), 4, b);
176                        if (myPrecision.ordinal() > TemporalPrecisionEnum.YEAR.ordinal()) {
177                                b.append('-');
178                                leftPadWithZeros(cal.get(Calendar.MONTH) + 1, 2, b);
179                                if (myPrecision.ordinal() > TemporalPrecisionEnum.MONTH.ordinal()) {
180                                        b.append('-');
181                                        leftPadWithZeros(cal.get(Calendar.DATE), 2, b);
182                                        if (myPrecision.ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
183                                                b.append('T');
184                                                leftPadWithZeros(cal.get(Calendar.HOUR_OF_DAY), 2, b);
185                                                b.append(':');
186                                                leftPadWithZeros(cal.get(Calendar.MINUTE), 2, b);
187                                                if (myPrecision.ordinal() > TemporalPrecisionEnum.MINUTE.ordinal()) {
188                                                        b.append(':');
189                                                        leftPadWithZeros(cal.get(Calendar.SECOND), 2, b);
190                                                        if (myPrecision.ordinal() > TemporalPrecisionEnum.SECOND.ordinal()) {
191                                                                b.append('.');
192                                                                b.append(myFractionalSeconds);
193                                                                for (int i = myFractionalSeconds.length(); i < 3; i++) {
194                                                                        b.append('0');
195                                                                }
196                                                        }
197                                                }
198
199                                                if (myTimeZoneZulu) {
200                                                        b.append('Z');
201                                                } else if (myTimeZone != null) {
202                                                        int offset = myTimeZone.getOffset(theValue.getTime());
203                                                        if (offset >= 0) {
204                                                                b.append('+');
205                                                        } else {
206                                                                b.append('-');
207                                                                offset = Math.abs(offset);
208                                                        }
209
210                                                        int hoursOffset = (int) (offset / DateUtils.MILLIS_PER_HOUR);
211                                                        leftPadWithZeros(hoursOffset, 2, b);
212                                                        b.append(':');
213                                                        int minutesOffset = (int) (offset % DateUtils.MILLIS_PER_HOUR);
214                                                        minutesOffset = (int) (minutesOffset / DateUtils.MILLIS_PER_MINUTE);
215                                                        leftPadWithZeros(minutesOffset, 2, b);
216                                                }
217                                        }
218                                }
219                        }
220                        return b.toString();
221                }
222        }
223
224        /**
225         * Returns the month with 1-index, e.g. 1=the first day of the month
226         */
227        public Integer getDay() {
228                return getFieldValue(Calendar.DAY_OF_MONTH);
229        }
230
231        /**
232         * Returns the default precision for the given datatype
233         */
234        protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
235
236        private Integer getFieldValue(int theField) {
237                if (getValue() == null) {
238                        return null;
239                }
240                Calendar cal = getValueAsCalendar();
241                return cal.get(theField);
242        }
243
244        /**
245         * Returns the hour of the day in a 24h clock, e.g. 13=1pm
246         */
247        public Integer getHour() {
248                return getFieldValue(Calendar.HOUR_OF_DAY);
249        }
250
251        /**
252         * Returns the milliseconds within the current second.
253         * <p>
254         * Note that this method returns the
255         * same value as {@link #getNanos()} but with less precision.
256         * </p>
257         */
258        public Integer getMillis() {
259                return getFieldValue(Calendar.MILLISECOND);
260        }
261
262        /**
263         * Returns the minute of the hour in the range 0-59
264         */
265        public Integer getMinute() {
266                return getFieldValue(Calendar.MINUTE);
267        }
268
269        /**
270         * Returns the month with 0-index, e.g. 0=January
271         */
272        public Integer getMonth() {
273                return getFieldValue(Calendar.MONTH);
274        }
275
276        /**
277         * Returns the nanoseconds within the current second
278         * <p>
279         * Note that this method returns the
280         * same value as {@link #getMillis()} but with more precision.
281         * </p>
282         */
283        public Long getNanos() {
284                if (isBlank(myFractionalSeconds)) {
285                        return null;
286                }
287                String retVal = StringUtils.rightPad(myFractionalSeconds, 9, '0');
288                retVal = retVal.substring(0, 9);
289                return Long.parseLong(retVal);
290        }
291
292        private int getOffsetIndex(String theValueString) {
293                int plusIndex = theValueString.indexOf('+', 16);
294                int minusIndex = theValueString.indexOf('-', 16);
295                int zIndex = theValueString.indexOf('Z', 16);
296                int retVal = Math.max(Math.max(plusIndex, minusIndex), zIndex);
297                if (retVal == -1) {
298                        return -1;
299                }
300                if ((retVal - 2) != (plusIndex + minusIndex + zIndex)) {
301                        throwBadDateFormat(theValueString);
302                }
303                return retVal;
304        }
305
306        /**
307         * Gets the precision for this datatype (using the default for the given type if not set)
308         *
309         * @see #setPrecision(TemporalPrecisionEnum)
310         */
311        public TemporalPrecisionEnum getPrecision() {
312                if (myPrecision == null) {
313                        return getDefaultPrecisionForDatatype();
314                }
315                return myPrecision;
316        }
317
318        /**
319         * Returns the second of the minute in the range 0-59
320         */
321        public Integer getSecond() {
322                return getFieldValue(Calendar.SECOND);
323        }
324
325        /**
326         * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
327         * supplied.
328         */
329        public TimeZone getTimeZone() {
330                if (myTimeZoneZulu) {
331                        return TimeZone.getTimeZone("GMT");
332                }
333                return myTimeZone;
334        }
335
336        /**
337         * Returns the value of this object as a {@link GregorianCalendar}
338         */
339        public GregorianCalendar getValueAsCalendar() {
340                if (getValue() == null) {
341                        return null;
342                }
343                GregorianCalendar cal;
344                if (getTimeZone() != null) {
345                        cal = new GregorianCalendar(getTimeZone());
346                } else {
347                        cal = new GregorianCalendar();
348                }
349                cal.setTime(getValue());
350                return cal;
351        }
352
353        /**
354         * Returns the year, e.g. 2015
355         */
356        public Integer getYear() {
357                return getFieldValue(Calendar.YEAR);
358        }
359
360        /**
361         * To be implemented by subclasses to indicate whether the given precision is allowed by this type
362         */
363        abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
364
365        /**
366         * Returns true if the timezone is set to GMT-0:00 (Z)
367         */
368        public boolean isTimeZoneZulu() {
369                return myTimeZoneZulu;
370        }
371
372        /**
373         * Returns <code>true</code> if this object represents a date that is today's date
374         *
375         * @throws NullPointerException
376         *            if {@link #getValue()} returns <code>null</code>
377         */
378        public boolean isToday() {
379                Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
380                return DateUtils.isSameDay(new Date(), getValue());
381        }
382
383        private void leftPadWithZeros(int theInteger, int theLength, StringBuilder theTarget) {
384                String string = Integer.toString(theInteger);
385                for (int i = string.length(); i < theLength; i++) {
386                        theTarget.append('0');
387                }
388                theTarget.append(string);
389        }
390
391        @Override
392        protected Date parse(String theValue) throws DataFormatException {
393                Calendar cal = new GregorianCalendar(0, 0, 0);
394                cal.setTimeZone(TimeZone.getDefault());
395                String value = theValue;
396                boolean fractionalSecondsSet = false;
397
398                if (value.length() > 0 && (value.charAt(0) == ' ' || value.charAt(value.length() - 1) == ' ')) {
399                        value = value.trim();
400                }
401
402                int length = value.length();
403                if (length == 0) {
404                        return null;
405                }
406
407                if (length < 4) {
408                        throwBadDateFormat(value);
409                }
410
411                TemporalPrecisionEnum precision = null;
412                cal.set(Calendar.YEAR, parseInt(value, value.substring(0, 4), 0, 9999));
413                precision = TemporalPrecisionEnum.YEAR;
414                if (length > 4) {
415                        validateCharAtIndexIs(value, 4, '-');
416                        validateLengthIsAtLeast(value, 7);
417                        int monthVal = parseInt(value, value.substring(5, 7), 1, 12) - 1;
418                        cal.set(Calendar.MONTH, monthVal);
419                        precision = TemporalPrecisionEnum.MONTH;
420                        if (length > 7) {
421                                validateCharAtIndexIs(value, 7, '-');
422                                validateLengthIsAtLeast(value, 10);
423                                cal.set(Calendar.DATE, 1); // for some reason getActualMaximum works incorrectly if date isn't set
424                                int actualMaximum = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
425                                cal.set(Calendar.DAY_OF_MONTH, parseInt(value, value.substring(8, 10), 1, actualMaximum));
426                                precision = TemporalPrecisionEnum.DAY;
427                                if (length > 10) {
428                                        validateLengthIsAtLeast(value, 17);
429                                        validateCharAtIndexIs(value, 10, 'T'); // yyyy-mm-ddThh:mm:ss
430                                        int offsetIdx = getOffsetIndex(value);
431                                        String time;
432                                        if (offsetIdx == -1) {
433                                                // throwBadDateFormat(theValue);
434                                                // No offset - should this be an error?
435                                                time = value.substring(11);
436                                        } else {
437                                                time = value.substring(11, offsetIdx);
438                                                String offsetString = value.substring(offsetIdx);
439                                                setTimeZone(value, offsetString);
440                                                cal.setTimeZone(getTimeZone());
441                                        }
442                                        int timeLength = time.length();
443
444                                        validateCharAtIndexIs(value, 13, ':');
445                                        cal.set(Calendar.HOUR_OF_DAY, parseInt(value, value.substring(11, 13), 0, 23));
446                                        cal.set(Calendar.MINUTE, parseInt(value, value.substring(14, 16), 0, 59));
447                                        precision = TemporalPrecisionEnum.MINUTE;
448                                        if (timeLength > 5) {
449                                                validateLengthIsAtLeast(value, 19);
450                                                validateCharAtIndexIs(value, 16, ':'); // yyyy-mm-ddThh:mm:ss
451                                                cal.set(Calendar.SECOND, parseInt(value, value.substring(17, 19), 0, 60)); // note: this allows leap seconds
452                                                precision = TemporalPrecisionEnum.SECOND;
453                                                if (timeLength > 8) {
454                                                        validateCharAtIndexIs(value, 19, '.'); // yyyy-mm-ddThh:mm:ss.SSSS
455                                                        validateLengthIsAtLeast(value, 20);
456                                                        int endIndex = getOffsetIndex(value);
457                                                        if (endIndex == -1) {
458                                                                endIndex = value.length();
459                                                        }
460                                                        int millis;
461                                                        String millisString;
462                                                        if (endIndex > 23) {
463                                                                myFractionalSeconds = value.substring(20, endIndex);
464                                                                fractionalSecondsSet = true;
465                                                                endIndex = 23;
466                                                                millisString = value.substring(20, endIndex);
467                                                                millis = parseInt(value, millisString, 0, 999);
468                                                        } else {
469                                                                millisString = value.substring(20, endIndex);
470                                                                millis = parseInt(value, millisString, 0, 999);
471                                                                myFractionalSeconds = millisString;
472                                                                fractionalSecondsSet = true;
473                                                        }
474                                                        if (millisString.length() == 1) {
475                                                                millis = millis * 100;
476                                                        } else if (millisString.length() == 2) {
477                                                                millis = millis * 10;
478                                                        }
479                                                        cal.set(Calendar.MILLISECOND, millis);
480                                                        precision = TemporalPrecisionEnum.MILLI;
481                                                }
482                                        }
483                                }
484                        } else {
485                                cal.set(Calendar.DATE, 1);
486                        }
487                } else {
488                        cal.set(Calendar.DATE, 1);
489                }
490
491                if (fractionalSecondsSet == false) {
492                        myFractionalSeconds = "";
493                }
494
495                myPrecision = precision;
496                return cal.getTime();
497
498        }
499
500        private int parseInt(String theValue, String theSubstring, int theLowerBound, int theUpperBound) {
501                int retVal = 0;
502                try {
503                        retVal = Integer.parseInt(theSubstring);
504                } catch (NumberFormatException e) {
505                        throwBadDateFormat(theValue);
506                }
507
508                if (retVal < theLowerBound || retVal > theUpperBound) {
509                        throwBadDateFormat(theValue);
510                }
511
512                return retVal;
513        }
514
515        /**
516         * Sets the month with 1-index, e.g. 1=the first day of the month
517         */
518        public BaseDateTimeType setDay(int theDay) {
519                setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31);
520                return this;
521        }
522
523        private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) {
524                validateValueInRange(theValue, theMinimum, theMaximum);
525                Calendar cal;
526                if (getValue() == null) {
527                        cal = new GregorianCalendar(0, 0, 0);
528                } else {
529                        cal = getValueAsCalendar();
530                }
531                if (theField != -1) {
532                        cal.set(theField, theValue);
533                }
534                if (theFractionalSeconds != null) {
535                        myFractionalSeconds = theFractionalSeconds;
536                } else if (theField == Calendar.MILLISECOND) {
537                        myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0');
538                }
539                super.setValue(cal.getTime());
540        }
541
542        /**
543         * Sets the hour of the day in a 24h clock, e.g. 13=1pm
544         */
545        public BaseDateTimeType setHour(int theHour) {
546                setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23);
547                return this;
548        }
549
550        /**
551         * Sets the milliseconds within the current second.
552         * <p>
553         * Note that this method sets the
554         * same value as {@link #setNanos(long)} but with less precision.
555         * </p>
556         */
557        public BaseDateTimeType setMillis(int theMillis) {
558                setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999);
559                return this;
560        }
561
562        /**
563         * Sets the minute of the hour in the range 0-59
564         */
565        public BaseDateTimeType setMinute(int theMinute) {
566                setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59);
567                return this;
568        }
569
570        /**
571         * Sets the month with 0-index, e.g. 0=January
572         */
573        public BaseDateTimeType setMonth(int theMonth) {
574                setFieldValue(Calendar.MONTH, theMonth, null, 0, 11);
575                return this;
576        }
577
578        /**
579         * Sets the nanoseconds within the current second
580         * <p>
581         * Note that this method sets the
582         * same value as {@link #setMillis(int)} but with more precision.
583         * </p>
584         */
585        public BaseDateTimeType setNanos(long theNanos) {
586                validateValueInRange(theNanos, 0, NANOS_PER_SECOND - 1);
587                String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0');
588
589                // Strip trailing 0s
590                for (int i = fractionalSeconds.length(); i > 0; i--) {
591                        if (fractionalSeconds.charAt(i - 1) != '0') {
592                                fractionalSeconds = fractionalSeconds.substring(0, i);
593                                break;
594                        }
595                }
596                int millis = (int) (theNanos / NANOS_PER_MILLIS);
597                setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999);
598                return this;
599        }
600
601        /**
602         * Sets the precision for this datatype
603         *
604         * @throws DataFormatException
605         */
606        public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
607                if (thePrecision == null) {
608                        throw new NullPointerException("Precision may not be null");
609                }
610                myPrecision = thePrecision;
611                updateStringValue();
612        }
613
614        /**
615         * Sets the second of the minute in the range 0-59
616         */
617        public BaseDateTimeType setSecond(int theSecond) {
618                setFieldValue(Calendar.SECOND, theSecond, null, 0, 59);
619                return this;
620        }
621
622        private BaseDateTimeType setTimeZone(String theWholeValue, String theValue) {
623
624                if (isBlank(theValue)) {
625                        throwBadDateFormat(theWholeValue);
626                } else if (theValue.charAt(0) == 'Z') {
627                        myTimeZone = null;
628                        myTimeZoneZulu = true;
629                } else if (theValue.length() != 6) {
630                        throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
631                } else if (theValue.charAt(3) != ':' || !(theValue.charAt(0) == '+' || theValue.charAt(0) == '-')) {
632                        throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
633                } else {
634                        parseInt(theWholeValue, theValue.substring(1, 3), 0, 23);
635                        parseInt(theWholeValue, theValue.substring(4, 6), 0, 59);
636                        myTimeZoneZulu = false;
637                        myTimeZone = TimeZone.getTimeZone("GMT" + theValue);
638                }
639
640                return this;
641        }
642
643        public BaseDateTimeType setTimeZone(TimeZone theTimeZone) {
644                myTimeZone = theTimeZone;
645                myTimeZoneZulu = false;
646                updateStringValue();
647                return this;
648        }
649
650        public BaseDateTimeType setTimeZoneZulu(boolean theTimeZoneZulu) {
651                myTimeZoneZulu = theTimeZoneZulu;
652                myTimeZone = null;
653                updateStringValue();
654                return this;
655        }
656
657        /**
658         * Sets the value for this type using the given Java Date object as the time, and using the default precision for
659         * this datatype (unless the precision is already set), as well as the local timezone as determined by the local operating
660         * system. Both of these properties may be modified in subsequent calls if neccesary.
661         */
662        @Override
663        public BaseDateTimeType setValue(Date theValue) {
664                setValue(theValue, getPrecision());
665                return this;
666        }
667
668        /**
669         * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as
670         * well as the local timezone as determined by the local operating system. Both of
671         * these properties may be modified in subsequent calls if neccesary.
672         *
673         * @param theValue
674         *           The date value
675         * @param thePrecision
676         *           The precision
677         * @throws DataFormatException
678         */
679        public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
680                if (getTimeZone() == null) {
681                        setTimeZone(TimeZone.getDefault());
682                }
683                myPrecision = thePrecision;
684                myFractionalSeconds = "";
685                if (theValue != null) {
686                        long millis = theValue.getTime() % 1000;
687                        if (millis < 0) {
688                                // This is for times before 1970 (see bug #444)
689                                millis = 1000 + millis;
690                        }
691                        String fractionalSeconds = Integer.toString((int) millis);
692                        myFractionalSeconds = StringUtils.leftPad(fractionalSeconds, 3, '0');
693                }
694                super.setValue(theValue);
695        }
696
697        @Override
698        public void setValueAsString(String theValue) throws DataFormatException {
699                clearTimeZone();
700                super.setValueAsString(theValue);
701        }
702
703        protected void setValueAsV3String(String theV3String) {
704                if (StringUtils.isBlank(theV3String)) {
705                        setValue(null);
706                } else {
707                        StringBuilder b = new StringBuilder();
708                        String timeZone = null;
709                        for (int i = 0; i < theV3String.length(); i++) {
710                                char nextChar = theV3String.charAt(i);
711                                if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
712                                        timeZone = (theV3String.substring(i));
713                                        break;
714                                }
715
716                                // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
717                                if (i == 4 || i == 6) {
718                                        b.append('-');
719                                } else if (i == 8) {
720                                        b.append('T');
721                                } else if (i == 10 || i == 12) {
722                                        b.append(':');
723                                }
724
725                                b.append(nextChar);
726                        }
727
728      if (b.length() == 13)
729        b.append(":00"); // schema rule, must have minutes
730                        if (b.length() == 16)
731                                b.append(":00"); // schema rule, must have seconds
732                        if (timeZone != null && b.length() > 10) {
733                                if (timeZone.length() == 5) {
734                                        b.append(timeZone.substring(0, 3));
735                                        b.append(':');
736                                        b.append(timeZone.substring(3));
737                                } else {
738                                        b.append(timeZone);
739                                }
740                        }
741
742                        setValueAsString(b.toString());
743                }
744        }
745
746        /**
747         * Sets the year, e.g. 2015
748         */
749        public BaseDateTimeType setYear(int theYear) {
750                setFieldValue(Calendar.YEAR, theYear, null, 0, 9999);
751                return this;
752        }
753
754        private void throwBadDateFormat(String theValue) {
755                throw new DataFormatException("Invalid date/time format: \"" + theValue + "\"");
756        }
757
758        private void throwBadDateFormat(String theValue, String theMesssage) {
759                throw new DataFormatException("Invalid date/time format: \"" + theValue + "\": " + theMesssage);
760        }
761
762        /**
763         * Returns a view of this date/time as a Calendar object. Note that the returned
764         * Calendar object is entirely independent from <code>this</code> object. Changes to the
765         * calendar will not affect <code>this</code>.
766         */
767        public Calendar toCalendar() {
768                Calendar retVal = Calendar.getInstance();
769                retVal.setTime(getValue());
770                retVal.setTimeZone(getTimeZone());
771                return retVal;
772        }
773
774        /**
775         * Returns a human readable version of this date/time using the system local format.
776         * <p>
777         * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value.
778         * For example, if this date object contains the value "2012-01-05T12:00:00-08:00",
779         * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a
780         * different time zone. If this behaviour is not what you want, use
781         * {@link #toHumanDisplayLocalTimezone()} instead.
782         * </p>
783         */
784        public String toHumanDisplay() {
785                TimeZone tz = getTimeZone();
786                Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance();
787                value.setTime(getValue());
788
789                switch (getPrecision()) {
790                case YEAR:
791                case MONTH:
792                case DAY:
793                        return ourHumanDateFormat.format(value);
794                case MILLI:
795                case SECOND:
796                default:
797                        return ourHumanDateTimeFormat.format(value);
798                }
799        }
800
801        /**
802         * Returns a human readable version of this date/time using the system local format, converted to the local timezone
803         * if neccesary.
804         *
805         * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
806         */
807        public String toHumanDisplayLocalTimezone() {
808                switch (getPrecision()) {
809                case YEAR:
810                case MONTH:
811                case DAY:
812                        return ourHumanDateFormat.format(getValue());
813                case MILLI:
814                case SECOND:
815                default:
816                        return ourHumanDateTimeFormat.format(getValue());
817                }
818        }
819
820        private void validateBeforeOrAfter(DateTimeType theDateTimeType) {
821                if (getValue() == null) {
822                        throw new NullPointerException("This BaseDateTimeType does not contain a value (getValue() returns null)");
823                }
824                if (theDateTimeType == null) {
825                        throw new NullPointerException("theDateTimeType must not be null");
826                }
827                if (theDateTimeType.getValue() == null) {
828                        throw new NullPointerException("The given BaseDateTimeType does not contain a value (theDateTimeType.getValue() returns null)");
829                }
830        }
831
832        private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) {
833                if (theValue.charAt(theIndex) != theChar) {
834                        throwBadDateFormat(theValue, "Expected character '" + theChar + "' at index " + theIndex + " but found " + theValue.charAt(theIndex));
835                }
836        }
837
838        private void validateLengthIsAtLeast(String theValue, int theLength) {
839                if (theValue.length() < theLength) {
840                        throwBadDateFormat(theValue);
841                }
842        }
843
844        private void validateValueInRange(long theValue, long theMinimum, long theMaximum) {
845                if (theValue < theMinimum || theValue > theMaximum) {
846                        throw new IllegalArgumentException("Value " + theValue + " is not between allowable range: " + theMinimum + " - " + theMaximum);
847                }
848        }
849
850        @Override
851   public boolean isDateTime() {
852          return true;
853        }
854
855  @Override
856  public BaseDateTimeType dateTimeValue() {
857    return this;
858  }
859
860  public boolean hasTime() {
861    return (myPrecision == TemporalPrecisionEnum.MINUTE || myPrecision == TemporalPrecisionEnum.SECOND || myPrecision == TemporalPrecisionEnum.MILLI);
862  }
863
864        /**
865         * This method implements a datetime equality check using the rules as defined by FHIRPath.
866         *
867         * This method returns:
868         * <ul>
869         *     <li>true if the given datetimes represent the exact same instant with the same precision (irrespective of the timezone)</li>
870         *     <li>true if the given datetimes represent the exact same instant but one includes milliseconds of <code>.[0]+</code> while the other includes only SECONDS precision (irrespecitve of the timezone)</li>
871         *     <li>true if the given datetimes represent the exact same year/year-month/year-month-date (if both operands have the same precision)</li>
872    *     <li>false if both datetimes have equal precision of MINUTE or greater, one has no timezone specified but the other does, and could not represent the same instant in any timezone</li>
873    *     <li>null if both datetimes have equal precision of MINUTE or greater, one has no timezone specified but the other does, and could potentially represent the same instant in any timezone</li>
874    *     <li>false if the given datetimes have the same precision but do not represent the same instant (irrespective of timezone)</li>
875         *     <li>null otherwise (since these datetimes are not comparable)</li>
876         * </ul>
877         */
878        public Boolean equalsUsingFhirPathRules(BaseDateTimeType theOther) {
879
880     BaseDateTimeType me = this;
881
882     // Per FHIRPath rules, we compare equivalence at the lowest precision of the two values,
883     // so if we need to, we'll clone either side and reduce its precision
884     int lowestPrecision = Math.min(me.getPrecision().ordinal(), theOther.getPrecision().ordinal());
885     TemporalPrecisionEnum lowestPrecisionEnum = TemporalPrecisionEnum.values()[lowestPrecision];
886     if (me.getPrecision() != lowestPrecisionEnum) {
887       me = new DateTimeType(me.getValueAsString());
888       me.setPrecision(lowestPrecisionEnum);
889     }
890     if (theOther.getPrecision() != lowestPrecisionEnum) {
891       theOther = new DateTimeType(theOther.getValueAsString());
892       theOther.setPrecision(lowestPrecisionEnum);
893     }
894
895                if (me.hasTimezoneIfRequired() != theOther.hasTimezoneIfRequired()) {
896                        if (me.getPrecision() == theOther.getPrecision()) {
897                                if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.MINUTE.ordinal() && theOther.getPrecision().ordinal() >= TemporalPrecisionEnum.MINUTE.ordinal()) {
898                    boolean couldBeTheSameTime = couldBeTheSameTime(me, theOther) || couldBeTheSameTime(theOther, me);
899                    if (!couldBeTheSameTime) {
900                        return false;
901                    }
902                                }
903                        }
904                        return null;
905                }
906
907                // Same precision
908                if (me.getPrecision() == theOther.getPrecision()) {
909        if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.MINUTE.ordinal()) {
910          long leftTime = me.getValue().getTime();
911          long rightTime = theOther.getValue().getTime();
912          return leftTime == rightTime;
913        } else {
914          String leftTime = me.getValueAsString();
915          String rightTime = theOther.getValueAsString();
916          return leftTime.equals(rightTime);
917        }
918                }
919
920                // Both represent 0 millis but the millis are optional
921                if (((Integer)0).equals(me.getMillis())) {
922                        if (((Integer)0).equals(theOther.getMillis())) {
923                                if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.SECOND.ordinal()) {
924                                        if (theOther.getPrecision().ordinal() >= TemporalPrecisionEnum.SECOND.ordinal()) {
925                                                return me.getValue().getTime() == theOther.getValue().getTime();
926                                        }
927                                }
928                        }
929                }
930
931                return false;
932        }
933
934    private boolean couldBeTheSameTime(BaseDateTimeType theArg1, BaseDateTimeType theArg2) {
935        boolean theCouldBeTheSameTime = false;
936        if (theArg1.getTimeZone() == null && theArg2.getTimeZone() != null) {
937            long lowLeft = new DateTimeType(theArg1.getValueAsString()+"Z").getValue().getTime() - (14 * DateUtils.MILLIS_PER_HOUR);
938            long highLeft = new DateTimeType(theArg1.getValueAsString()+"Z").getValue().getTime() + (14 * DateUtils.MILLIS_PER_HOUR);
939            long right = theArg2.getValue().getTime();
940            if (right >= lowLeft && right <= highLeft) {
941                theCouldBeTheSameTime = true;
942            }
943        }
944        return theCouldBeTheSameTime;
945    }
946
947    boolean hasTimezoneIfRequired() {
948                return getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal() ||
949                                getTimeZone() != null;
950        }
951
952
953}