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