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