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