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