
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}