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