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