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