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