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