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