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 2006-2009 Sun Microsystems, Inc. 025 * Portions Copyright 2009 D. J. Hagberg, Millibits Consulting, Inc. 026 * Portions Copyright 2012-2015 ForgeRock AS 027 */ 028package org.opends.server.schema; 029 030import static org.opends.messages.SchemaMessages.*; 031import static org.opends.server.schema.SchemaConstants.*; 032import static org.opends.server.util.ServerConstants.*; 033 034import java.util.Calendar; 035import java.util.Date; 036import java.util.GregorianCalendar; 037import java.util.TimeZone; 038 039import org.forgerock.i18n.LocalizableMessage; 040import org.forgerock.i18n.slf4j.LocalizedLogger; 041import org.forgerock.opendj.ldap.ByteSequence; 042import org.forgerock.opendj.ldap.ByteString; 043import org.forgerock.opendj.ldap.ResultCode; 044import org.forgerock.opendj.ldap.schema.Schema; 045import org.forgerock.opendj.ldap.schema.Syntax; 046import org.opends.server.admin.std.server.AttributeSyntaxCfg; 047import org.opends.server.api.AttributeSyntax; 048import org.opends.server.types.DirectoryException; 049 050/** 051 * This class defines the generalized time attribute syntax, which is a way of 052 * representing time in a form like "YYYYMMDDhhmmssZ". The actual form is 053 * somewhat flexible, and may omit the minute and second information, or may 054 * include sub-second information. It may also replace "Z" with a time zone 055 * offset like "-0500" for representing values that are not in UTC. 056 */ 057public class GeneralizedTimeSyntax 058 extends AttributeSyntax<AttributeSyntaxCfg> 059{ 060 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 061 062 /** UTC TimeZone is assumed to never change over JVM lifetime. */ 063 private static final TimeZone TIME_ZONE_UTC_OBJ = 064 TimeZone.getTimeZone(TIME_ZONE_UTC); 065 066 /** 067 * Creates a new instance of this syntax. Note that the only thing that 068 * should be done here is to invoke the default constructor for the 069 * superclass. All initialization should be performed in the 070 * <CODE>initializeSyntax</CODE> method. 071 */ 072 public GeneralizedTimeSyntax() 073 { 074 super(); 075 } 076 077 /** {@inheritDoc} */ 078 @Override 079 public Syntax getSDKSyntax(Schema schema) 080 { 081 return schema.getSyntax(SchemaConstants.SYNTAX_GENERALIZED_TIME_OID); 082 } 083 084 /** 085 * Retrieves the common name for this attribute syntax. 086 * 087 * @return The common name for this attribute syntax. 088 */ 089 @Override 090 public String getName() 091 { 092 return SYNTAX_GENERALIZED_TIME_NAME; 093 } 094 095 /** 096 * Retrieves the OID for this attribute syntax. 097 * 098 * @return The OID for this attribute syntax. 099 */ 100 @Override 101 public String getOID() 102 { 103 return SYNTAX_GENERALIZED_TIME_OID; 104 } 105 106 /** 107 * Retrieves a description for this attribute syntax. 108 * 109 * @return A description for this attribute syntax. 110 */ 111 @Override 112 public String getDescription() 113 { 114 return SYNTAX_GENERALIZED_TIME_DESCRIPTION; 115 } 116 117 /** 118 * Retrieves the generalized time representation of the provided date. 119 * 120 * @param d The date to retrieve in generalized time form. 121 * 122 * @return The generalized time representation of the provided date. 123 */ 124 public static String format(Date d) 125 { 126 return d == null ? null : format(d.getTime()); 127 } 128 129 /** 130 * Retrieves the generalized time representation of the provided date. 131 * 132 * @param t The timestamp to retrieve in generalized time form. 133 * 134 * @return The generalized time representation of the provided date. 135 */ 136 public static String format(long t) 137 { 138 // Generalized time has the format yyyyMMddHHmmss.SSS'Z' 139 140 // Do this in a thread-safe non-synchronized fashion. 141 // (Simple)DateFormat is neither fast nor thread-safe. 142 143 StringBuilder sb = new StringBuilder(19); 144 145 GregorianCalendar calendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ); 146 calendar.setLenient(false); 147 calendar.setTimeInMillis(t); 148 149 // Format the year yyyy. 150 int n = calendar.get(Calendar.YEAR); 151 if (n < 0) 152 { 153 throw new IllegalArgumentException("Year cannot be < 0:" + n); 154 } 155 else if (n < 10) 156 { 157 sb.append("000"); 158 } 159 else if (n < 100) 160 { 161 sb.append("00"); 162 } 163 else if (n < 1000) 164 { 165 sb.append("0"); 166 } 167 sb.append(n); 168 169 // Format the month MM. 170 n = calendar.get(Calendar.MONTH) + 1; 171 if (n < 10) 172 { 173 sb.append("0"); 174 } 175 sb.append(n); 176 177 // Format the day dd. 178 n = calendar.get(Calendar.DAY_OF_MONTH); 179 if (n < 10) 180 { 181 sb.append("0"); 182 } 183 sb.append(n); 184 185 // Format the hour HH. 186 n = calendar.get(Calendar.HOUR_OF_DAY); 187 if (n < 10) 188 { 189 sb.append("0"); 190 } 191 sb.append(n); 192 193 // Format the minute mm. 194 n = calendar.get(Calendar.MINUTE); 195 if (n < 10) 196 { 197 sb.append("0"); 198 } 199 sb.append(n); 200 201 // Format the seconds ss. 202 n = calendar.get(Calendar.SECOND); 203 if (n < 10) 204 { 205 sb.append("0"); 206 } 207 sb.append(n); 208 209 // Format the milli-seconds. 210 sb.append('.'); 211 n = calendar.get(Calendar.MILLISECOND); 212 if (n < 10) 213 { 214 sb.append("00"); 215 } 216 else if (n < 100) 217 { 218 sb.append("0"); 219 } 220 sb.append(n); 221 222 // Format the timezone (always Z). 223 sb.append('Z'); 224 225 return sb.toString(); 226 } 227 228 /** 229 * Retrieves an attribute value containing a generalized time representation 230 * of the provided date. 231 * 232 * @param time The time for which to retrieve the generalized time value. 233 * 234 * @return The attribute value created from the date. 235 */ 236 public static ByteString createGeneralizedTimeValue(long time) 237 { 238 return ByteString.valueOfUtf8(format(time)); 239 } 240 241 /** 242 * Decodes the provided normalized value as a generalized time value and 243 * retrieves a timestamp containing its representation. 244 * 245 * @param value The normalized value to decode using the generalized time 246 * syntax. 247 * 248 * @return The timestamp created from the provided generalized time value. 249 * 250 * @throws DirectoryException If the provided value cannot be parsed as a 251 * valid generalized time string. 252 */ 253 public static long decodeGeneralizedTimeValue(ByteSequence value) 254 throws DirectoryException 255 { 256 int year = 0; 257 int month = 0; 258 int day = 0; 259 int hour = 0; 260 int minute = 0; 261 int second = 0; 262 263 264 // Get the value as a string and verify that it is at least long enough for 265 // "YYYYMMDDhhZ", which is the shortest allowed value. 266 String valueString = value.toString().toUpperCase(); 267 int length = valueString.length(); 268 if (length < 11) 269 { 270 LocalizableMessage message = 271 WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT.get(valueString); 272 throw new DirectoryException( 273 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 274 } 275 276 277 // The first four characters are the century and year, and they must be 278 // numeric digits between 0 and 9. 279 for (int i=0; i < 4; i++) 280 { 281 switch (valueString.charAt(i)) 282 { 283 case '0': 284 year = (year * 10); 285 break; 286 287 case '1': 288 year = (year * 10) + 1; 289 break; 290 291 case '2': 292 year = (year * 10) + 2; 293 break; 294 295 case '3': 296 year = (year * 10) + 3; 297 break; 298 299 case '4': 300 year = (year * 10) + 4; 301 break; 302 303 case '5': 304 year = (year * 10) + 5; 305 break; 306 307 case '6': 308 year = (year * 10) + 6; 309 break; 310 311 case '7': 312 year = (year * 10) + 7; 313 break; 314 315 case '8': 316 year = (year * 10) + 8; 317 break; 318 319 case '9': 320 year = (year * 10) + 9; 321 break; 322 323 default: 324 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR.get( 325 valueString, valueString.charAt(i)); 326 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 327 } 328 } 329 330 331 // The next two characters are the month, and they must form the string 332 // representation of an integer between 01 and 12. 333 char m1 = valueString.charAt(4); 334 char m2 = valueString.charAt(5); 335 switch (m1) 336 { 337 case '0': 338 // m2 must be a digit between 1 and 9. 339 switch (m2) 340 { 341 case '1': 342 month = Calendar.JANUARY; 343 break; 344 345 case '2': 346 month = Calendar.FEBRUARY; 347 break; 348 349 case '3': 350 month = Calendar.MARCH; 351 break; 352 353 case '4': 354 month = Calendar.APRIL; 355 break; 356 357 case '5': 358 month = Calendar.MAY; 359 break; 360 361 case '6': 362 month = Calendar.JUNE; 363 break; 364 365 case '7': 366 month = Calendar.JULY; 367 break; 368 369 case '8': 370 month = Calendar.AUGUST; 371 break; 372 373 case '9': 374 month = Calendar.SEPTEMBER; 375 break; 376 377 default: 378 LocalizableMessage message = 379 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, 380 valueString.substring(4, 6)); 381 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 382 message); 383 } 384 break; 385 case '1': 386 // m2 must be a digit between 0 and 2. 387 switch (m2) 388 { 389 case '0': 390 month = Calendar.OCTOBER; 391 break; 392 393 case '1': 394 month = Calendar.NOVEMBER; 395 break; 396 397 case '2': 398 month = Calendar.DECEMBER; 399 break; 400 401 default: 402 LocalizableMessage message = 403 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, 404 valueString.substring(4, 6)); 405 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 406 message); 407 } 408 break; 409 default: 410 LocalizableMessage message = 411 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, 412 valueString.substring(4, 6)); 413 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 414 message); 415 } 416 417 418 // The next two characters should be the day of the month, and they must 419 // form the string representation of an integer between 01 and 31. 420 // This doesn't do any validation against the year or month, so it will 421 // allow dates like April 31, or February 29 in a non-leap year, but we'll 422 // let those slide. 423 char d1 = valueString.charAt(6); 424 char d2 = valueString.charAt(7); 425 switch (d1) 426 { 427 case '0': 428 // d2 must be a digit between 1 and 9. 429 switch (d2) 430 { 431 case '1': 432 day = 1; 433 break; 434 435 case '2': 436 day = 2; 437 break; 438 439 case '3': 440 day = 3; 441 break; 442 443 case '4': 444 day = 4; 445 break; 446 447 case '5': 448 day = 5; 449 break; 450 451 case '6': 452 day = 6; 453 break; 454 455 case '7': 456 day = 7; 457 break; 458 459 case '8': 460 day = 8; 461 break; 462 463 case '9': 464 day = 9; 465 break; 466 467 default: 468 LocalizableMessage message = 469 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, 470 valueString.substring(6, 8)); 471 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 472 message); 473 } 474 break; 475 476 case '1': 477 // d2 must be a digit between 0 and 9. 478 switch (d2) 479 { 480 case '0': 481 day = 10; 482 break; 483 484 case '1': 485 day = 11; 486 break; 487 488 case '2': 489 day = 12; 490 break; 491 492 case '3': 493 day = 13; 494 break; 495 496 case '4': 497 day = 14; 498 break; 499 500 case '5': 501 day = 15; 502 break; 503 504 case '6': 505 day = 16; 506 break; 507 508 case '7': 509 day = 17; 510 break; 511 512 case '8': 513 day = 18; 514 break; 515 516 case '9': 517 day = 19; 518 break; 519 520 default: 521 LocalizableMessage message = 522 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, 523 valueString.substring(6, 8)); 524 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 525 message); 526 } 527 break; 528 529 case '2': 530 // d2 must be a digit between 0 and 9. 531 switch (d2) 532 { 533 case '0': 534 day = 20; 535 break; 536 537 case '1': 538 day = 21; 539 break; 540 541 case '2': 542 day = 22; 543 break; 544 545 case '3': 546 day = 23; 547 break; 548 549 case '4': 550 day = 24; 551 break; 552 553 case '5': 554 day = 25; 555 break; 556 557 case '6': 558 day = 26; 559 break; 560 561 case '7': 562 day = 27; 563 break; 564 565 case '8': 566 day = 28; 567 break; 568 569 case '9': 570 day = 29; 571 break; 572 573 default: 574 LocalizableMessage message = 575 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, 576 valueString.substring(6, 8)); 577 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 578 message); 579 } 580 break; 581 582 case '3': 583 // d2 must be either 0 or 1. 584 switch (d2) 585 { 586 case '0': 587 day = 30; 588 break; 589 590 case '1': 591 day = 31; 592 break; 593 594 default: 595 LocalizableMessage message = 596 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, 597 valueString.substring(6, 8)); 598 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 599 message); 600 } 601 break; 602 603 default: 604 LocalizableMessage message = 605 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, 606 valueString.substring(6, 8)); 607 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 608 message); 609 } 610 611 612 // The next two characters must be the hour, and they must form the string 613 // representation of an integer between 00 and 23. 614 char h1 = valueString.charAt(8); 615 char h2 = valueString.charAt(9); 616 switch (h1) 617 { 618 case '0': 619 switch (h2) 620 { 621 case '0': 622 hour = 0; 623 break; 624 625 case '1': 626 hour = 1; 627 break; 628 629 case '2': 630 hour = 2; 631 break; 632 633 case '3': 634 hour = 3; 635 break; 636 637 case '4': 638 hour = 4; 639 break; 640 641 case '5': 642 hour = 5; 643 break; 644 645 case '6': 646 hour = 6; 647 break; 648 649 case '7': 650 hour = 7; 651 break; 652 653 case '8': 654 hour = 8; 655 break; 656 657 case '9': 658 hour = 9; 659 break; 660 661 default: 662 LocalizableMessage message = 663 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, 664 valueString.substring(8, 10)); 665 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 666 message); 667 } 668 break; 669 670 case '1': 671 switch (h2) 672 { 673 case '0': 674 hour = 10; 675 break; 676 677 case '1': 678 hour = 11; 679 break; 680 681 case '2': 682 hour = 12; 683 break; 684 685 case '3': 686 hour = 13; 687 break; 688 689 case '4': 690 hour = 14; 691 break; 692 693 case '5': 694 hour = 15; 695 break; 696 697 case '6': 698 hour = 16; 699 break; 700 701 case '7': 702 hour = 17; 703 break; 704 705 case '8': 706 hour = 18; 707 break; 708 709 case '9': 710 hour = 19; 711 break; 712 713 default: 714 LocalizableMessage message = 715 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, 716 valueString.substring(8, 10)); 717 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 718 message); 719 } 720 break; 721 722 case '2': 723 switch (h2) 724 { 725 case '0': 726 hour = 20; 727 break; 728 729 case '1': 730 hour = 21; 731 break; 732 733 case '2': 734 hour = 22; 735 break; 736 737 case '3': 738 hour = 23; 739 break; 740 741 default: 742 LocalizableMessage message = 743 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, 744 valueString.substring(8, 10)); 745 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 746 message); 747 } 748 break; 749 750 default: 751 LocalizableMessage message = 752 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, 753 valueString.substring(8, 10)); 754 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 755 message); 756 } 757 758 759 // Next, there should be either two digits comprising an integer between 00 760 // and 59 (for the minute), a letter 'Z' (for the UTC specifier), a plus 761 // or minus sign followed by two or four digits (for the UTC offset), or a 762 // period or comma representing the fraction. 763 m1 = valueString.charAt(10); 764 switch (m1) 765 { 766 case '0': 767 case '1': 768 case '2': 769 case '3': 770 case '4': 771 case '5': 772 // There must be at least two more characters, and the next one must 773 // be a digit between 0 and 9. 774 if (length < 13) 775 { 776 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10); 777 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 778 } 779 780 781 minute = 10 * (m1 - '0'); 782 783 switch (valueString.charAt(11)) 784 { 785 case '0': 786 break; 787 788 case '1': 789 minute += 1; 790 break; 791 792 case '2': 793 minute += 2; 794 break; 795 796 case '3': 797 minute += 3; 798 break; 799 800 case '4': 801 minute += 4; 802 break; 803 804 case '5': 805 minute += 5; 806 break; 807 808 case '6': 809 minute += 6; 810 break; 811 812 case '7': 813 minute += 7; 814 break; 815 816 case '8': 817 minute += 8; 818 break; 819 820 case '9': 821 minute += 9; 822 break; 823 824 default: 825 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE. 826 get(valueString, 827 valueString.substring(10, 12)); 828 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 829 message); 830 } 831 832 break; 833 834 case 'Z': 835 // This is fine only if we are at the end of the value. 836 if (length == 11) 837 { 838 try 839 { 840 GregorianCalendar calendar = new GregorianCalendar(); 841 calendar.setLenient(false); 842 calendar.setTimeZone(TIME_ZONE_UTC_OBJ); 843 calendar.set(year, month, day, hour, minute, second); 844 calendar.set(Calendar.MILLISECOND, 0); 845 return calendar.getTimeInMillis(); 846 } 847 catch (Exception e) 848 { 849 logger.traceException(e); 850 851 // This should only happen if the provided date wasn't legal 852 // (e.g., September 31). 853 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(valueString, e); 854 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message, e); 855 } 856 } 857 else 858 { 859 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10); 860 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 861 } 862 863 case '+': 864 case '-': 865 // These are fine only if there are exactly two or four more digits that 866 // specify a valid offset. 867 if (length == 13 || length == 15) 868 { 869 try 870 { 871 GregorianCalendar calendar = new GregorianCalendar(); 872 calendar.setLenient(false); 873 calendar.setTimeZone(getTimeZoneForOffset(valueString, 10)); 874 calendar.set(year, month, day, hour, minute, second); 875 calendar.set(Calendar.MILLISECOND, 0); 876 return calendar.getTimeInMillis(); 877 } 878 catch (Exception e) 879 { 880 logger.traceException(e); 881 882 // This should only happen if the provided date wasn't legal 883 // (e.g., September 31). 884 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME. 885 get(valueString, e); 886 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message, e); 887 } 888 } 889 else 890 { 891 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10); 892 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 893 } 894 895 case '.': 896 case ',': 897 return finishDecodingFraction(valueString, 11, year, month, day, hour, 898 minute, second, 3600000); 899 900 default: 901 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10); 902 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 903 } 904 905 906 // Next, there should be either two digits comprising an integer between 00 907 // and 60 (for the second, including a possible leap second), a letter 'Z' 908 // (for the UTC specifier), a plus or minus sign followed by two or four 909 // digits (for the UTC offset), or a period or comma to start the fraction. 910 char s1 = valueString.charAt(12); 911 switch (s1) 912 { 913 case '0': 914 case '1': 915 case '2': 916 case '3': 917 case '4': 918 case '5': 919 // There must be at least two more characters, and the next one must 920 // be a digit between 0 and 9. 921 if (length < 15) 922 { 923 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12); 924 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 925 } 926 927 928 second = 10 * (s1 - '0'); 929 930 switch (valueString.charAt(13)) 931 { 932 case '0': 933 break; 934 935 case '1': 936 second += 1; 937 break; 938 939 case '2': 940 second += 2; 941 break; 942 943 case '3': 944 second += 3; 945 break; 946 947 case '4': 948 second += 4; 949 break; 950 951 case '5': 952 second += 5; 953 break; 954 955 case '6': 956 second += 6; 957 break; 958 959 case '7': 960 second += 7; 961 break; 962 963 case '8': 964 second += 8; 965 break; 966 967 case '9': 968 second += 9; 969 break; 970 971 default: 972 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE. 973 get(valueString, 974 valueString.substring(12, 14)); 975 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 976 message); 977 } 978 979 break; 980 981 case '6': 982 // There must be at least two more characters and the next one must be 983 // a 0. 984 if (length < 15) 985 { 986 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12); 987 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 988 message); 989 } 990 991 if (valueString.charAt(13) != '0') 992 { 993 LocalizableMessage message = 994 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND.get(valueString, 995 valueString.substring(12, 14)); 996 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 997 message); 998 } 999 1000 second = 60; 1001 break; 1002 1003 case 'Z': 1004 // This is fine only if we are at the end of the value. 1005 if (length == 13) 1006 { 1007 try 1008 { 1009 GregorianCalendar calendar = new GregorianCalendar(); 1010 calendar.setLenient(false); 1011 calendar.setTimeZone(TIME_ZONE_UTC_OBJ); 1012 calendar.set(year, month, day, hour, minute, second); 1013 calendar.set(Calendar.MILLISECOND, 0); 1014 return calendar.getTimeInMillis(); 1015 } 1016 catch (Exception e) 1017 { 1018 logger.traceException(e); 1019 1020 // This should only happen if the provided date wasn't legal 1021 // (e.g., September 31). 1022 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME. 1023 get(valueString, e); 1024 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1025 message, e); 1026 } 1027 } 1028 else 1029 { 1030 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12); 1031 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1032 message); 1033 } 1034 1035 case '+': 1036 case '-': 1037 // These are fine only if there are exactly two or four more digits that 1038 // specify a valid offset. 1039 if (length == 15 || length == 17) 1040 { 1041 try 1042 { 1043 GregorianCalendar calendar = new GregorianCalendar(); 1044 calendar.setLenient(false); 1045 calendar.setTimeZone(getTimeZoneForOffset(valueString, 12)); 1046 calendar.set(year, month, day, hour, minute, second); 1047 calendar.set(Calendar.MILLISECOND, 0); 1048 return calendar.getTimeInMillis(); 1049 } 1050 catch (Exception e) 1051 { 1052 logger.traceException(e); 1053 1054 // This should only happen if the provided date wasn't legal 1055 // (e.g., September 31). 1056 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME. 1057 get(valueString, e); 1058 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1059 message, e); 1060 } 1061 } 1062 else 1063 { 1064 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12); 1065 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1066 message); 1067 } 1068 1069 case '.': 1070 case ',': 1071 return finishDecodingFraction(valueString, 13, year, month, day, hour, 1072 minute, second, 60000); 1073 1074 default: 1075 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12); 1076 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1077 message); 1078 } 1079 1080 1081 // Next, there should be either a period or comma followed by between one 1082 // and three digits (to specify the sub-second), a letter 'Z' (for the UTC 1083 // specifier), or a plus or minus sign followed by two our four digits (for 1084 // the UTC offset). 1085 switch (valueString.charAt(14)) 1086 { 1087 case '.': 1088 case ',': 1089 return finishDecodingFraction(valueString, 15, year, month, day, hour, 1090 minute, second, 1000); 1091 1092 case 'Z': 1093 // This is fine only if we are at the end of the value. 1094 if (length == 15) 1095 { 1096 try 1097 { 1098 GregorianCalendar calendar = new GregorianCalendar(); 1099 calendar.setLenient(false); 1100 calendar.setTimeZone(TIME_ZONE_UTC_OBJ); 1101 calendar.set(year, month, day, hour, minute, second); 1102 calendar.set(Calendar.MILLISECOND, 0); 1103 return calendar.getTimeInMillis(); 1104 } 1105 catch (Exception e) 1106 { 1107 logger.traceException(e); 1108 1109 // This should only happen if the provided date wasn't legal 1110 // (e.g., September 31). 1111 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME. 1112 get(valueString, e); 1113 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1114 message, e); 1115 } 1116 } 1117 else 1118 { 1119 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get( 1120 valueString, valueString.charAt(14), 14); 1121 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1122 message); 1123 } 1124 1125 case '+': 1126 case '-': 1127 // These are fine only if there are exactly two or four more digits that 1128 // specify a valid offset. 1129 if (length == 17 || length == 19) 1130 { 1131 try 1132 { 1133 GregorianCalendar calendar = new GregorianCalendar(); 1134 calendar.setLenient(false); 1135 calendar.setTimeZone(getTimeZoneForOffset(valueString, 14)); 1136 calendar.set(year, month, day, hour, minute, second); 1137 calendar.set(Calendar.MILLISECOND, 0); 1138 return calendar.getTimeInMillis(); 1139 } 1140 catch (Exception e) 1141 { 1142 logger.traceException(e); 1143 1144 // This should only happen if the provided date wasn't legal 1145 // (e.g., September 31). 1146 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME. 1147 get(valueString, e); 1148 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1149 message, e); 1150 } 1151 } 1152 else 1153 { 1154 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get( 1155 valueString, valueString.charAt(14), 14); 1156 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1157 message); 1158 } 1159 1160 default: 1161 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get( 1162 valueString, valueString.charAt(14), 14); 1163 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1164 message); 1165 } 1166 } 1167 1168 /** 1169 * Completes decoding the generalized time value containing a fractional 1170 * component. It will also decode the trailing 'Z' or offset. 1171 * 1172 * @param value The whole value, including the fractional component and 1173 * time zone information. 1174 * @param startPos The position of the first character after the period 1175 * in the value string. 1176 * @param year The year decoded from the provided value. 1177 * @param month The month decoded from the provided value. 1178 * @param day The day decoded from the provided value. 1179 * @param hour The hour decoded from the provided value. 1180 * @param minute The minute decoded from the provided value. 1181 * @param second The second decoded from the provided value. 1182 * @param multiplier The multiplier value that should be used to scale the 1183 * fraction appropriately. If it's a fraction of an hour, 1184 * then it should be 3600000 (60*60*1000). If it's a 1185 * fraction of a minute, then it should be 60000. If it's 1186 * a fraction of a second, then it should be 1000. 1187 * 1188 * @return The timestamp created from the provided generalized time value 1189 * including the fractional element. 1190 * 1191 * @throws DirectoryException If the provided value cannot be parsed as a 1192 * valid generalized time string. 1193 */ 1194 private static long finishDecodingFraction(String value, int startPos, 1195 int year, int month, int day, 1196 int hour, int minute, int second, 1197 int multiplier) 1198 throws DirectoryException 1199 { 1200 int length = value.length(); 1201 StringBuilder fractionBuffer = new StringBuilder(2 + length - startPos); 1202 fractionBuffer.append("0."); 1203 1204 TimeZone timeZone = null; 1205 1206outerLoop: 1207 for (int i=startPos; i < length; i++) 1208 { 1209 char c = value.charAt(i); 1210 switch (c) 1211 { 1212 case '0': 1213 case '1': 1214 case '2': 1215 case '3': 1216 case '4': 1217 case '5': 1218 case '6': 1219 case '7': 1220 case '8': 1221 case '9': 1222 fractionBuffer.append(c); 1223 break; 1224 1225 case 'Z': 1226 // This is only acceptable if we're at the end of the value. 1227 if (i != value.length() - 1) 1228 { 1229 LocalizableMessage message = 1230 WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR. 1231 get(value, c); 1232 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1233 message); 1234 } 1235 1236 timeZone = TIME_ZONE_UTC_OBJ; 1237 break outerLoop; 1238 1239 case '+': 1240 case '-': 1241 timeZone = getTimeZoneForOffset(value, i); 1242 break outerLoop; 1243 1244 default: 1245 LocalizableMessage message = 1246 WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR. 1247 get(value, c); 1248 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1249 message); 1250 } 1251 } 1252 1253 if (fractionBuffer.length() == 2) 1254 { 1255 LocalizableMessage message = 1256 WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION.get(value); 1257 throw new DirectoryException( 1258 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 1259 } 1260 1261 if (timeZone == null) 1262 { 1263 LocalizableMessage message = 1264 WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO.get(value); 1265 throw new DirectoryException( 1266 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 1267 } 1268 1269 Double fractionValue = Double.parseDouble(fractionBuffer.toString()); 1270 long additionalMilliseconds = Math.round(fractionValue * multiplier); 1271 1272 try 1273 { 1274 GregorianCalendar calendar = new GregorianCalendar(); 1275 calendar.setLenient(false); 1276 calendar.setTimeZone(timeZone); 1277 calendar.set(year, month, day, hour, minute, second); 1278 calendar.set(Calendar.MILLISECOND, 0); 1279 return calendar.getTimeInMillis() + additionalMilliseconds; 1280 } 1281 catch (Exception e) 1282 { 1283 logger.traceException(e); 1284 1285 // This should only happen if the provided date wasn't legal 1286 // (e.g., September 31). 1287 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, e); 1288 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1289 message, e); 1290 } 1291 } 1292 1293 /** 1294 * Decodes a time zone offset from the provided value. 1295 * 1296 * @param value The whole value, including the offset. 1297 * @param startPos The position of the first character that is 1298 * contained in the offset. This should be the 1299 * position of the plus or minus character. 1300 * 1301 * @return The {@code TimeZone} object representing the decoded time zone. 1302 * 1303 * @throws DirectoryException If the provided value does not contain a valid 1304 * offset. 1305 */ 1306 private static TimeZone getTimeZoneForOffset(String value, int startPos) 1307 throws DirectoryException 1308 { 1309 String offSetStr = value.substring(startPos); 1310 int len = offSetStr.length(); 1311 if (len != 3 && len != 5) 1312 { 1313 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get( 1314 value, offSetStr); 1315 throw new DirectoryException( 1316 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 1317 } 1318 1319 1320 // The first character must be either a plus or minus. 1321 switch (offSetStr.charAt(0)) 1322 { 1323 case '+': 1324 case '-': 1325 // These are OK. 1326 break; 1327 1328 default: 1329 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get( 1330 value, offSetStr); 1331 throw new DirectoryException( 1332 ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1333 message); 1334 } 1335 1336 1337 // The first two characters must be an integer between 00 and 23. 1338 switch (offSetStr.charAt(1)) 1339 { 1340 case '0': 1341 case '1': 1342 switch (offSetStr.charAt(2)) 1343 { 1344 case '0': 1345 case '1': 1346 case '2': 1347 case '3': 1348 case '4': 1349 case '5': 1350 case '6': 1351 case '7': 1352 case '8': 1353 case '9': 1354 // These are all fine. 1355 break; 1356 1357 default: 1358 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET. 1359 get(value, offSetStr); 1360 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1361 message); 1362 } 1363 break; 1364 1365 case '2': 1366 switch (offSetStr.charAt(2)) 1367 { 1368 case '0': 1369 case '1': 1370 case '2': 1371 case '3': 1372 // These are all fine. 1373 break; 1374 1375 default: 1376 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET. 1377 get(value, offSetStr); 1378 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1379 message); 1380 } 1381 break; 1382 1383 default: 1384 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get( 1385 value, offSetStr); 1386 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1387 message); 1388 } 1389 1390 1391 // If there are two more characters, then they must be an integer between 1392 // 00 and 59. 1393 if (len == 5) 1394 { 1395 switch (offSetStr.charAt(3)) 1396 { 1397 case '0': 1398 case '1': 1399 case '2': 1400 case '3': 1401 case '4': 1402 case '5': 1403 switch (offSetStr.charAt(4)) 1404 { 1405 case '0': 1406 case '1': 1407 case '2': 1408 case '3': 1409 case '4': 1410 case '5': 1411 case '6': 1412 case '7': 1413 case '8': 1414 case '9': 1415 // These are all fine. 1416 break; 1417 1418 default: 1419 LocalizableMessage message = 1420 WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET. 1421 get(value, offSetStr); 1422 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1423 message); 1424 } 1425 break; 1426 1427 default: 1428 LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET. 1429 get(value, offSetStr); 1430 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1431 message); 1432 } 1433 } 1434 1435 1436 // If we've gotten here, then it looks like a valid offset. We can create a 1437 // time zone by using "GMT" followed by the offset. 1438 return TimeZone.getTimeZone("GMT" + offSetStr); 1439 } 1440} 1441