001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2012-2015 ForgeRock AS. 025 */ 026package org.forgerock.opendj.ldap; 027 028import java.util.Calendar; 029import java.util.Date; 030import java.util.GregorianCalendar; 031import java.util.TimeZone; 032 033import org.forgerock.i18n.LocalizableMessage; 034import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2; 035import org.forgerock.i18n.LocalizedIllegalArgumentException; 036import org.forgerock.util.Reject; 037 038import static com.forgerock.opendj.ldap.CoreMessages.*; 039 040/** 041 * An LDAP generalized time as defined in RFC 4517. This class facilitates 042 * parsing of generalized time values to and from {@link Date} and 043 * {@link Calendar} classes. 044 * <p> 045 * The following are examples of generalized time values: 046 * 047 * <pre> 048 * 199412161032Z 049 * 199412160532-0500 050 * </pre> 051 * 052 * @see <a href="http://tools.ietf.org/html/rfc4517#section-3.3.13">RFC 4517 - 053 * Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching 054 * Rules </a> 055 */ 056public final class GeneralizedTime implements Comparable<GeneralizedTime> { 057 058 /** UTC TimeZone is assumed to never change over JVM lifetime. */ 059 private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone("UTC"); 060 061 /** The smallest time representable using the generalized time syntax. */ 062 public static final GeneralizedTime MIN_GENERALIZED_TIME = valueOf("00010101000000Z"); 063 064 /** The smallest time in milli-seconds representable using the generalized time syntax. */ 065 public static final long MIN_GENERALIZED_TIME_MS = MIN_GENERALIZED_TIME.getTimeInMillis(); 066 067 /** 068 * Returns a generalized time whose value is the current time, using the 069 * default time zone and locale. 070 * 071 * @return A generalized time whose value is the current time. 072 */ 073 public static GeneralizedTime currentTime() { 074 return valueOf(Calendar.getInstance()); 075 } 076 077 /** 078 * Returns a generalized time representing the provided {@code Calendar}. 079 * <p> 080 * The provided calendar will be defensively copied in order to preserve 081 * immutability. 082 * 083 * @param calendar 084 * The calendar to be converted to a generalized time. 085 * @return A generalized time representing the provided {@code Calendar}. 086 */ 087 public static GeneralizedTime valueOf(final Calendar calendar) { 088 Reject.ifNull(calendar); 089 return new GeneralizedTime((Calendar) calendar.clone(), null, Long.MIN_VALUE, null); 090 } 091 092 /** 093 * Returns a generalized time representing the provided {@code Date}. 094 * <p> 095 * The provided date will be defensively copied in order to preserve 096 * immutability. 097 * 098 * @param date 099 * The date to be converted to a generalized time. 100 * @return A generalized time representing the provided {@code Date}. 101 */ 102 public static GeneralizedTime valueOf(final Date date) { 103 Reject.ifNull(date); 104 return new GeneralizedTime(null, (Date) date.clone(), Long.MIN_VALUE, null); 105 } 106 107 /** 108 * Returns a generalized time representing the provided time in milliseconds 109 * since the epoch. 110 * 111 * @param timeMS 112 * The time to be converted to a generalized time. 113 * @return A generalized time representing the provided time in milliseconds 114 * since the epoch. 115 */ 116 public static GeneralizedTime valueOf(final long timeMS) { 117 Reject.ifTrue(timeMS < MIN_GENERALIZED_TIME_MS, "timeMS is too old to represent as a generalized time"); 118 return new GeneralizedTime(null, null, timeMS, null); 119 } 120 121 /** 122 * Parses the provided string as an LDAP generalized time. 123 * 124 * @param time 125 * The generalized time value to be parsed. 126 * @return The parsed generalized time. 127 * @throws LocalizedIllegalArgumentException 128 * If {@code time} cannot be parsed as a valid generalized time 129 * string. 130 * @throws NullPointerException 131 * If {@code time} was {@code null}. 132 */ 133 public static GeneralizedTime valueOf(final String time) { 134 int year = 0; 135 int month = 0; 136 int day = 0; 137 int hour = 0; 138 int minute = 0; 139 int second = 0; 140 141 // Get the value as a string and verify that it is at least long 142 // enough for "YYYYMMDDhhZ", which is the shortest allowed value. 143 final String valueString = time.toUpperCase(); 144 final int length = valueString.length(); 145 if (length < 11) { 146 final LocalizableMessage message = 147 WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT.get(valueString); 148 throw new LocalizedIllegalArgumentException(message); 149 } 150 151 // The first four characters are the century and year, and they must 152 // be numeric digits between 0 and 9. 153 for (int i = 0; i < 4; i++) { 154 char c = valueString.charAt(i); 155 final int val = toInt(c, 156 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR, valueString, String.valueOf(c)); 157 year = (year * 10) + val; 158 } 159 160 // The next two characters are the month, and they must form the 161 // string representation of an integer between 01 and 12. 162 char m1 = valueString.charAt(4); 163 final char m2 = valueString.charAt(5); 164 final String monthValue = valueString.substring(4, 6); 165 switch (m1) { 166 case '0': 167 // m2 must be a digit between 1 and 9. 168 switch (m2) { 169 case '1': 170 month = Calendar.JANUARY; 171 break; 172 173 case '2': 174 month = Calendar.FEBRUARY; 175 break; 176 177 case '3': 178 month = Calendar.MARCH; 179 break; 180 181 case '4': 182 month = Calendar.APRIL; 183 break; 184 185 case '5': 186 month = Calendar.MAY; 187 break; 188 189 case '6': 190 month = Calendar.JUNE; 191 break; 192 193 case '7': 194 month = Calendar.JULY; 195 break; 196 197 case '8': 198 month = Calendar.AUGUST; 199 break; 200 201 case '9': 202 month = Calendar.SEPTEMBER; 203 break; 204 205 default: 206 throw new LocalizedIllegalArgumentException( 207 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue)); 208 } 209 break; 210 case '1': 211 // m2 must be a digit between 0 and 2. 212 switch (m2) { 213 case '0': 214 month = Calendar.OCTOBER; 215 break; 216 217 case '1': 218 month = Calendar.NOVEMBER; 219 break; 220 221 case '2': 222 month = Calendar.DECEMBER; 223 break; 224 225 default: 226 throw new LocalizedIllegalArgumentException( 227 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue)); 228 } 229 break; 230 default: 231 throw new LocalizedIllegalArgumentException( 232 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue)); 233 } 234 235 // The next two characters should be the day of the month, and they 236 // must form the string representation of an integer between 01 and 237 // 31. This doesn't do any validation against the year or month, so 238 // it will allow dates like April 31, or February 29 in a non-leap 239 // year, but we'll let those slide. 240 final char d1 = valueString.charAt(6); 241 final char d2 = valueString.charAt(7); 242 final String dayValue = valueString.substring(6, 8); 243 switch (d1) { 244 case '0': 245 // d2 must be a digit between 1 and 9. 246 day = toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue); 247 if (day == 0) { 248 throw new LocalizedIllegalArgumentException( 249 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue)); 250 } 251 break; 252 253 case '1': 254 // d2 must be a digit between 0 and 9. 255 day = 10 + toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue); 256 break; 257 258 case '2': 259 // d2 must be a digit between 0 and 9. 260 day = 20 + toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue); 261 break; 262 263 case '3': 264 // d2 must be either 0 or 1. 265 switch (d2) { 266 case '0': 267 day = 30; 268 break; 269 270 case '1': 271 day = 31; 272 break; 273 274 default: 275 throw new LocalizedIllegalArgumentException( 276 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue)); 277 } 278 break; 279 280 default: 281 throw new LocalizedIllegalArgumentException( 282 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue)); 283 } 284 285 // The next two characters must be the hour, and they must form the 286 // string representation of an integer between 00 and 23. 287 final char h1 = valueString.charAt(8); 288 final char h2 = valueString.charAt(9); 289 final String hourValue = valueString.substring(8, 10); 290 switch (h1) { 291 case '0': 292 hour = toInt(h2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR, valueString, hourValue); 293 break; 294 295 case '1': 296 hour = 10 + toInt(h2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR, valueString, hourValue); 297 break; 298 299 case '2': 300 switch (h2) { 301 case '0': 302 hour = 20; 303 break; 304 305 case '1': 306 hour = 21; 307 break; 308 309 case '2': 310 hour = 22; 311 break; 312 313 case '3': 314 hour = 23; 315 break; 316 317 default: 318 throw new LocalizedIllegalArgumentException( 319 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, hourValue)); 320 } 321 break; 322 323 default: 324 throw new LocalizedIllegalArgumentException( 325 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, hourValue)); 326 } 327 328 // Next, there should be either two digits comprising an integer 329 // between 00 and 59 (for the minute), a letter 'Z' (for the UTC 330 // specifier), a plus or minus sign followed by two or four digits 331 // (for the UTC offset), or a period or comma representing the 332 // fraction. 333 m1 = valueString.charAt(10); 334 switch (m1) { 335 case '0': 336 case '1': 337 case '2': 338 case '3': 339 case '4': 340 case '5': 341 // There must be at least two more characters, and the next one 342 // must be a digit between 0 and 9. 343 if (length < 13) { 344 throw invalidChar(valueString, m1, 10); 345 } 346 347 minute = 10 * (m1 - '0'); 348 minute += toInt(valueString.charAt(11), 349 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE, valueString, valueString.substring(10, 12)); 350 351 break; 352 353 case 'Z': 354 case 'z': 355 // This is fine only if we are at the end of the value. 356 if (length == 11) { 357 final TimeZone tz = TIME_ZONE_UTC_OBJ; 358 return createTime(valueString, year, month, day, hour, minute, second, tz); 359 } else { 360 throw invalidChar(valueString, m1, 10); 361 } 362 363 case '+': 364 case '-': 365 // These are fine only if there are exactly two or four more 366 // digits that specify a valid offset. 367 if (length == 13 || length == 15) { 368 final TimeZone tz = getTimeZoneForOffset(valueString, 10); 369 return createTime(valueString, year, month, day, hour, minute, second, tz); 370 } else { 371 throw invalidChar(valueString, m1, 10); 372 } 373 374 case '.': 375 case ',': 376 return finishDecodingFraction(valueString, 11, year, month, day, hour, minute, second, 377 3600000); 378 379 default: 380 throw invalidChar(valueString, m1, 10); 381 } 382 383 // Next, there should be either two digits comprising an integer 384 // between 00 and 60 (for the second, including a possible leap 385 // second), a letter 'Z' (for the UTC specifier), a plus or minus 386 // sign followed by two or four digits (for the UTC offset), or a 387 // period or comma to start the fraction. 388 final char s1 = valueString.charAt(12); 389 switch (s1) { 390 case '0': 391 case '1': 392 case '2': 393 case '3': 394 case '4': 395 case '5': 396 // There must be at least two more characters, and the next one 397 // must be a digit between 0 and 9. 398 if (length < 15) { 399 throw invalidChar(valueString, s1, 12); 400 } 401 402 second = 10 * (s1 - '0'); 403 second += toInt(valueString.charAt(13), 404 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE, valueString, valueString.substring(12, 14)); 405 406 break; 407 408 case '6': 409 // There must be at least two more characters and the next one 410 // must be a 0. 411 if (length < 15) { 412 throw invalidChar(valueString, s1, 12); 413 } 414 415 if (valueString.charAt(13) != '0') { 416 throw new LocalizedIllegalArgumentException( 417 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND.get( 418 valueString, valueString.substring(12, 14))); 419 } 420 421 second = 60; 422 break; 423 424 case 'Z': 425 case 'z': 426 // This is fine only if we are at the end of the value. 427 if (length == 13) { 428 final TimeZone tz = TIME_ZONE_UTC_OBJ; 429 return createTime(valueString, year, month, day, hour, minute, second, tz); 430 } else { 431 throw invalidChar(valueString, s1, 12); 432 } 433 434 case '+': 435 case '-': 436 // These are fine only if there are exactly two or four more 437 // digits that specify a valid offset. 438 if (length == 15 || length == 17) { 439 final TimeZone tz = getTimeZoneForOffset(valueString, 12); 440 return createTime(valueString, year, month, day, hour, minute, second, tz); 441 } else { 442 throw invalidChar(valueString, s1, 12); 443 } 444 445 case '.': 446 case ',': 447 return finishDecodingFraction(valueString, 13, year, month, day, hour, minute, second, 448 60000); 449 450 default: 451 throw invalidChar(valueString, s1, 12); 452 } 453 454 // Next, there should be either a period or comma followed by 455 // between one and three digits (to specify the sub-second), a 456 // letter 'Z' (for the UTC specifier), or a plus or minus sign 457 // followed by two our four digits (for the UTC offset). 458 switch (valueString.charAt(14)) { 459 case '.': 460 case ',': 461 return finishDecodingFraction(valueString, 15, year, month, day, hour, minute, second, 462 1000); 463 464 case 'Z': 465 case 'z': 466 // This is fine only if we are at the end of the value. 467 if (length == 15) { 468 final TimeZone tz = TIME_ZONE_UTC_OBJ; 469 return createTime(valueString, year, month, day, hour, minute, second, tz); 470 } else { 471 throw invalidChar(valueString, valueString.charAt(14), 14); 472 } 473 474 case '+': 475 case '-': 476 // These are fine only if there are exactly two or four more 477 // digits that specify a valid offset. 478 if (length == 17 || length == 19) { 479 final TimeZone tz = getTimeZoneForOffset(valueString, 14); 480 return createTime(valueString, year, month, day, hour, minute, second, tz); 481 } else { 482 throw invalidChar(valueString, valueString.charAt(14), 14); 483 } 484 485 default: 486 throw invalidChar(valueString, valueString.charAt(14), 14); 487 } 488 } 489 490 private static LocalizedIllegalArgumentException invalidChar(String valueString, char c, int pos) { 491 return new LocalizedIllegalArgumentException( 492 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get( 493 valueString, String.valueOf(c), pos)); 494 } 495 496 private static int toInt(char c, Arg2<Object, Object> invalidSyntaxMsg, String valueString, String unitValue) { 497 switch (c) { 498 case '0': 499 return 0; 500 case '1': 501 return 1; 502 case '2': 503 return 2; 504 case '3': 505 return 3; 506 case '4': 507 return 4; 508 case '5': 509 return 5; 510 case '6': 511 return 6; 512 case '7': 513 return 7; 514 case '8': 515 return 8; 516 case '9': 517 return 9; 518 default: 519 throw new LocalizedIllegalArgumentException( 520 invalidSyntaxMsg.get(valueString, unitValue)); 521 } 522 } 523 524 /** 525 * Returns a generalized time object representing the provided date / time 526 * parameters. 527 * 528 * @param value 529 * The generalized time string representation. 530 * @param year 531 * The year. 532 * @param month 533 * The month. 534 * @param day 535 * The day. 536 * @param hour 537 * The hour. 538 * @param minute 539 * The minute. 540 * @param second 541 * The second. 542 * @param tz 543 * The timezone. 544 * @return A generalized time representing the provided date / time 545 * parameters. 546 * @throws LocalizedIllegalArgumentException 547 * If the generalized time could not be created. 548 */ 549 private static GeneralizedTime createTime(final String value, final int year, final int month, 550 final int day, final int hour, final int minute, final int second, final TimeZone tz) { 551 try { 552 final GregorianCalendar calendar = new GregorianCalendar(); 553 calendar.setLenient(false); 554 calendar.setTimeZone(tz); 555 calendar.set(year, month, day, hour, minute, second); 556 calendar.set(Calendar.MILLISECOND, 0); 557 return new GeneralizedTime(calendar, null, Long.MIN_VALUE, value); 558 } catch (final Exception e) { 559 // This should only happen if the provided date wasn't legal 560 // (e.g., September 31). 561 final LocalizableMessage message = 562 WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e)); 563 throw new LocalizedIllegalArgumentException(message, e); 564 } 565 } 566 567 /** 568 * Completes decoding the generalized time value containing a fractional 569 * component. It will also decode the trailing 'Z' or offset. 570 * 571 * @param value 572 * The whole value, including the fractional component and time 573 * zone information. 574 * @param startPos 575 * The position of the first character after the period in the 576 * value string. 577 * @param year 578 * The year decoded from the provided value. 579 * @param month 580 * The month decoded from the provided value. 581 * @param day 582 * The day decoded from the provided value. 583 * @param hour 584 * The hour decoded from the provided value. 585 * @param minute 586 * The minute decoded from the provided value. 587 * @param second 588 * The second decoded from the provided value. 589 * @param multiplier 590 * The multiplier value that should be used to scale the fraction 591 * appropriately. If it's a fraction of an hour, then it should 592 * be 3600000 (60*60*1000). If it's a fraction of a minute, then 593 * it should be 60000. If it's a fraction of a second, then it 594 * should be 1000. 595 * @return The timestamp created from the provided generalized time value 596 * including the fractional element. 597 * @throws LocalizedIllegalArgumentException 598 * If the provided value cannot be parsed as a valid generalized 599 * time string. 600 */ 601 private static GeneralizedTime finishDecodingFraction(final String value, final int startPos, 602 final int year, final int month, final int day, final int hour, final int minute, 603 final int second, final int multiplier) { 604 final int length = value.length(); 605 final StringBuilder fractionBuffer = new StringBuilder((2 + length) - startPos); 606 fractionBuffer.append("0."); 607 608 TimeZone timeZone = null; 609 610 outerLoop: 611 for (int i = startPos; i < length; i++) { 612 final char c = value.charAt(i); 613 switch (c) { 614 case '0': 615 case '1': 616 case '2': 617 case '3': 618 case '4': 619 case '5': 620 case '6': 621 case '7': 622 case '8': 623 case '9': 624 fractionBuffer.append(c); 625 break; 626 627 case 'Z': 628 case 'z': 629 // This is only acceptable if we're at the end of the value. 630 if (i != (value.length() - 1)) { 631 final LocalizableMessage message = 632 WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value, 633 String.valueOf(c)); 634 throw new LocalizedIllegalArgumentException(message); 635 } 636 637 timeZone = TIME_ZONE_UTC_OBJ; 638 break outerLoop; 639 640 case '+': 641 case '-': 642 timeZone = getTimeZoneForOffset(value, i); 643 break outerLoop; 644 645 default: 646 final LocalizableMessage message = 647 WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value, String 648 .valueOf(c)); 649 throw new LocalizedIllegalArgumentException(message); 650 } 651 } 652 653 if (fractionBuffer.length() == 2) { 654 final LocalizableMessage message = 655 WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION.get(value); 656 throw new LocalizedIllegalArgumentException(message); 657 } 658 659 if (timeZone == null) { 660 final LocalizableMessage message = 661 WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO.get(value); 662 throw new LocalizedIllegalArgumentException(message); 663 } 664 665 final Double fractionValue = Double.parseDouble(fractionBuffer.toString()); 666 final int additionalMilliseconds = (int) Math.round(fractionValue * multiplier); 667 668 try { 669 final GregorianCalendar calendar = new GregorianCalendar(); 670 calendar.setLenient(false); 671 calendar.setTimeZone(timeZone); 672 calendar.set(year, month, day, hour, minute, second); 673 calendar.set(Calendar.MILLISECOND, additionalMilliseconds); 674 return new GeneralizedTime(calendar, null, Long.MIN_VALUE, value); 675 } catch (final Exception e) { 676 // This should only happen if the provided date wasn't legal 677 // (e.g., September 31). 678 final LocalizableMessage message = 679 WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e)); 680 throw new LocalizedIllegalArgumentException(message, e); 681 } 682 } 683 684 /** 685 * Decodes a time zone offset from the provided value. 686 * 687 * @param value 688 * The whole value, including the offset. 689 * @param startPos 690 * The position of the first character that is contained in the 691 * offset. This should be the position of the plus or minus 692 * character. 693 * @return The {@code TimeZone} object representing the decoded time zone. 694 */ 695 private static TimeZone getTimeZoneForOffset(final String value, final int startPos) { 696 final String offSetStr = value.substring(startPos); 697 final int len = offSetStr.length(); 698 if (len != 3 && len != 5) { 699 final LocalizableMessage message = 700 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 701 throw new LocalizedIllegalArgumentException(message); 702 } 703 704 // The first character must be either a plus or minus. 705 switch (offSetStr.charAt(0)) { 706 case '+': 707 case '-': 708 // These are OK. 709 break; 710 711 default: 712 final LocalizableMessage message = 713 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 714 throw new LocalizedIllegalArgumentException(message); 715 } 716 717 // The first two characters must be an integer between 00 and 23. 718 switch (offSetStr.charAt(1)) { 719 case '0': 720 case '1': 721 switch (offSetStr.charAt(2)) { 722 case '0': 723 case '1': 724 case '2': 725 case '3': 726 case '4': 727 case '5': 728 case '6': 729 case '7': 730 case '8': 731 case '9': 732 // These are all fine. 733 break; 734 735 default: 736 final LocalizableMessage message = 737 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 738 throw new LocalizedIllegalArgumentException(message); 739 } 740 break; 741 742 case '2': 743 switch (offSetStr.charAt(2)) { 744 case '0': 745 case '1': 746 case '2': 747 case '3': 748 // These are all fine. 749 break; 750 751 default: 752 final LocalizableMessage message = 753 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 754 throw new LocalizedIllegalArgumentException(message); 755 } 756 break; 757 758 default: 759 final LocalizableMessage message = 760 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 761 throw new LocalizedIllegalArgumentException(message); 762 } 763 764 // If there are two more characters, then they must be an integer 765 // between 00 and 59. 766 if (offSetStr.length() == 5) { 767 switch (offSetStr.charAt(3)) { 768 case '0': 769 case '1': 770 case '2': 771 case '3': 772 case '4': 773 case '5': 774 switch (offSetStr.charAt(4)) { 775 case '0': 776 case '1': 777 case '2': 778 case '3': 779 case '4': 780 case '5': 781 case '6': 782 case '7': 783 case '8': 784 case '9': 785 // These are all fine. 786 break; 787 788 default: 789 final LocalizableMessage message = 790 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 791 throw new LocalizedIllegalArgumentException(message); 792 } 793 break; 794 795 default: 796 final LocalizableMessage message = 797 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr); 798 throw new LocalizedIllegalArgumentException(message); 799 } 800 } 801 802 // If we've gotten here, then it looks like a valid offset. We can 803 // create a time zone by using "GMT" followed by the offset. 804 return TimeZone.getTimeZone("GMT" + offSetStr); 805 } 806 807 /** Lazily constructed internal representations. */ 808 private volatile Calendar calendar; 809 private volatile Date date; 810 private volatile String stringValue; 811 private volatile long timeMS; 812 813 private GeneralizedTime(final Calendar calendar, final Date date, final long time, 814 final String stringValue) { 815 this.calendar = calendar; 816 this.date = date; 817 this.timeMS = time; 818 this.stringValue = stringValue; 819 } 820 821 /** {@inheritDoc} */ 822 @Override 823 public int compareTo(final GeneralizedTime o) { 824 final Long timeMS1 = getTimeInMillis(); 825 final Long timeMS2 = o.getTimeInMillis(); 826 return timeMS1.compareTo(timeMS2); 827 } 828 829 /** {@inheritDoc} */ 830 @Override 831 public boolean equals(final Object obj) { 832 if (this == obj) { 833 return true; 834 } else if (obj instanceof GeneralizedTime) { 835 return getTimeInMillis() == ((GeneralizedTime) obj).getTimeInMillis(); 836 } else { 837 return false; 838 } 839 } 840 841 /** 842 * Returns the value of this generalized time in milliseconds since the 843 * epoch. 844 * 845 * @return The value of this generalized time in milliseconds since the 846 * epoch. 847 */ 848 public long getTimeInMillis() { 849 long tmpTimeMS = timeMS; 850 if (tmpTimeMS == Long.MIN_VALUE) { 851 if (date != null) { 852 tmpTimeMS = date.getTime(); 853 } else { 854 tmpTimeMS = calendar.getTimeInMillis(); 855 } 856 timeMS = tmpTimeMS; 857 } 858 return tmpTimeMS; 859 } 860 861 /** {@inheritDoc} */ 862 @Override 863 public int hashCode() { 864 return ((Long) getTimeInMillis()).hashCode(); 865 } 866 867 /** 868 * Returns a {@code Calendar} representation of this generalized time. 869 * <p> 870 * Subsequent modifications to the returned calendar will not alter the 871 * internal state of this generalized time. 872 * 873 * @return A {@code Calendar} representation of this generalized time. 874 */ 875 public Calendar toCalendar() { 876 return (Calendar) getCalendar().clone(); 877 } 878 879 /** 880 * Returns a {@code Date} representation of this generalized time. 881 * <p> 882 * Subsequent modifications to the returned date will not alter the internal 883 * state of this generalized time. 884 * 885 * @return A {@code Date} representation of this generalized time. 886 */ 887 public Date toDate() { 888 Date tmpDate = date; 889 if (tmpDate == null) { 890 tmpDate = new Date(getTimeInMillis()); 891 date = tmpDate; 892 } 893 return (Date) tmpDate.clone(); 894 } 895 896 /** {@inheritDoc} */ 897 @Override 898 public String toString() { 899 String tmpString = stringValue; 900 if (tmpString == null) { 901 // Do this in a thread-safe non-synchronized fashion. 902 // (Simple)DateFormat is neither fast nor thread-safe. 903 final StringBuilder sb = new StringBuilder(19); 904 final Calendar tmpCalendar = getCalendar(); 905 906 // Format the year yyyy. 907 int n = tmpCalendar.get(Calendar.YEAR); 908 if (n < 0) { 909 throw new IllegalArgumentException("Year cannot be < 0:" + n); 910 } else if (n < 10) { 911 sb.append("000"); 912 } else if (n < 100) { 913 sb.append("00"); 914 } else if (n < 1000) { 915 sb.append("0"); 916 } 917 sb.append(n); 918 919 // Format the month MM. 920 n = tmpCalendar.get(Calendar.MONTH) + 1; 921 if (n < 10) { 922 sb.append("0"); 923 } 924 sb.append(n); 925 926 // Format the day dd. 927 n = tmpCalendar.get(Calendar.DAY_OF_MONTH); 928 if (n < 10) { 929 sb.append("0"); 930 } 931 sb.append(n); 932 933 // Format the hour HH. 934 n = tmpCalendar.get(Calendar.HOUR_OF_DAY); 935 if (n < 10) { 936 sb.append("0"); 937 } 938 sb.append(n); 939 940 // Format the minute mm. 941 n = tmpCalendar.get(Calendar.MINUTE); 942 if (n < 10) { 943 sb.append("0"); 944 } 945 sb.append(n); 946 947 // Format the seconds ss. 948 n = tmpCalendar.get(Calendar.SECOND); 949 if (n < 10) { 950 sb.append("0"); 951 } 952 sb.append(n); 953 954 // Format the milli-seconds. 955 n = tmpCalendar.get(Calendar.MILLISECOND); 956 if (n != 0) { 957 sb.append('.'); 958 if (n < 10) { 959 sb.append("00"); 960 } else if (n < 100) { 961 sb.append("0"); 962 } 963 sb.append(n); 964 } 965 966 // Format the timezone. 967 n = tmpCalendar.get(Calendar.ZONE_OFFSET) + tmpCalendar.get(Calendar.DST_OFFSET); 968 if (n == 0) { 969 sb.append('Z'); 970 } else { 971 if (n < 0) { 972 sb.append('-'); 973 n = -n; 974 } else { 975 sb.append('+'); 976 } 977 n = n / 60000; // Minutes. 978 979 final int h = n / 60; 980 if (h < 10) { 981 sb.append("0"); 982 } 983 sb.append(h); 984 985 final int m = n % 60; 986 if (m < 10) { 987 sb.append("0"); 988 } 989 sb.append(m); 990 } 991 tmpString = sb.toString(); 992 stringValue = tmpString; 993 } 994 return tmpString; 995 } 996 997 private Calendar getCalendar() { 998 Calendar tmpCalendar = calendar; 999 if (tmpCalendar == null) { 1000 tmpCalendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ); 1001 tmpCalendar.setLenient(false); 1002 tmpCalendar.setTimeInMillis(getTimeInMillis()); 1003 calendar = tmpCalendar; 1004 } 1005 return tmpCalendar; 1006 } 1007}