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