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