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