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