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