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