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