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