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