View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package javax.mail.internet;
21  
22  import java.text.FieldPosition;
23  import java.text.NumberFormat;
24  import java.text.ParseException;
25  import java.text.ParsePosition;
26  import java.text.SimpleDateFormat;
27  import java.util.Calendar;
28  import java.util.Date;
29  import java.util.GregorianCalendar;
30  import java.util.Locale;
31  import java.util.TimeZone;
32  
33  /**
34   * Formats ths date as specified by
35   * draft-ietf-drums-msg-fmt-08 dated January 26, 2000
36   * which supercedes RFC822.
37   * <p/>
38   * <p/>
39   * The format used is <code>EEE, d MMM yyyy HH:mm:ss Z</code> and
40   * locale is always US-ASCII.
41   *
42   * @version $Rev: 628009 $ $Date: 2008-02-15 05:53:02 -0500 (Fri, 15 Feb 2008) $
43   */
44  public class MailDateFormat extends SimpleDateFormat {
45      public MailDateFormat() {
46          super("EEE, d MMM yyyy HH:mm:ss Z (z)", Locale.US);
47      }
48  
49      public StringBuffer format(Date date, StringBuffer buffer, FieldPosition position) {
50          return super.format(date, buffer, position);
51      }
52  
53      /**
54       * Parse a Mail date into a Date object.  This uses fairly 
55       * lenient rules for the format because the Mail standards 
56       * for dates accept multiple formats.
57       * 
58       * @param string   The input string.
59       * @param position The position argument.
60       * 
61       * @return The Date object with the information inside. 
62       */
63      public Date parse(String string, ParsePosition position) {
64          MailDateParser parser = new MailDateParser(string, position);
65          try {
66              return parser.parse(isLenient()); 
67          } catch (ParseException e) {
68              e.printStackTrace(); 
69              // just return a null for any parsing errors 
70              return null; 
71          }
72      }
73  
74      /**
75       * The calendar cannot be set
76       * @param calendar
77       * @throws UnsupportedOperationException
78       */
79      public void setCalendar(Calendar calendar) {
80          throw new UnsupportedOperationException();
81      }
82  
83      /**
84       * The format cannot be set
85       * @param format
86       * @throws UnsupportedOperationException
87       */
88      public void setNumberFormat(NumberFormat format) {
89          throw new UnsupportedOperationException();
90      }
91      
92      
93      // utility class for handling date parsing issues 
94      class MailDateParser {
95          // our list of defined whitespace characters 
96          static final String whitespace = " \t\r\n"; 
97          
98          // current parsing position 
99          int current; 
100         // our end parsing position 
101         int endOffset; 
102         // the date source string 
103         String source; 
104         // The parsing position. We update this as we move along and 
105         // also for any parsing errors 
106         ParsePosition pos; 
107         
108         public MailDateParser(String source, ParsePosition pos) 
109         {
110             this.source = source; 
111             this.pos = pos; 
112             // we start using the providing parsing index. 
113             this.current = pos.getIndex(); 
114             this.endOffset = source.length(); 
115         }
116         
117         /**
118          * Parse the timestamp, returning a date object. 
119          * 
120          * @param lenient The lenient setting from the Formatter object.
121          * 
122          * @return A Date object based off of parsing the date string.
123          * @exception ParseException
124          */
125         public Date parse(boolean lenient) throws ParseException {
126             // we just skip over any next date format, which means scanning ahead until we
127             // find the first numeric character 
128             locateNumeric(); 
129             // the day can be either 1 or two digits 
130             int day = parseNumber(1, 2); 
131             // step over the delimiter 
132             skipDateDelimiter(); 
133             // parse off the month (which is in character format) 
134             int month = parseMonth(); 
135             // step over the delimiter 
136             skipDateDelimiter(); 
137             // now pull of the year, which can be either 2-digit or 4-digit 
138             int year = parseYear(); 
139             // white space is required here 
140             skipRequiredWhiteSpace(); 
141             // accept a 1 or 2 digit hour 
142             int hour = parseNumber(1, 2);
143             skipRequiredChar(':'); 
144             // the minutes must be two digit 
145             int minutes = parseNumber(2, 2);
146             
147             // the seconds are optional, but the ":" tells us if they are to 
148             // be expected. 
149             int seconds = 0; 
150             if (skipOptionalChar(':')) {
151                 seconds = parseNumber(2, 2); 
152             }
153             // skip over the white space 
154             skipWhiteSpace(); 
155             // and finally the timezone information 
156             int offset = parseTimeZone(); 
157             
158             // set the index of how far we've parsed this 
159             pos.setIndex(current);
160             
161             // create a calendar for creating the date 
162             Calendar greg = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 
163             // we inherit the leniency rules 
164             greg.setLenient(lenient);
165             greg.set(year, month, day, hour, minutes, seconds); 
166             // now adjust by the offset.  This seems a little strange, but we  
167             // need to negate the offset because this is a UTC calendar, so we need to 
168             // apply the reverse adjustment.  for example, for the EST timezone, the offset 
169             // value will be -300 (5 hours).  If the time was 15:00:00, the UTC adjusted time 
170             // needs to be 20:00:00, so we subract -300 minutes. 
171             greg.add(Calendar.MINUTE, -offset); 
172             // now return this timestamp. 
173             return greg.getTime(); 
174         }
175         
176         
177         /**
178          * Skip over a position where there's a required value 
179          * expected. 
180          * 
181          * @param ch     The required character.
182          * 
183          * @exception ParseException
184          */
185         private void skipRequiredChar(char ch) throws ParseException {
186             if (current >= endOffset) {
187                 parseError("Delimiter '" + ch + "' expected"); 
188             }
189             if (source.charAt(current) != ch) {
190                 parseError("Delimiter '" + ch + "' expected"); 
191             }
192             current++; 
193         }
194         
195         
196         /**
197          * Skip over a position where iff the position matches the
198          * character
199          * 
200          * @param ch     The required character.
201          * 
202          * @return true if the character was there, false otherwise.
203          * @exception ParseException
204          */
205         private boolean skipOptionalChar(char ch) {
206             if (current >= endOffset) {
207                 return false; 
208             }
209             if (source.charAt(current) != ch) {
210                 return false; 
211             }
212             current++; 
213             return true; 
214         }
215         
216         
217         /**
218          * Skip over any white space characters until we find 
219          * the next real bit of information.  Will scan completely to the 
220          * end, if necessary. 
221          */
222         private void skipWhiteSpace() {
223             while (current < endOffset) {
224                 // if this is not in the white space list, then success. 
225                 if (whitespace.indexOf(source.charAt(current)) < 0) {
226                     return; 
227                 }
228                 current++; 
229             }
230             
231             // everything used up, just return 
232         }
233         
234         
235         /**
236          * Skip over any non-white space characters until we find 
237          * either a whitespace char or the end of the data.
238          */
239         private void skipNonWhiteSpace() {
240             while (current < endOffset) {
241                 // if this is not in the white space list, then success. 
242                 if (whitespace.indexOf(source.charAt(current)) >= 0) {
243                     return; 
244                 }
245                 current++; 
246             }
247             
248             // everything used up, just return 
249         }
250         
251         
252         /**
253          * Skip over any white space characters until we find 
254          * the next real bit of information.  Will scan completely to the 
255          * end, if necessary. 
256          */
257         private void skipRequiredWhiteSpace() throws ParseException {
258             int start = current; 
259             
260             while (current < endOffset) {
261                 // if this is not in the white space list, then success. 
262                 if (whitespace.indexOf(source.charAt(current)) < 0) {
263                     // we must have at least one white space character 
264                     if (start == current) {
265                         parseError("White space character expected"); 
266                     }
267                     return; 
268                 }
269                 current++; 
270             }
271             // everything used up, just return, but make sure we had at least one  
272             // white space
273             if (start == current) {
274                 parseError("White space character expected"); 
275             }
276         }
277         
278         private void parseError(String message) throws ParseException {
279             // we've got an error, set the index to the end. 
280             pos.setErrorIndex(current);
281             throw new ParseException(message, current); 
282         }
283         
284         
285         /**
286          * Locate an expected numeric field. 
287          * 
288          * @exception ParseException
289          */
290         private void locateNumeric() throws ParseException {
291             while (current < endOffset) {
292                 // found a digit?  we're done
293                 if (Character.isDigit(source.charAt(current))) {
294                     return; 
295                 }
296                 current++; 
297             }
298             // we've got an error, set the index to the end. 
299             parseError("Number field expected"); 
300         }
301         
302         
303         /**
304          * Parse out an expected numeric field. 
305          * 
306          * @param minDigits The minimum number of digits we expect in this filed.
307          * @param maxDigits The maximum number of digits expected.  Parsing will
308          *                  stop at the first non-digit character.  An exception will
309          *                  be thrown if the field contained more than maxDigits
310          *                  in it.
311          * 
312          * @return The parsed numeric value. 
313          * @exception ParseException
314          */
315         private int parseNumber(int minDigits, int maxDigits) throws ParseException {
316             int start = current; 
317             int accumulator = 0; 
318             while (current < endOffset) {
319                 char ch = source.charAt(current); 
320                 // if this is not a digit character, then quit
321                 if (!Character.isDigit(ch)) {
322                     break; 
323                 }
324                 // add the digit value into the accumulator 
325                 accumulator = accumulator * 10 + Character.digit(ch, 10); 
326                 current++; 
327             }
328             
329             int fieldLength = current - start; 
330             if (fieldLength < minDigits || fieldLength > maxDigits) {
331                 parseError("Invalid number field"); 
332             }
333             
334             return accumulator; 
335         }
336         
337         /**
338          * Skip a delimiter between the date portions of the
339          * string.  The IMAP internal date format uses "-", so 
340          * we either accept a single "-" or any number of white
341          * space characters (at least one required). 
342          * 
343          * @exception ParseException
344          */
345         private void skipDateDelimiter() throws ParseException {
346             if (current >= endOffset) {
347                 parseError("Invalid date field delimiter"); 
348             }
349             
350             if (source.charAt(current) == '-') {
351                 current++; 
352             }
353             else {
354                 // must be at least a single whitespace character 
355                 skipRequiredWhiteSpace(); 
356             }
357         }
358         
359         
360         /**
361          * Parse a character month name into the date month 
362          * offset.
363          * 
364          * @return 
365          * @exception ParseException
366          */
367         private int parseMonth() throws ParseException {
368             if ((endOffset - current) < 3) {
369                 parseError("Invalid month"); 
370             }
371             
372             int monthOffset = 0; 
373             String month = source.substring(current, current + 3).toLowerCase();
374             
375             if (month.equals("jan")) {
376                 monthOffset = 0; 
377             }
378             else if (month.equals("feb")) {
379                 monthOffset = 1; 
380             }
381             else if (month.equals("mar")) {
382                 monthOffset = 2; 
383             }
384             else if (month.equals("apr")) {
385                 monthOffset = 3; 
386             }
387             else if (month.equals("may")) {
388                 monthOffset = 4; 
389             }
390             else if (month.equals("jun")) {
391                 monthOffset = 5; 
392             }
393             else if (month.equals("jul")) {
394                 monthOffset = 6; 
395             }
396             else if (month.equals("aug")) {
397                 monthOffset = 7; 
398             }
399             else if (month.equals("sep")) {
400                 monthOffset = 8; 
401             }
402             else if (month.equals("oct")) {
403                 monthOffset = 9; 
404             }
405             else if (month.equals("nov")) {
406                 monthOffset = 10; 
407             }
408             else if (month.equals("dec")) {
409                 monthOffset = 11; 
410             }
411             else {
412                 parseError("Invalid month"); 
413             }
414             
415             // ok, this is valid.  Update the position and return it 
416             current += 3;
417             return monthOffset; 
418         }
419         
420         /**
421          * Parse off a year field that might be expressed as 
422          * either 2 or 4 digits. 
423          * 
424          * @return The numeric value of the year. 
425          * @exception ParseException
426          */
427         private int parseYear() throws ParseException {
428             // the year is between 2 to 4 digits 
429             int year = parseNumber(2, 4); 
430             
431             // the two digit years get some sort of adjustment attempted. 
432             if (year < 50) {
433                 year += 2000; 
434             }
435             else if (year < 100) {
436                 year += 1990; 
437             }
438             return year; 
439         }
440         
441         
442         /**
443          * Parse all of the different timezone options. 
444          * 
445          * @return The timezone offset.
446          * @exception ParseException
447          */
448         private int parseTimeZone() throws ParseException {
449             if (current >= endOffset) {
450                 parseError("Missing time zone"); 
451             }
452             
453             // get the first non-blank. If this is a sign character, this 
454             // is a zone offset.  
455             char sign = source.charAt(current); 
456             
457             if (sign == '-' || sign == '+') {
458                 // need to step over the sign character 
459                 current++; 
460                 // a numeric timezone is always a 4 digit number, but 
461                 // expressed as minutes/seconds.  I'm too lazy to write a 
462                 // different parser that will bound on just a couple of characters, so 
463                 // we'll grab this as a single value and adjust     
464                 int zoneInfo = parseNumber(4, 4);
465                 
466                 int offset = (zoneInfo / 100) * 60 + (zoneInfo % 100); 
467                 // negate this, if we have a negativeo offset 
468                 if (sign == '-') {
469                     offset = -offset; 
470                 }
471                 return offset; 
472             }
473             else {
474                 // need to parse this out using the obsolete zone names.  This will be 
475                 // either a 3-character code (defined set), or a single character military 
476                 // zone designation. 
477                 int start = current; 
478                 skipNonWhiteSpace(); 
479                 String name = source.substring(start, current).toUpperCase(); 
480                 
481                 if (name.length() == 1) {
482                     return militaryZoneOffset(name); 
483                 }
484                 else if (name.length() <= 3) {
485                     return namedZoneOffset(name); 
486                 }
487                 else {
488                     parseError("Invalid time zone"); 
489                 }
490                 return 0; 
491             }
492         }
493         
494         
495         /**
496          * Parse the obsolete mail timezone specifiers. The
497          * allowed set of timezones are terribly US centric. 
498          * That's the spec.  The preferred timezone form is 
499          * the +/-mmss form. 
500          * 
501          * @param name   The input name.
502          * 
503          * @return The standard timezone offset for the specifier.
504          * @exception ParseException
505          */
506         private int namedZoneOffset(String name) throws ParseException {
507             
508             // NOTE:  This is "UT", NOT "UTC"
509             if (name.equals("UT")) {
510                 return 0; 
511             }
512             else if (name.equals("GMT")) {
513                 return 0; 
514             }
515             else if (name.equals("EST")) {
516                 return -300; 
517             }
518             else if (name.equals("EDT")) {
519                 return -240; 
520             }
521             else if (name.equals("CST")) {
522                 return -360; 
523             }
524             else if (name.equals("CDT")) {
525                 return -300; 
526             }
527             else if (name.equals("MST")) {
528                 return -420; 
529             }
530             else if (name.equals("MDT")) {
531                 return -360; 
532             }
533             else if (name.equals("PST")) {
534                 return -480; 
535             }
536             else if (name.equals("PDT")) {
537                 return -420; 
538             }
539             else {
540                 parseError("Invalid time zone"); 
541                 return 0; 
542             }
543         }
544         
545         
546         /**
547          * Parse a single-character military timezone. 
548          * 
549          * @param name   The one-character name.
550          * 
551          * @return The offset corresponding to the military designation.
552          */
553         private int militaryZoneOffset(String name) throws ParseException {
554             switch (Character.toUpperCase(name.charAt(0))) {
555                 case 'A':
556                     return 60; 
557                 case 'B':
558                     return 120; 
559                 case 'C':
560                     return 180;
561                 case 'D':
562                     return 240;
563                 case 'E':
564                     return 300;
565                 case 'F':
566                     return 360;
567                 case 'G':
568                     return 420;
569                 case 'H':
570                     return 480;
571                 case 'I':
572                     return 540;
573                 case 'K':
574                     return 600;
575                 case 'L':
576                     return 660;
577                 case 'M':
578                     return 720;
579                 case 'N':
580                     return -60;
581                 case 'O':
582                     return -120;
583                 case 'P':
584                     return -180;
585                 case 'Q':
586                     return -240;
587                 case 'R':
588                     return -300;
589                 case 'S':
590                     return -360;
591                 case 'T':
592                     return -420;
593                 case 'U':
594                     return -480;
595                 case 'V':
596                     return -540;
597                 case 'W':
598                     return -600;
599                 case 'X':
600                     return -660;
601                 case 'Y':
602                     return -720;
603                 case 'Z':
604                     return 0;    
605                 default:
606                     parseError("Invalid time zone");
607                     return 0; 
608             }
609         }
610     }
611 }