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