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