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