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