001package org.hl7.fhir.dstu2.model;
002
003/*-
004 * #%L
005 * org.hl7.fhir.dstu2
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
024import org.apache.commons.lang3.StringUtils;
025import org.apache.commons.lang3.Validate;
026import org.apache.commons.lang3.time.DateUtils;
027import org.apache.commons.lang3.time.FastDateFormat;
028import org.hl7.fhir.utilities.DateTimeUtil;
029
030import java.text.ParseException;
031import java.util.*;
032import java.util.regex.Pattern;
033
034import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.*;
035
036public abstract class BaseDateTimeType extends PrimitiveType<Date> {
037
038        private static final long serialVersionUID = 1L;
039
040        /*
041         * Add any new formatters to the static block below!!
042         */
043        private static final List<FastDateFormat> ourFormatters;
044
045        private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}");
046        private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}");
047        private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy");
048        private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd");
049        private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd");
050        private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}");
051        private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss");
052        private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");
053        private static final FastDateFormat ourYearMonthDayTimeMilliUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC"));
054        private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
055        private static final FastDateFormat ourYearMonthDayTimeUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"));
056        private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZZ");
057        private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM");
058        private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM");
059        private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}");
060        private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}");
061        private static final FastDateFormat ourYearMonthDayTimeMinsFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm");
062        private static final FastDateFormat ourYearMonthDayTimeMinsUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC"));
063        private static final FastDateFormat ourYearMonthDayTimeMinsZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mmZZ");
064
065        private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
066        private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
067
068        static {
069                ArrayList<FastDateFormat> formatters = new ArrayList<FastDateFormat>();
070                formatters.add(ourYearFormat);
071                formatters.add(ourYearMonthDayFormat);
072                formatters.add(ourYearMonthDayNoDashesFormat);
073                formatters.add(ourYearMonthDayTimeFormat);
074                formatters.add(ourYearMonthDayTimeUTCZFormat);
075                formatters.add(ourYearMonthDayTimeZoneFormat);
076                formatters.add(ourYearMonthDayTimeMilliFormat);
077                formatters.add(ourYearMonthDayTimeMilliUTCZFormat);
078                formatters.add(ourYearMonthDayTimeMilliZoneFormat);
079                formatters.add(ourYearMonthDayTimeMinsFormat);
080                formatters.add(ourYearMonthDayTimeMinsUTCZFormat);
081                formatters.add(ourYearMonthDayTimeMinsZoneFormat);
082                formatters.add(ourYearMonthFormat);
083                formatters.add(ourYearMonthNoDashesFormat);
084                ourFormatters = Collections.unmodifiableList(formatters);
085        }
086
087        private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND;
088
089        private TimeZone myTimeZone;
090        private boolean myTimeZoneZulu = false;
091
092        /**
093         * Constructor
094         */
095        public BaseDateTimeType() {
096                // nothing
097        }
098
099        /**
100         * Constructor
101         * 
102         * @throws IllegalArgumentException
103         *             If the specified precision is not allowed for this type
104         */
105        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) {
106                setValue(theDate, thePrecision);
107                if (isPrecisionAllowed(thePrecision) == false) {
108                        throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
109                }
110        }
111
112        /**
113         * Constructor
114         * 
115         * @throws IllegalArgumentException
116         *             If the specified precision is not allowed for this type
117         */
118        public BaseDateTimeType(String theString) {
119                setValueAsString(theString);
120                if (isPrecisionAllowed(getPrecision()) == false) {
121                        throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString);
122                }
123        }
124
125        /**
126         * Constructor
127         */
128        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
129                this(theDate, thePrecision);
130                setTimeZone(theTimeZone);
131        }
132
133        private void clearTimeZone() {
134                myTimeZone = null;
135                myTimeZoneZulu = false;
136        }
137
138        @Override
139        protected String encode(Date theValue) {
140                if (theValue == null) {
141                        return null;
142                } else {
143                        switch (myPrecision) {
144                        case DAY:
145                                return ourYearMonthDayFormat.format(theValue);
146                        case MONTH:
147                                return ourYearMonthFormat.format(theValue);
148                        case YEAR:
149                                return ourYearFormat.format(theValue);
150                        case MINUTE:
151                                if (myTimeZoneZulu) {
152                                        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
153                                        cal.setTime(theValue);
154                                        return ourYearMonthDayTimeMinsFormat.format(cal) + "Z";
155                                } else if (myTimeZone != null) {
156                                        GregorianCalendar cal = new GregorianCalendar(myTimeZone);
157                                        cal.setTime(theValue);
158                                        return (ourYearMonthDayTimeMinsZoneFormat.format(cal));
159                                } else {
160                                        return ourYearMonthDayTimeMinsFormat.format(theValue);
161                                }
162                        case SECOND:
163                                if (myTimeZoneZulu) {
164                                        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
165                                        cal.setTime(theValue);
166                                        return ourYearMonthDayTimeFormat.format(cal) + "Z";
167                                } else if (myTimeZone != null) {
168                                        GregorianCalendar cal = new GregorianCalendar(myTimeZone);
169                                        cal.setTime(theValue);
170                                        return (ourYearMonthDayTimeZoneFormat.format(cal));
171                                } else {
172                                        return ourYearMonthDayTimeFormat.format(theValue);
173                                }
174                        case MILLI:
175                                if (myTimeZoneZulu) {
176                                        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
177                                        cal.setTime(theValue);
178                                        return ourYearMonthDayTimeMilliFormat.format(cal) + "Z";
179                                } else if (myTimeZone != null) {
180                                        GregorianCalendar cal = new GregorianCalendar(myTimeZone);
181                                        cal.setTime(theValue);
182                                        return (ourYearMonthDayTimeMilliZoneFormat.format(cal));
183                                } else {
184                                        return ourYearMonthDayTimeMilliFormat.format(theValue);
185                                }
186                        }
187                        throw new IllegalStateException("Invalid precision (this is a bug, shouldn't happen https://xkcd.com/2200/): " + myPrecision);
188                }
189        }
190
191        /**
192         * Returns the default precision for the given datatype
193         */
194        protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
195
196        /**
197         * Gets the precision for this datatype (using the default for the given type if not set)
198         * 
199         * @see #setPrecision(TemporalPrecisionEnum)
200         */
201        public TemporalPrecisionEnum getPrecision() {
202                if (myPrecision == null) {
203                        return getDefaultPrecisionForDatatype();
204                }
205                return myPrecision;
206        }
207
208        /**
209         * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
210         * supplied.
211         */
212        public TimeZone getTimeZone() {
213                return myTimeZone;
214        }
215
216        private boolean hasOffset(String theValue) {
217                boolean inTime = false;
218                for (int i = 0; i < theValue.length(); i++) {
219                        switch (theValue.charAt(i)) {
220                        case 'T':
221                                inTime = true;
222                                break;
223                        case '+':
224                        case '-':
225                                if (inTime) {
226                                        return true;
227                                }
228                                break;
229                        }
230                }
231                return false;
232        }
233
234        /**
235         * To be implemented by subclasses to indicate whether the given precision is allowed by this type
236         */
237        abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
238
239        public boolean isTimeZoneZulu() {
240                return myTimeZoneZulu;
241        }
242
243        /**
244         * Returns <code>true</code> if this object represents a date that is today's date
245         * 
246         * @throws NullPointerException
247         *             if {@link #getValue()} returns <code>null</code>
248         */
249        public boolean isToday() {
250                Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
251                return DateUtils.isSameDay(new Date(), getValue());
252        }
253
254        @Override
255        protected Date parse(String theValue) throws IllegalArgumentException {
256                try {
257                        if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) {
258                                if (!isPrecisionAllowed(YEAR)) {
259                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
260                                        // " does not support YEAR precision): " + theValue);
261                                }
262                                setPrecision(YEAR);
263                                clearTimeZone();
264                                return ((ourYearFormat).parse(theValue));
265                        } else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) {
266                                // Eg. 198401 (allow this just to be lenient)
267                                if (!isPrecisionAllowed(MONTH)) {
268                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
269                                        // " does not support DAY precision): " + theValue);
270                                }
271                                setPrecision(MONTH);
272                                clearTimeZone();
273                                return ((ourYearMonthNoDashesFormat).parse(theValue));
274                        } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) {
275                                // E.g. 1984-01 (this is valid according to the spec)
276                                if (!isPrecisionAllowed(MONTH)) {
277                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
278                                        // " does not support MONTH precision): " + theValue);
279                                }
280                                setPrecision(MONTH);
281                                clearTimeZone();
282                                return ((ourYearMonthFormat).parse(theValue));
283                        } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) {
284                                // Eg. 19840101 (allow this just to be lenient)
285                                if (!isPrecisionAllowed(DAY)) {
286                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
287                                        // " does not support DAY precision): " + theValue);
288                                }
289                                setPrecision(DAY);
290                                clearTimeZone();
291                                return ((ourYearMonthDayNoDashesFormat).parse(theValue));
292                        } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) {
293                                // E.g. 1984-01-01 (this is valid according to the spec)
294                                if (!isPrecisionAllowed(DAY)) {
295                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
296                                        // " does not support DAY precision): " + theValue);
297                                }
298                                setPrecision(DAY);
299                                clearTimeZone();
300                                return ((ourYearMonthDayFormat).parse(theValue));
301                        } else if (theValue.length() >= 16) { // date and time with possible time zone
302                                int firstColonIndex = theValue.indexOf(':');
303                                if (firstColonIndex == -1) {
304                                        throw new IllegalArgumentException("Invalid date/time string: " + theValue);
305                                }
306                                
307                                boolean hasSeconds = theValue.length() > firstColonIndex+3 ? theValue.charAt(firstColonIndex+3) == ':' : false; 
308                                
309                                int dotIndex = theValue.length() >= 18 ? theValue.indexOf('.', 18): -1;
310                                boolean hasMillis = dotIndex > -1;
311
312//                              if (!hasMillis && !isPrecisionAllowed(SECOND)) {
313                                        // ourLog.debug("Invalid date/time string (data type does not support SECONDS precision): " +
314                                        // theValue);
315//                              } else if (hasMillis && !isPrecisionAllowed(MILLI)) {
316                                        // ourLog.debug("Invalid date/time string (data type " + getClass().getSimpleName() +
317                                        // " does not support MILLIS precision):" + theValue);
318//                              }
319
320                                Date retVal;
321                                if (hasMillis) {
322                                        try {
323                                                if (hasOffset(theValue)) {
324                                                        retVal = ourYearMonthDayTimeMilliZoneFormat.parse(theValue);
325                                                } else if (theValue.endsWith("Z")) {
326                                                        retVal = ourYearMonthDayTimeMilliUTCZFormat.parse(theValue);
327                                                } else {
328                                                        retVal = ourYearMonthDayTimeMilliFormat.parse(theValue);
329                                                }
330                                        } catch (ParseException p2) {
331                                                throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
332                                        }
333                                        setTimeZone(theValue, hasMillis);
334                                        setPrecision(TemporalPrecisionEnum.MILLI);
335                                } else if (hasSeconds) {
336                                        try {
337                                                if (hasOffset(theValue)) {
338                                                        retVal = ourYearMonthDayTimeZoneFormat.parse(theValue);
339                                                } else if (theValue.endsWith("Z")) {
340                                                        retVal = ourYearMonthDayTimeUTCZFormat.parse(theValue);
341                                                } else {
342                                                        retVal = ourYearMonthDayTimeFormat.parse(theValue);
343                                                }
344                                        } catch (ParseException p2) {
345                                                throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
346                                        }
347
348                                        setTimeZone(theValue, hasMillis);
349                                        setPrecision(TemporalPrecisionEnum.SECOND);
350                                } else {
351                                        try {
352                                                if (hasOffset(theValue)) {
353                                                        retVal = ourYearMonthDayTimeMinsZoneFormat.parse(theValue);
354                                                } else if (theValue.endsWith("Z")) {
355                                                        retVal = ourYearMonthDayTimeMinsUTCZFormat.parse(theValue);
356                                                } else {
357                                                        retVal = ourYearMonthDayTimeMinsFormat.parse(theValue);
358                                                }
359                                        } catch (ParseException p2) {
360                                                throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue, p2);
361                                        }
362
363                                        setTimeZone(theValue, hasMillis);
364                                        setPrecision(TemporalPrecisionEnum.MINUTE);
365                                }
366
367                                return retVal;
368                        } else {
369                                throw new IllegalArgumentException("Invalid date/time string (invalid length): " + theValue);
370                        }
371                } catch (ParseException e) {
372                        throw new IllegalArgumentException("Invalid date string (" + e.getMessage() + "): " + theValue);
373                }
374        }
375
376        /**
377         * Sets the precision for this datatype using field values from {@link Calendar}. Valid values are:
378         * <ul>
379         * <li>{@link Calendar#SECOND}
380         * <li>{@link Calendar#DAY_OF_MONTH}
381         * <li>{@link Calendar#MONTH}
382         * <li>{@link Calendar#YEAR}
383         * </ul>
384         * 
385         * @throws IllegalArgumentException
386         */
387        public void setPrecision(TemporalPrecisionEnum thePrecision) throws IllegalArgumentException {
388                if (thePrecision == null) {
389                        throw new NullPointerException("Precision may not be null");
390                }
391                myPrecision = thePrecision;
392                updateStringValue();
393        }
394
395        private void setTimeZone(String theValueString, boolean hasMillis) {
396                clearTimeZone();
397                int timeZoneStart = 19;
398                if (hasMillis)
399                        timeZoneStart += 4;
400                if (theValueString.endsWith("Z")) {
401                        setTimeZoneZulu(true);
402                } else if (theValueString.indexOf("GMT", timeZoneStart) != -1) {
403                        setTimeZone(TimeZone.getTimeZone(theValueString.substring(timeZoneStart)));
404                } else if (theValueString.indexOf('+', timeZoneStart) != -1 || theValueString.indexOf('-', timeZoneStart) != -1) {
405                        setTimeZone(TimeZone.getTimeZone("GMT" + theValueString.substring(timeZoneStart)));
406                }
407        }
408
409        public void setTimeZone(TimeZone theTimeZone) {
410                myTimeZone = theTimeZone;
411                updateStringValue();
412        }
413
414        public void setTimeZoneZulu(boolean theTimeZoneZulu) {
415                myTimeZoneZulu = theTimeZoneZulu;
416                updateStringValue();
417        }
418
419        /**
420         * Sets the value of this date/time using the default level of precision
421         * for this datatype
422         * using the system local time zone
423         * 
424         * @param theValue
425         *            The date value
426         */
427        @Override
428        public BaseDateTimeType setValue(Date theValue) {
429                if (myTimeZoneZulu == false && myTimeZone == null) {
430                        myTimeZone = TimeZone.getDefault();
431                }
432                myPrecision = getDefaultPrecisionForDatatype();
433                BaseDateTimeType retVal = (BaseDateTimeType) super.setValue(theValue);
434                return retVal;
435        }
436
437        /**
438         * Sets the value of this date/time using the specified level of precision
439         * using the system local time zone
440         * 
441         * @param theValue
442         *            The date value
443         * @param thePrecision
444         *            The precision
445         * @throws IllegalArgumentException
446         */
447        public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws IllegalArgumentException {
448                if (myTimeZoneZulu == false && myTimeZone == null) {
449                        myTimeZone = TimeZone.getDefault();
450                }
451                myPrecision = thePrecision;
452                super.setValue(theValue);
453        }
454
455        @Override
456        public void setValueAsString(String theValue) throws IllegalArgumentException {
457                clearTimeZone();
458                super.setValueAsString(theValue);
459        }
460
461        /**
462         * For unit tests only
463         */
464        static List<FastDateFormat> getFormatters() {
465                return ourFormatters;
466        }
467
468        public boolean before(DateTimeType theDateTimeType) {
469                return getValue().before(theDateTimeType.getValue());
470        }
471
472        public boolean after(DateTimeType theDateTimeType) {
473                return getValue().after(theDateTimeType.getValue());
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    return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString());
488  }
489
490  /**
491   * Returns a human readable version of this date/time using the system local format, converted to the local timezone
492   * if neccesary.
493   *
494   * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
495   */
496  public String toHumanDisplayLocalTimezone() {
497    return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString());
498  }
499
500
501        /**
502         * Returns a view of this date/time as a Calendar object
503         */
504        public Calendar toCalendar() {
505                Calendar retVal = Calendar.getInstance();
506                retVal.setTime(getValue());
507                retVal.setTimeZone(getTimeZone());
508                return retVal;
509        }
510
511        /**
512         * Sets the TimeZone offset in minutes relative to GMT
513         */
514        public void setOffsetMinutes(int theZoneOffsetMinutes) {
515                int offsetAbs = Math.abs(theZoneOffsetMinutes);
516
517                int mins = offsetAbs % 60;
518                int hours = offsetAbs / 60;
519
520                if (theZoneOffsetMinutes < 0) {
521                        setTimeZone(TimeZone.getTimeZone("GMT-" + hours + ":" + mins));
522                } else {
523                        setTimeZone(TimeZone.getTimeZone("GMT+" + hours + ":" + mins));
524                }
525        }
526
527        /**
528         * Returns the time in millis as represented by this Date/Time
529         */
530        public long getTime() {
531                return getValue().getTime();
532        }
533
534        /**
535         * Adds the given amount to the field specified by theField
536         * 
537         * @param theField
538         *            The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR}
539         * @param theValue
540         *            The number to add (or subtract for a negative number)
541         */
542        public void add(int theField, int theValue) {
543                switch (theField) {
544                case Calendar.YEAR:
545                        setValue(DateUtils.addYears(getValue(), theValue), getPrecision());
546                        break;
547                case Calendar.MONTH:
548                        setValue(DateUtils.addMonths(getValue(), theValue), getPrecision());
549                        break;
550                case Calendar.DATE:
551                        setValue(DateUtils.addDays(getValue(), theValue), getPrecision());
552                        break;
553                case Calendar.HOUR:
554                        setValue(DateUtils.addHours(getValue(), theValue), getPrecision());
555                        break;
556                case Calendar.MINUTE:
557                        setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision());
558                        break;
559                case Calendar.SECOND:
560                        setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision());
561                        break;
562                case Calendar.MILLISECOND:
563                        setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision());
564                        break;
565                default:
566                        throw new IllegalArgumentException("Unknown field constant: " + theField);
567                }
568        }
569
570        protected void setValueAsV3String(String theV3String) {
571                if (StringUtils.isBlank(theV3String)) {
572                        setValue(null);
573                } else {
574                        StringBuilder b = new StringBuilder();
575                        String timeZone = null;
576                        for (int i = 0; i < theV3String.length(); i++) {
577                                char nextChar = theV3String.charAt(i);
578                                if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
579                                        timeZone = (theV3String.substring(i));
580                                        break;
581                                }
582                                
583                                // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
584                                if (i == 4 || i == 6) {
585                                        b.append('-');
586                                } else if (i == 8) {
587                                        b.append('T');
588                                } else if (i == 10 || i == 12) {
589                                        b.append(':');
590                                }
591                                
592                                b.append(nextChar);
593                        }
594
595                        if (b.length() == 16)
596                                b.append(":00"); // schema rule, must have seconds
597                        if (timeZone != null && b.length() > 10) {
598                                if (timeZone.length() ==5) {
599                                        b.append(timeZone.substring(0, 3));
600                                        b.append(':');
601                                        b.append(timeZone.substring(3));
602                                }else {
603                                        b.append(timeZone);
604                                }
605                        }
606                        
607                        setValueAsString(b.toString());
608                }
609        }
610
611}