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