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