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 org.apache.commons.lang3.StringUtils; 043import org.apache.commons.lang3.Validate; 044import org.apache.commons.lang3.time.DateUtils; 045import org.apache.commons.lang3.time.FastDateFormat; 046 047import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 048import ca.uhn.fhir.parser.DataFormatException; 049import org.hl7.fhir.utilities.DateTimeUtil; 050 051public abstract class BaseDateTimeType extends PrimitiveType<Date> { 052 053 static final long NANOS_PER_MILLIS = 1000000L; 054 055 static final long NANOS_PER_SECOND = 1000000000L; 056 private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>(); 057 private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM); 058 059 private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM); 060 private static final long serialVersionUID = 1L; 061 062 private String myFractionalSeconds; 063 private TemporalPrecisionEnum myPrecision = null; 064 private TimeZone myTimeZone; 065 private boolean myTimeZoneZulu = false; 066 067 /** 068 * Constructor 069 */ 070 public BaseDateTimeType() { 071 // nothing 072 } 073 074 /** 075 * Constructor 076 * 077 * @throws IllegalArgumentException 078 * If the specified precision is not allowed for this type 079 */ 080 public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) { 081 setValue(theDate, thePrecision); 082 validatePrecisionAndThrowIllegalArgumentException(); 083 } 084 085 /** 086 * Constructor 087 */ 088 public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) { 089 this(theDate, thePrecision); 090 setTimeZone(theTimeZone); 091 validatePrecisionAndThrowIllegalArgumentException(); 092 } 093 094 /** 095 * Constructor 096 * 097 * @throws IllegalArgumentException 098 * If the specified precision is not allowed for this type 099 */ 100 public BaseDateTimeType(String theString) { 101 setValueAsString(theString); 102 validatePrecisionAndThrowIllegalArgumentException(); 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(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 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, 16); 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, 59)); 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 if (precision == TemporalPrecisionEnum.MINUTE) { 507 validatePrecisionAndThrowIllegalArgumentException(); 508 } 509 510 myPrecision = precision; 511 return cal.getTime(); 512 513 } 514 515 private int parseInt(String theValue, String theSubstring, int theLowerBound, int theUpperBound) { 516 int retVal = 0; 517 try { 518 retVal = Integer.parseInt(theSubstring); 519 } catch (NumberFormatException e) { 520 throwBadDateFormat(theValue); 521 } 522 523 if (retVal < theLowerBound || retVal > theUpperBound) { 524 throwBadDateFormat(theValue); 525 } 526 527 return retVal; 528 } 529 530 /** 531 * Sets the month with 1-index, e.g. 1=the first day of the month 532 */ 533 public BaseDateTimeType setDay(int theDay) { 534 setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31); 535 return this; 536 } 537 538 private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) { 539 validateValueInRange(theValue, theMinimum, theMaximum); 540 Calendar cal; 541 if (getValue() == null) { 542 cal = new GregorianCalendar(); 543 } else { 544 cal = getValueAsCalendar(); 545 } 546 if (theField != -1) { 547 cal.set(theField, theValue); 548 } 549 if (theFractionalSeconds != null) { 550 myFractionalSeconds = theFractionalSeconds; 551 } else if (theField == Calendar.MILLISECOND) { 552 myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0'); 553 } 554 super.setValue(cal.getTime()); 555 } 556 557 /** 558 * Sets the hour of the day in a 24h clock, e.g. 13=1pm 559 */ 560 public BaseDateTimeType setHour(int theHour) { 561 setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23); 562 return this; 563 } 564 565 /** 566 * Sets the milliseconds within the current second. 567 * <p> 568 * Note that this method sets the 569 * same value as {@link #setNanos(long)} but with less precision. 570 * </p> 571 */ 572 public BaseDateTimeType setMillis(int theMillis) { 573 setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999); 574 return this; 575 } 576 577 /** 578 * Sets the minute of the hour in the range 0-59 579 */ 580 public BaseDateTimeType setMinute(int theMinute) { 581 setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59); 582 return this; 583 } 584 585 /** 586 * Sets the month with 0-index, e.g. 0=January 587 */ 588 public BaseDateTimeType setMonth(int theMonth) { 589 setFieldValue(Calendar.MONTH, theMonth, null, 0, 11); 590 return this; 591 } 592 593 /** 594 * Sets the nanoseconds within the current second 595 * <p> 596 * Note that this method sets the 597 * same value as {@link #setMillis(int)} but with more precision. 598 * </p> 599 */ 600 public BaseDateTimeType setNanos(long theNanos) { 601 validateValueInRange(theNanos, 0, NANOS_PER_SECOND - 1); 602 String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0'); 603 604 // Strip trailing 0s 605 for (int i = fractionalSeconds.length(); i > 0; i--) { 606 if (fractionalSeconds.charAt(i - 1) != '0') { 607 fractionalSeconds = fractionalSeconds.substring(0, i); 608 break; 609 } 610 } 611 int millis = (int) (theNanos / NANOS_PER_MILLIS); 612 setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999); 613 return this; 614 } 615 616 /** 617 * Sets the precision for this datatype 618 * 619 * @throws DataFormatException 620 */ 621 public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException { 622 if (thePrecision == null) { 623 throw new NullPointerException("Precision may not be null"); 624 } 625 myPrecision = thePrecision; 626 updateStringValue(); 627 } 628 629 /** 630 * Sets the second of the minute in the range 0-59 631 */ 632 public BaseDateTimeType setSecond(int theSecond) { 633 setFieldValue(Calendar.SECOND, theSecond, null, 0, 59); 634 return this; 635 } 636 637 private BaseDateTimeType setTimeZone(String theWholeValue, String theValue) { 638 639 if (isBlank(theValue)) { 640 throwBadDateFormat(theWholeValue); 641 } else if (theValue.charAt(0) == 'Z') { 642 myTimeZone = null; 643 myTimeZoneZulu = true; 644 } else if (theValue.length() != 6) { 645 throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\""); 646 } else if (theValue.charAt(3) != ':' || !(theValue.charAt(0) == '+' || theValue.charAt(0) == '-')) { 647 throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\""); 648 } else { 649 parseInt(theWholeValue, theValue.substring(1, 3), 0, 23); 650 parseInt(theWholeValue, theValue.substring(4, 6), 0, 59); 651 myTimeZoneZulu = false; 652 myTimeZone = getTimeZone("GMT" + theValue); 653 } 654 655 return this; 656 } 657 658 public BaseDateTimeType setTimeZone(TimeZone theTimeZone) { 659 myTimeZone = theTimeZone; 660 myTimeZoneZulu = false; 661 updateStringValue(); 662 return this; 663 } 664 665 public BaseDateTimeType setTimeZoneZulu(boolean theTimeZoneZulu) { 666 myTimeZoneZulu = theTimeZoneZulu; 667 myTimeZone = null; 668 updateStringValue(); 669 return this; 670 } 671 672 /** 673 * Sets the value for this type using the given Java Date object as the time, and using the default precision for 674 * this datatype (unless the precision is already set), as well as the local timezone as determined by the local operating 675 * system. Both of these properties may be modified in subsequent calls if neccesary. 676 */ 677 @Override 678 public BaseDateTimeType setValue(Date theValue) { 679 setValue(theValue, getPrecision()); 680 return this; 681 } 682 683 /** 684 * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as 685 * well as the local timezone as determined by the local operating system. Both of 686 * these properties may be modified in subsequent calls if neccesary. 687 * 688 * @param theValue 689 * The date value 690 * @param thePrecision 691 * The precision 692 * @throws DataFormatException 693 */ 694 public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException { 695 if (getTimeZone() == null) { 696 setTimeZone(TimeZone.getDefault()); 697 } 698 myPrecision = thePrecision; 699 myFractionalSeconds = ""; 700 if (theValue != null) { 701 long millis = theValue.getTime() % 1000; 702 if (millis < 0) { 703 // This is for times before 1970 (see bug #444) 704 millis = 1000 + millis; 705 } 706 String fractionalSeconds = Integer.toString((int) millis); 707 myFractionalSeconds = StringUtils.leftPad(fractionalSeconds, 3, '0'); 708 } 709 super.setValue(theValue); 710 } 711 712 @Override 713 public void setValueAsString(String theString) throws DataFormatException { 714 clearTimeZone(); 715 super.setValueAsString(theString); 716 } 717 718 protected void setValueAsV3String(String theV3String) { 719 if (StringUtils.isBlank(theV3String)) { 720 setValue(null); 721 } else { 722 StringBuilder b = new StringBuilder(); 723 String timeZone = null; 724 for (int i = 0; i < theV3String.length(); i++) { 725 char nextChar = theV3String.charAt(i); 726 if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') { 727 timeZone = (theV3String.substring(i)); 728 break; 729 } 730 731 // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString()); 732 if (i == 4 || i == 6) { 733 b.append('-'); 734 } else if (i == 8) { 735 b.append('T'); 736 } else if (i == 10 || i == 12) { 737 b.append(':'); 738 } 739 740 b.append(nextChar); 741 } 742 743 if (b.length() == 16) 744 b.append(":00"); // schema rule, must have seconds 745 if (timeZone != null && b.length() > 10) { 746 if (timeZone.length() == 5) { 747 b.append(timeZone.substring(0, 3)); 748 b.append(':'); 749 b.append(timeZone.substring(3)); 750 } else { 751 b.append(timeZone); 752 } 753 } 754 755 setValueAsString(b.toString()); 756 } 757 } 758 759 /** 760 * Sets the year, e.g. 2015 761 */ 762 public BaseDateTimeType setYear(int theYear) { 763 setFieldValue(Calendar.YEAR, theYear, null, 0, 9999); 764 return this; 765 } 766 767 private void throwBadDateFormat(String theValue) { 768 throw new DataFormatException("Invalid date/time format: \"" + theValue + "\""); 769 } 770 771 private void throwBadDateFormat(String theValue, String theMesssage) { 772 throw new DataFormatException("Invalid date/time format: \"" + theValue + "\": " + theMesssage); 773 } 774 775 /** 776 * Returns a view of this date/time as a Calendar object. Note that the returned 777 * Calendar object is entirely independent from <code>this</code> object. Changes to the 778 * calendar will not affect <code>this</code>. 779 */ 780 public Calendar toCalendar() { 781 Calendar retVal = Calendar.getInstance(); 782 retVal.setTime(getValue()); 783 retVal.setTimeZone(getTimeZone()); 784 return retVal; 785 } 786 787 /** 788 * Returns a human readable version of this date/time using the system local format. 789 * <p> 790 * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value. 791 * For example, if this date object contains the value "2012-01-05T12:00:00-08:00", 792 * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a 793 * different time zone. If this behaviour is not what you want, use 794 * {@link #toHumanDisplayLocalTimezone()} instead. 795 * </p> 796 */ 797 public String toHumanDisplay() { 798 return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); 799 } 800 801 /** 802 * Returns a human readable version of this date/time using the system local format, converted to the local timezone 803 * if neccesary. 804 * 805 * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. 806 */ 807 public String toHumanDisplayLocalTimezone() { 808 return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); 809 } 810 811 private void validateBeforeOrAfter(DateTimeType theDateTimeType) { 812 if (getValue() == null) { 813 throw new NullPointerException("This BaseDateTimeType does not contain a value (getValue() returns null)"); 814 } 815 if (theDateTimeType == null) { 816 throw new NullPointerException("theDateTimeType must not be null"); 817 } 818 if (theDateTimeType.getValue() == null) { 819 throw new NullPointerException("The given BaseDateTimeType does not contain a value (theDateTimeType.getValue() returns null)"); 820 } 821 } 822 823 private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) { 824 if (theValue.charAt(theIndex) != theChar) { 825 throwBadDateFormat(theValue, "Expected character '" + theChar + "' at index " + theIndex + " but found " + theValue.charAt(theIndex)); 826 } 827 } 828 829 private void validateLengthIsAtLeast(String theValue, int theLength) { 830 if (theValue.length() < theLength) { 831 throwBadDateFormat(theValue); 832 } 833 } 834 835 private void validatePrecisionAndThrowIllegalArgumentException() { 836 if (!isPrecisionAllowed(getPrecision())) { 837 throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + getValueAsString()); 838 } 839 } 840 841 private void validateValueInRange(long theValue, long theMinimum, long theMaximum) { 842 if (theValue < theMinimum || theValue > theMaximum) { 843 throw new IllegalArgumentException("Value " + theValue + " is not between allowable range: " + theMinimum + " - " + theMaximum); 844 } 845 } 846 847 private TimeZone getTimeZone(String offset) { 848 return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone); 849 } 850 851}