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