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