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