001package org.hl7.fhir.dstu2.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 032import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.Validate; 035import org.apache.commons.lang3.time.DateUtils; 036import org.apache.commons.lang3.time.FastDateFormat; 037import org.hl7.fhir.utilities.DateTimeUtil; 038 039import javax.annotation.Nullable; 040import java.text.ParseException; 041import java.util.*; 042import java.util.concurrent.ConcurrentHashMap; 043import java.util.regex.Pattern; 044 045import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.*; 046 047public abstract class BaseDateTimeType extends PrimitiveType<Date> { 048 049 private static final long serialVersionUID = 1L; 050 051 /* 052 * Add any new formatters to the static block below!! 053 */ 054 private static final List<FastDateFormat> ourFormatters; 055 056 private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}"); 057 private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}"); 058 private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy"); 059 private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd"); 060 private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd"); 061 private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}"); 062 private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss"); 063 private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat 064 .getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS"); 065 private static final FastDateFormat ourYearMonthDayTimeMilliUTCZFormat = FastDateFormat 066 .getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC")); 067 private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat 068 .getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"); 069 private static final FastDateFormat ourYearMonthDayTimeUTCZFormat = FastDateFormat 070 .getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")); 071 private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat 072 .getInstance("yyyy-MM-dd'T'HH:mm:ssZZ"); 073 private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM"); 074 private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM"); 075 private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}"); 076 private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}"); 077 private static final FastDateFormat ourYearMonthDayTimeMinsFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm"); 078 private static final FastDateFormat ourYearMonthDayTimeMinsUTCZFormat = FastDateFormat 079 .getInstance("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC")); 080 private static final FastDateFormat ourYearMonthDayTimeMinsZoneFormat = FastDateFormat 081 .getInstance("yyyy-MM-dd'T'HH:mmZZ"); 082 083 private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, 084 FastDateFormat.MEDIUM); 085 private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM); 086 private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>(); 087 088 static { 089 ArrayList<FastDateFormat> formatters = new ArrayList<FastDateFormat>(); 090 formatters.add(ourYearFormat); 091 formatters.add(ourYearMonthDayFormat); 092 formatters.add(ourYearMonthDayNoDashesFormat); 093 formatters.add(ourYearMonthDayTimeFormat); 094 formatters.add(ourYearMonthDayTimeUTCZFormat); 095 formatters.add(ourYearMonthDayTimeZoneFormat); 096 formatters.add(ourYearMonthDayTimeMilliFormat); 097 formatters.add(ourYearMonthDayTimeMilliUTCZFormat); 098 formatters.add(ourYearMonthDayTimeMilliZoneFormat); 099 formatters.add(ourYearMonthDayTimeMinsFormat); 100 formatters.add(ourYearMonthDayTimeMinsUTCZFormat); 101 formatters.add(ourYearMonthDayTimeMinsZoneFormat); 102 formatters.add(ourYearMonthFormat); 103 formatters.add(ourYearMonthNoDashesFormat); 104 ourFormatters = Collections.unmodifiableList(formatters); 105 } 106 107 private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND; 108 109 private TimeZone myTimeZone; 110 private boolean myTimeZoneZulu = false; 111 112 /** 113 * Constructor 114 */ 115 public BaseDateTimeType() { 116 // nothing 117 } 118 119 /** 120 * Constructor 121 * 122 * @throws IllegalArgumentException If the specified precision is not allowed 123 * for this type 124 */ 125 public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) { 126 setValue(theDate, thePrecision); 127 validatePrecisionAndThrowIllegalArgumentException(); 128 } 129 130 /** 131 * Constructor 132 * 133 * @throws IllegalArgumentException If the specified precision is not allowed 134 * for this type 135 */ 136 public BaseDateTimeType(String theString) { 137 setValueAsString(theString); 138 validatePrecisionAndThrowIllegalArgumentException(); 139 } 140 141 /** 142 * Constructor 143 */ 144 public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) { 145 this(theDate, thePrecision); 146 setTimeZone(theTimeZone); 147 validatePrecisionAndThrowIllegalArgumentException(); 148 } 149 150 private void clearTimeZone() { 151 myTimeZone = null; 152 myTimeZoneZulu = false; 153 } 154 155 private void validatePrecisionAndThrowIllegalArgumentException() { 156 if (!isPrecisionAllowed(getPrecision())) { 157 throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() 158 + " does not support " + getPrecision() + " precision): " + getValueAsString()); 159 } 160 } 161 162 /** 163 * @param thePrecision 164 * @return the String value of this instance with the specified precision. 165 */ 166 public String getValueAsString(TemporalPrecisionEnum thePrecision) { 167 return encode(getValue(), thePrecision); 168 } 169 170 @Override 171 protected String encode(Date theValue) { 172 return encode(theValue, myPrecision); 173 } 174 175 @Nullable 176 private String encode(Date theValue, TemporalPrecisionEnum thePrecision) { 177 if (theValue == null) { 178 return null; 179 } else { 180 switch (thePrecision) { 181 case DAY: 182 return ourYearMonthDayFormat.format(theValue); 183 case MONTH: 184 return ourYearMonthFormat.format(theValue); 185 case YEAR: 186 return ourYearFormat.format(theValue); 187 case MINUTE: 188 if (myTimeZoneZulu) { 189 GregorianCalendar cal = new GregorianCalendar(getTimeZone("GMT")); 190 cal.setTime(theValue); 191 return ourYearMonthDayTimeMinsFormat.format(cal) + "Z"; 192 } else if (myTimeZone != null) { 193 GregorianCalendar cal = new GregorianCalendar(myTimeZone); 194 cal.setTime(theValue); 195 return (ourYearMonthDayTimeMinsZoneFormat.format(cal)); 196 } else { 197 return ourYearMonthDayTimeMinsFormat.format(theValue); 198 } 199 case SECOND: 200 if (myTimeZoneZulu) { 201 GregorianCalendar cal = new GregorianCalendar(getTimeZone("GMT")); 202 cal.setTime(theValue); 203 return ourYearMonthDayTimeFormat.format(cal) + "Z"; 204 } else if (myTimeZone != null) { 205 GregorianCalendar cal = new GregorianCalendar(myTimeZone); 206 cal.setTime(theValue); 207 return (ourYearMonthDayTimeZoneFormat.format(cal)); 208 } else { 209 return ourYearMonthDayTimeFormat.format(theValue); 210 } 211 case MILLI: 212 if (myTimeZoneZulu) { 213 GregorianCalendar cal = new GregorianCalendar(getTimeZone("GMT")); 214 cal.setTime(theValue); 215 return ourYearMonthDayTimeMilliFormat.format(cal) + "Z"; 216 } else if (myTimeZone != null) { 217 GregorianCalendar cal = new GregorianCalendar(myTimeZone); 218 cal.setTime(theValue); 219 return (ourYearMonthDayTimeMilliZoneFormat.format(cal)); 220 } else { 221 return ourYearMonthDayTimeMilliFormat.format(theValue); 222 } 223 } 224 throw new IllegalStateException( 225 "Invalid precision (this is a bug, shouldn't happen https://xkcd.com/2200/): " + thePrecision); 226 } 227 } 228 229 /** 230 * Returns the default precision for the given datatype 231 */ 232 protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype(); 233 234 /** 235 * Gets the precision for this datatype (using the default for the given type if 236 * not set) 237 * 238 * @see #setPrecision(TemporalPrecisionEnum) 239 */ 240 public TemporalPrecisionEnum getPrecision() { 241 if (myPrecision == null) { 242 return getDefaultPrecisionForDatatype(); 243 } 244 return myPrecision; 245 } 246 247 /** 248 * Returns the TimeZone associated with this dateTime's value. May return 249 * <code>null</code> if no timezone was supplied. 250 */ 251 public TimeZone getTimeZone() { 252 return myTimeZone; 253 } 254 255 private boolean hasOffset(String theValue) { 256 boolean inTime = false; 257 for (int i = 0; i < theValue.length(); i++) { 258 switch (theValue.charAt(i)) { 259 case 'T': 260 inTime = true; 261 break; 262 case '+': 263 case '-': 264 if (inTime) { 265 return true; 266 } 267 break; 268 } 269 } 270 return false; 271 } 272 273 /** 274 * To be implemented by subclasses to indicate whether the given precision is 275 * allowed by this type 276 */ 277 abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision); 278 279 public boolean isTimeZoneZulu() { 280 return myTimeZoneZulu; 281 } 282 283 /** 284 * Returns <code>true</code> if this object represents a date that is today's 285 * date 286 * 287 * @throws NullPointerException if {@link #getValue()} returns <code>null</code> 288 */ 289 public boolean isToday() { 290 Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value"); 291 return DateUtils.isSameDay(new Date(), getValue()); 292 } 293 294 @Override 295 protected Date parse(String theValue) throws IllegalArgumentException { 296 try { 297 if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) { 298 setPrecision(YEAR); 299 clearTimeZone(); 300 return ((ourYearFormat).parse(theValue)); 301 } else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) { 302 // Eg. 198401 (allow this just to be lenient) 303 setPrecision(MONTH); 304 clearTimeZone(); 305 return ((ourYearMonthNoDashesFormat).parse(theValue)); 306 } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) { 307 // E.g. 1984-01 (this is valid according to the spec) 308 setPrecision(MONTH); 309 clearTimeZone(); 310 return ((ourYearMonthFormat).parse(theValue)); 311 } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) { 312 // Eg. 19840101 (allow this just to be lenient) 313 setPrecision(DAY); 314 clearTimeZone(); 315 return ((ourYearMonthDayNoDashesFormat).parse(theValue)); 316 } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) { 317 // E.g. 1984-01-01 (this is valid according to the spec) 318 setPrecision(DAY); 319 clearTimeZone(); 320 return ((ourYearMonthDayFormat).parse(theValue)); 321 } else if (theValue.length() >= 16) { // date and time with possible time zone 322 int firstColonIndex = theValue.indexOf(':'); 323 if (firstColonIndex == -1) { 324 throw new IllegalArgumentException("Invalid date/time string: " + theValue); 325 } 326 327 boolean hasSeconds = theValue.length() > firstColonIndex + 3 ? theValue.charAt(firstColonIndex + 3) == ':' 328 : false; 329 330 int dotIndex = theValue.length() >= 18 ? theValue.indexOf('.', 18) : -1; 331 boolean hasMillis = dotIndex > -1; 332 333 Date retVal; 334 if (hasMillis) { 335 try { 336 if (hasOffset(theValue)) { 337 retVal = ourYearMonthDayTimeMilliZoneFormat.parse(theValue); 338 } else if (theValue.endsWith("Z")) { 339 retVal = ourYearMonthDayTimeMilliUTCZFormat.parse(theValue); 340 } else { 341 retVal = ourYearMonthDayTimeMilliFormat.parse(theValue); 342 } 343 } catch (ParseException p2) { 344 throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue); 345 } 346 setTimeZone(theValue, hasMillis); 347 setPrecision(TemporalPrecisionEnum.MILLI); 348 } else if (hasSeconds) { 349 try { 350 if (hasOffset(theValue)) { 351 retVal = ourYearMonthDayTimeZoneFormat.parse(theValue); 352 } else if (theValue.endsWith("Z")) { 353 retVal = ourYearMonthDayTimeUTCZFormat.parse(theValue); 354 } else { 355 retVal = ourYearMonthDayTimeFormat.parse(theValue); 356 } 357 } catch (ParseException p2) { 358 throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue); 359 } 360 361 setTimeZone(theValue, hasMillis); 362 setPrecision(TemporalPrecisionEnum.SECOND); 363 } else { 364 try { 365 if (hasOffset(theValue)) { 366 retVal = ourYearMonthDayTimeMinsZoneFormat.parse(theValue); 367 } else if (theValue.endsWith("Z")) { 368 retVal = ourYearMonthDayTimeMinsUTCZFormat.parse(theValue); 369 } else { 370 retVal = ourYearMonthDayTimeMinsFormat.parse(theValue); 371 } 372 } catch (ParseException p2) { 373 throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue, p2); 374 } 375 376 setTimeZone(theValue, hasMillis); 377 setPrecision(TemporalPrecisionEnum.MINUTE); 378 } 379 380 return retVal; 381 } else { 382 throw new IllegalArgumentException("Invalid date/time string (invalid length): " + theValue); 383 } 384 } catch (ParseException e) { 385 throw new IllegalArgumentException("Invalid date string (" + e.getMessage() + "): " + theValue); 386 } 387 } 388 389 /** 390 * Sets the precision for this datatype using field values from 391 * {@link Calendar}. Valid values are: 392 * <ul> 393 * <li>{@link Calendar#SECOND} 394 * <li>{@link Calendar#DAY_OF_MONTH} 395 * <li>{@link Calendar#MONTH} 396 * <li>{@link Calendar#YEAR} 397 * </ul> 398 * 399 * @throws IllegalArgumentException 400 */ 401 public void setPrecision(TemporalPrecisionEnum thePrecision) throws IllegalArgumentException { 402 if (thePrecision == null) { 403 throw new NullPointerException("Precision may not be null"); 404 } 405 myPrecision = thePrecision; 406 updateStringValue(); 407 } 408 409 private void setTimeZone(String theValueString, boolean hasMillis) { 410 clearTimeZone(); 411 int timeZoneStart = 19; 412 if (hasMillis) 413 timeZoneStart += 4; 414 if (theValueString.endsWith("Z")) { 415 setTimeZoneZulu(true); 416 } else if (theValueString.indexOf("GMT", timeZoneStart) != -1) { 417 setTimeZone(getTimeZone(theValueString.substring(timeZoneStart))); 418 } else if (theValueString.indexOf('+', timeZoneStart) != -1 || theValueString.indexOf('-', timeZoneStart) != -1) { 419 setTimeZone(getTimeZone("GMT" + theValueString.substring(timeZoneStart))); 420 } 421 } 422 423 public void setTimeZone(TimeZone theTimeZone) { 424 myTimeZone = theTimeZone; 425 updateStringValue(); 426 } 427 428 public void setTimeZoneZulu(boolean theTimeZoneZulu) { 429 myTimeZoneZulu = theTimeZoneZulu; 430 updateStringValue(); 431 } 432 433 /** 434 * Sets the value of this date/time using the default level of precision for 435 * this datatype using the system local time zone 436 * 437 * @param theValue The date value 438 */ 439 @Override 440 public BaseDateTimeType setValue(Date theValue) { 441 if (myTimeZoneZulu == false && myTimeZone == null) { 442 myTimeZone = TimeZone.getDefault(); 443 } 444 myPrecision = getDefaultPrecisionForDatatype(); 445 BaseDateTimeType retVal = (BaseDateTimeType) super.setValue(theValue); 446 return retVal; 447 } 448 449 /** 450 * Sets the value of this date/time using the specified level of precision using 451 * the system local time zone 452 * 453 * @param theValue The date value 454 * @param thePrecision The precision 455 * @throws IllegalArgumentException 456 */ 457 public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws IllegalArgumentException { 458 if (myTimeZoneZulu == false && myTimeZone == null) { 459 myTimeZone = TimeZone.getDefault(); 460 } 461 myPrecision = thePrecision; 462 super.setValue(theValue); 463 } 464 465 @Override 466 public void setValueAsString(String theString) throws IllegalArgumentException { 467 clearTimeZone(); 468 super.setValueAsString(theString); 469 } 470 471 /** 472 * For unit tests only 473 */ 474 static List<FastDateFormat> getFormatters() { 475 return ourFormatters; 476 } 477 478 public boolean before(DateTimeType theDateTimeType) { 479 return getValue().before(theDateTimeType.getValue()); 480 } 481 482 public boolean after(DateTimeType theDateTimeType) { 483 return getValue().after(theDateTimeType.getValue()); 484 } 485 486 /** 487 * Returns a human readable version of this date/time using the system local 488 * format. 489 * <p> 490 * <b>Note on time zones:</b> This method renders the value using the time zone 491 * that is contained within the value. For example, if this date object contains 492 * the value "2012-01-05T12:00:00-08:00", the human display will be rendered as 493 * "12:00:00" even if the application is being executed on a system in a 494 * different time zone. If this behaviour is not what you want, use 495 * {@link #toHumanDisplayLocalTimezone()} instead. 496 * </p> 497 */ 498 public String toHumanDisplay() { 499 return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); 500 } 501 502 /** 503 * Returns a human readable version of this date/time using the system local 504 * format, converted to the local timezone if neccesary. 505 * 506 * @see #toHumanDisplay() for a method which does not convert the time to the 507 * local timezone before rendering it. 508 */ 509 public String toHumanDisplayLocalTimezone() { 510 return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); 511 } 512 513 /** 514 * Returns a view of this date/time as a Calendar object 515 */ 516 public Calendar toCalendar() { 517 Calendar retVal = Calendar.getInstance(); 518 retVal.setTime(getValue()); 519 retVal.setTimeZone(getTimeZone()); 520 return retVal; 521 } 522 523 /** 524 * Sets the TimeZone offset in minutes relative to GMT 525 */ 526 public void setOffsetMinutes(int theZoneOffsetMinutes) { 527 int offsetAbs = Math.abs(theZoneOffsetMinutes); 528 529 int mins = offsetAbs % 60; 530 int hours = offsetAbs / 60; 531 532 if (theZoneOffsetMinutes < 0) { 533 setTimeZone(getTimeZone("GMT-" + hours + ":" + mins)); 534 } else { 535 setTimeZone(getTimeZone("GMT+" + hours + ":" + mins)); 536 } 537 } 538 539 /** 540 * Returns the time in millis as represented by this Date/Time 541 */ 542 public long getTime() { 543 return getValue().getTime(); 544 } 545 546 /** 547 * Adds the given amount to the field specified by theField 548 * 549 * @param theField The field, uses constants from {@link Calendar} such as 550 * {@link Calendar#YEAR} 551 * @param theValue The number to add (or subtract for a negative number) 552 */ 553 public void add(int theField, int theValue) { 554 switch (theField) { 555 case Calendar.YEAR: 556 setValue(DateUtils.addYears(getValue(), theValue), getPrecision()); 557 break; 558 case Calendar.MONTH: 559 setValue(DateUtils.addMonths(getValue(), theValue), getPrecision()); 560 break; 561 case Calendar.DATE: 562 setValue(DateUtils.addDays(getValue(), theValue), getPrecision()); 563 break; 564 case Calendar.HOUR: 565 setValue(DateUtils.addHours(getValue(), theValue), getPrecision()); 566 break; 567 case Calendar.MINUTE: 568 setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision()); 569 break; 570 case Calendar.SECOND: 571 setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision()); 572 break; 573 case Calendar.MILLISECOND: 574 setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision()); 575 break; 576 default: 577 throw new IllegalArgumentException("Unknown field constant: " + theField); 578 } 579 } 580 581 protected void setValueAsV3String(String theV3String) { 582 if (StringUtils.isBlank(theV3String)) { 583 setValue(null); 584 } else { 585 StringBuilder b = new StringBuilder(); 586 String timeZone = null; 587 for (int i = 0; i < theV3String.length(); i++) { 588 char nextChar = theV3String.charAt(i); 589 if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') { 590 timeZone = (theV3String.substring(i)); 591 break; 592 } 593 594 // assertEquals("2013-02-02T20:13:03-05:00", 595 // DateAndTime.parseV3("20130202201303-0500").toString()); 596 if (i == 4 || i == 6) { 597 b.append('-'); 598 } else if (i == 8) { 599 b.append('T'); 600 } else if (i == 10 || i == 12) { 601 b.append(':'); 602 } 603 604 b.append(nextChar); 605 } 606 607 if (b.length() == 16) 608 b.append(":00"); // schema rule, must have seconds 609 if (timeZone != null && b.length() > 10) { 610 if (timeZone.length() == 5) { 611 b.append(timeZone.substring(0, 3)); 612 b.append(':'); 613 b.append(timeZone.substring(3)); 614 } else { 615 b.append(timeZone); 616 } 617 } 618 619 setValueAsString(b.toString()); 620 } 621 } 622 623 private TimeZone getTimeZone(String offset) { 624 return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone); 625 } 626 627}