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