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