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