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