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