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