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