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