1 /*
2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3  *
4  * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
5  *
6  * The contents of this file are subject to the terms of either the GNU
7  * General Public License Version 2 only ("GPL") or the Common Development
8  * and Distribution License("CDDL") (collectively, the "License").  You
9  * may not use this file except in compliance with the License.  You can
10  * obtain a copy of the License at
11  * https://oss.oracle.com/licenses/CDDL+GPL-1.1
12  * or LICENSE.txt.  See the License for the specific
13  * language governing permissions and limitations under the License.
14  *
15  * When distributing the software, include this License Header Notice in each
16  * file and include the License file at LICENSE.txt.
17  *
18  * GPL Classpath Exception:
19  * Oracle designates this particular file as subject to the "Classpath"
20  * exception as provided by Oracle in the GPL Version 2 section of the License
21  * file that accompanied this code.
22  *
23  * Modifications:
24  * If applicable, add the following below the License Header, with the fields
25  * enclosed by brackets [] replaced by your own identifying information:
26  * "Portions Copyright [year] [name of copyright owner]"
27  *
28  * Contributor(s):
29  * If you wish your version of this file to be governed by only the CDDL or
30  * only the GPL Version 2, indicate your decision by adding "[Contributor]
31  * elects to include this software in this distribution under the [CDDL or GPL
32  * Version 2] license."  If you don't indicate a single choice of license, a
33  * recipient has the option to distribute your version of this file under
34  * either the CDDL, the GPL Version 2 or to extend the choice of license to
35  * its licensees as provided above.  However, if you add GPL Version 2 code
36  * and therefore, elected the GPL Version 2 license, then the option applies
37  * only if the new code is made subject to such option by the copyright
38  * holder.
39  */

40
41 package javax.mail.internet;
42
43 import java.io.IOException;
44 import java.io.ObjectInputStream;
45 import java.io.ObjectStreamException;
46 import java.util.Date;
47 import java.util.Calendar;
48 import java.util.Locale;
49 import java.util.TimeZone;
50 import java.util.logging.Level;
51 import java.text.DateFormatSymbols;
52 import java.text.SimpleDateFormat;
53 import java.text.NumberFormat;
54 import java.text.FieldPosition;
55 import java.text.ParsePosition;
56 import java.text.ParseException;
57
58 import com.sun.mail.util.MailLogger;
59
60 /**
61  * Formats and parses date specification based on
62  * <a href="http://www.ietf.org/rfc/rfc2822.txt" target="_top">RFC 2822</a>. <p>
63  *
64  * This class does not support methods that influence the format. It always
65  * formats the date based on the specification below.<p>
66  *
67  * 3.3. Date and Time Specification
68  * <p>
69  * Date and time occur in several header fields.  This section specifies
70  * the syntax for a full date and time specification.  Though folding
71  * white space is permitted throughout the date-time specification, it is
72  * RECOMMENDED that a single space be used in each place that FWS appears
73  * (whether it is required or optional); some older implementations may
74  * not interpret other occurrences of folding white space correctly.
75  * <pre>
76  * date-time       =       [ day-of-week "," ] date FWS time [CFWS]
77  *
78  * day-of-week     =       ([FWS] day-name) / obs-day-of-week
79  *
80  * day-name        =       "Mon" / "Tue" / "Wed" / "Thu" /
81  *                         "Fri" / "Sat" / "Sun"
82  *
83  * date            =       day month year
84  *
85  * year            =       4*DIGIT / obs-year
86  *
87  * month           =       (FWS month-name FWS) / obs-month
88  *
89  * month-name      =       "Jan" / "Feb" / "Mar" / "Apr" /
90  *                         "May" / "Jun" / "Jul" / "Aug" /
91  *                         "Sep" / "Oct" / "Nov" / "Dec"
92  *
93  * day             =       ([FWS] 1*2DIGIT) / obs-day
94  *
95  * time            =       time-of-day FWS zone
96  *
97  * time-of-day     =       hour ":" minute [ ":" second ]
98  *
99  * hour            =       2DIGIT / obs-hour
100  *
101  * minute          =       2DIGIT / obs-minute
102  *
103  * second          =       2DIGIT / obs-second
104  *
105  * zone            =       (( "+" / "-" ) 4DIGIT) / obs-zone
106  * </pre>
107  * The day is the numeric day of the month.  The year is any numeric year
108  * 1900 or later.
109  * <p>
110  * The time-of-day specifies the number of hours, minutes, and optionally
111  * seconds since midnight of the date indicated.
112  * <p>
113  * The date and time-of-day SHOULD express local time.
114  * <p>
115  * The zone specifies the offset from Coordinated Universal Time (UTC,
116  * formerly referred to as "Greenwich Mean Time") that the date and
117  * time-of-day represent.  The "+" or "-" indicates whether the
118  * time-of-day is ahead of (i.e., east of) or behind (i.e., west of)
119  * Universal Time.  The first two digits indicate the number of hours
120  * difference from Universal Time, and the last two digits indicate the
121  * number of minutes difference from Universal Time.  (Hence, +hhmm means
122  * +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes).  The
123  * form "+0000" SHOULD be used to indicate a time zone at Universal Time.
124  * Though "-0000" also indicates Universal Time, it is used to indicate
125  * that the time was generated on a system that may be in a local time
126  * zone other than Universal Time and therefore indicates that the
127  * date-time contains no information about the local time zone.
128  * <p>
129  * A date-time specification MUST be semantically valid.  That is, the
130  * day-of-the-week (if included) MUST be the day implied by the date, the
131  * numeric day-of-month MUST be between 1 and the number of days allowed
132  * for the specified month (in the specified year), the time-of-day MUST
133  * be in the range 00:00:00 through 23:59:60 (the number of seconds
134  * allowing for a leap second; see [STD12]), and the zone MUST be within
135  * the range -9959 through +9959.
136  *
137  * <h3><a name="synchronization">Synchronization</a></h3>
138  * 
139  * <p>
140  * Date formats are not synchronized.
141  * It is recommended to create separate format instances for each thread.
142  * If multiple threads access a format concurrently, it must be synchronized
143  * externally.
144  *
145  * @author    Anthony Vanelverdinghe
146  * @author    Max Spivak
147  * @since    JavaMail 1.2
148  */

149 public class MailDateFormat extends SimpleDateFormat {
150
151     private static final long serialVersionUID = -8148227605210628779L;
152     private static final String PATTERN = "EEE, d MMM yyyy HH:mm:ss Z (z)";
153
154     private static final MailLogger LOGGER = new MailLogger(
155             MailDateFormat.class"DEBUG"false, System.out);
156
157     private static final int UNKNOWN_DAY_NAME = -1;
158     private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
159     private static final int LEAP_SECOND = 60;
160
161     /**
162      * Create a new date format for the RFC2822 specification with lenient
163      * parsing.
164      */

165     public MailDateFormat() {
166         super(PATTERN, Locale.US);
167     }
168
169     /**
170      * Allows to serialize instances such that they are deserializable with the
171      * previous implementation.
172      *
173      * @return the object to be serialized
174      * @throws ObjectStreamException    never
175      */

176     private Object writeReplace() throws ObjectStreamException {
177         MailDateFormat fmt = new MailDateFormat();
178         fmt.superApplyPattern("EEE, d MMM yyyy HH:mm:ss 'XXXXX' (z)");
179         fmt.setTimeZone(getTimeZone());
180         return fmt;
181     }
182
183     /**
184      * Allows to deserialize instances that were serialized with the previous
185      * implementation.
186      *
187      * @param in the stream containing the serialized object
188      * @throws IOException    on read failures
189      * @throws ClassNotFoundException    never
190      */

191     private void readObject(ObjectInputStream in)
192             throws IOException, ClassNotFoundException {
193         in.defaultReadObject();
194         super.applyPattern(PATTERN);
195     }
196
197     /**
198      * Overrides Cloneable.
199      *
200      * @return a clone of this instance
201      * @since JavaMail 1.6
202      */

203     @Override
204     public MailDateFormat clone() {
205         return (MailDateFormat) super.clone();
206     }
207
208     /**
209      * Formats the given date in the format specified by 
210      * RFC 2822 in the current TimeZone.
211      *
212      * @param   date            the Date object
213      * @param   dateStrBuf      the formatted string
214      * @param   fieldPosition   the current field position
215      * @return    StringBuffer    the formatted String
216      * @since            JavaMail 1.2
217      */

218     @Override
219     public StringBuffer format(Date date, StringBuffer dateStrBuf,
220             FieldPosition fieldPosition) {
221         return super.format(date, dateStrBuf, fieldPosition);
222     }
223
224     /**
225      * Parses the given date in the format specified by
226      * RFC 2822.
227      * <ul>
228      * <li>With strict parsing, obs-* tokens are unsupported. Lenient parsing
229      * supports obs-year and obs-zone, with the exception of the 1-character
230      * military time zones.
231      * <li>The optional CFWS token at the end is not parsed.
232      * <li>RFC 2822 specifies that a zone of "-0000" indicates that the
233      * date-time contains no information about the local time zone. This class
234      * uses the UTC time zone in this case.
235      * </ul>
236      *
237      * @param   text    the formatted date to be parsed
238      * @param   pos     the current parse position
239      * @return    Date    the parsed date. In case of error, returns null.
240      * @since        JavaMail 1.2
241      */

242     @Override
243     public Date parse(String text, ParsePosition pos) {
244         if (text == null || pos == null) {
245             throw new NullPointerException();
246         } else if (0 > pos.getIndex() || pos.getIndex() >= text.length()) {
247             return null;
248         }
249
250         return isLenient()
251                 ? new Rfc2822LenientParser(text, pos).parse()
252                 : new Rfc2822StrictParser(text, pos).parse();
253     }
254
255     /**
256      * This method always throws an UnsupportedOperationException and should not
257      * be used because RFC 2822 mandates a specific calendar.
258      *
259      * @throws UnsupportedOperationException if this method is invoked
260      */

261     @Override
262     public void setCalendar(Calendar newCalendar) {
263         throw new UnsupportedOperationException("Method "
264                 + "setCalendar() shouldn't be called");
265     }
266
267     /**
268      * This method always throws an UnsupportedOperationException and should not
269      * be used because RFC 2822 mandates a specific number format.
270      *
271      * @throws UnsupportedOperationException if this method is invoked
272      */

273     @Override
274     public void setNumberFormat(NumberFormat newNumberFormat) {
275         throw new UnsupportedOperationException("Method "
276                 + "setNumberFormat() shouldn't be called");
277     }
278
279     /**
280      * This method always throws an UnsupportedOperationException and should not
281      * be used because RFC 2822 mandates a specific pattern.
282      *
283      * @throws UnsupportedOperationException if this method is invoked
284      * @since JavaMail 1.6
285      */

286     @Override
287     public void applyLocalizedPattern(String pattern) {
288         throw new UnsupportedOperationException("Method "
289                 + "applyLocalizedPattern() shouldn't be called");
290     }
291
292     /**
293      * This method always throws an UnsupportedOperationException and should not
294      * be used because RFC 2822 mandates a specific pattern.
295      *
296      * @throws UnsupportedOperationException if this method is invoked
297      * @since JavaMail 1.6
298      */

299     @Override
300     public void applyPattern(String pattern) {
301         throw new UnsupportedOperationException("Method "
302                 + "applyPattern() shouldn't be called");
303     }
304
305     /**
306      * This method allows serialization to change the pattern.
307      */

308     private void superApplyPattern(String pattern) {
309         super.applyPattern(pattern);
310     }
311
312     /**
313      * This method always throws an UnsupportedOperationException and should not
314      * be used because RFC 2822 mandates another strategy for interpreting
315      * 2-digits years.
316      *
317      * @return the start of the 100-year period into which two digit years are
318      * parsed
319      * @throws UnsupportedOperationException if this method is invoked
320      * @since JavaMail 1.6
321      */

322     @Override
323     public Date get2DigitYearStart() {
324         throw new UnsupportedOperationException("Method "
325                 + "get2DigitYearStart() shouldn't be called");
326     }
327
328     /**
329      * This method always throws an UnsupportedOperationException and should not
330      * be used because RFC 2822 mandates another strategy for interpreting
331      * 2-digits years.
332      *
333      * @throws UnsupportedOperationException if this method is invoked
334      * @since JavaMail 1.6
335      */

336     @Override
337     public void set2DigitYearStart(Date startDate) {
338         throw new UnsupportedOperationException("Method "
339                 + "set2DigitYearStart() shouldn't be called");
340     }
341
342     /**
343      * This method always throws an UnsupportedOperationException and should not
344      * be used because RFC 2822 mandates specific date format symbols.
345      *
346      * @throws UnsupportedOperationException if this method is invoked
347      * @since JavaMail 1.6
348      */

349     @Override
350     public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
351         throw new UnsupportedOperationException("Method "
352                 + "setDateFormatSymbols() shouldn't be called");
353     }
354
355     /**
356      * Returns the date, as specified by the parameters.
357      *
358      * @param dayName
359      * @param day
360      * @param month
361      * @param year
362      * @param hour
363      * @param minute
364      * @param second
365      * @param zone
366      * @return the date, as specified by the parameters
367      * @throws IllegalArgumentException if this instance's Calendar is
368      * non-lenient and any of the parameters have invalid values, or if dayName
369      * is not consistent with day-month-year
370      */

371     private Date toDate(int dayName, int day, int month, int year,
372             int hour, int minute, int second, int zone) {
373         if (second == LEAP_SECOND) {
374             second = 59;
375         }
376
377         TimeZone tz = calendar.getTimeZone();
378         try {
379             calendar.setTimeZone(UTC);
380             calendar.clear();
381             calendar.set(year, month, day, hour, minute, second);
382
383             if (dayName == UNKNOWN_DAY_NAME
384                     || dayName == calendar.get(Calendar.DAY_OF_WEEK)) {
385                 calendar.add(Calendar.MINUTE, zone);
386                 return calendar.getTime();
387             } else {
388                 throw new IllegalArgumentException("Inconsistent day-name");
389             }
390         } finally {
391             calendar.setTimeZone(tz);
392         }
393     }
394
395     /**
396      * This class provides the building blocks for date parsing.
397      * <p>
398      * It has the following invariants:
399      * <ul>
400      * <li>no exceptions are thrown, except for java.text.ParseException from
401      * parse* methods
402      * <li>when parse* throws ParseException OR get* returns INVALID_CHAR OR
403      * skip* returns false OR peek* is invoked, then pos.getIndex() on method
404      * exit is the same as it was on method entry
405      * </ul>
406      */

407     private static abstract class AbstractDateParser {
408
409         static final int INVALID_CHAR = -1;
410         static final int MAX_YEAR_DIGITS = 8; // guarantees that:
411         // year < new GregorianCalendar().getMaximum(Calendar.YEAR)
412
413         final String text;
414         final ParsePosition pos;
415
416         AbstractDateParser(String text, ParsePosition pos) {
417             this.text = text;
418             this.pos = pos;
419         }
420
421         final Date parse() {
422             int startPosition = pos.getIndex();
423             try {
424                 return tryParse();
425             } catch (Exception e) { // == ParseException | RuntimeException e
426                 if (LOGGER.isLoggable(Level.FINE)) {
427                     LOGGER.log(Level.FINE, "Bad date: '" + text + "'", e);
428                 }
429                 pos.setErrorIndex(pos.getIndex());
430                 pos.setIndex(startPosition);
431                 return null;
432             }
433         }
434
435         abstract Date tryParse() throws ParseException;
436
437         /**
438          * @return the java.util.Calendar constant for the parsed day name
439          */

440         final int parseDayName() throws ParseException {
441             switch (getChar()) {
442                 case 'S':
443                     if (skipPair('u', 'n')) {
444                         return Calendar.SUNDAY;
445                     } else if (skipPair('a', 't')) {
446                         return Calendar.SATURDAY;
447                     }
448                     break;
449                 case 'T':
450                     if (skipPair('u', 'e')) {
451                         return Calendar.TUESDAY;
452                     } else if (skipPair('h', 'u')) {
453                         return Calendar.THURSDAY;
454                     }
455                     break;
456                 case 'M':
457                     if (skipPair('o', 'n')) {
458                         return Calendar.MONDAY;
459                     }
460                     break;
461                 case 'W':
462                     if (skipPair('e', 'd')) {
463                         return Calendar.WEDNESDAY;
464                     }
465                     break;
466                 case 'F':
467                     if (skipPair('r', 'i')) {
468                         return Calendar.FRIDAY;
469                     }
470                     break;
471                 case INVALID_CHAR:
472                     throw new ParseException("Invalid day-name",
473                             pos.getIndex());
474             }
475             pos.setIndex(pos.getIndex() - 1);
476             throw new ParseException("Invalid day-name", pos.getIndex());
477         }
478
479         /**
480          * @return the java.util.Calendar constant for the parsed month name
481          */

482         @SuppressWarnings("fallthrough")
483         final int parseMonthName(boolean caseSensitive) throws ParseException {
484             switch (getChar()) {
485                 case 'j':
486                     if (caseSensitive) {
487                         break;
488                     }
489                 case 'J':
490                     if (skipChar('u') || (!caseSensitive && skipChar('U'))) {
491                         if (skipChar('l') || (!caseSensitive
492                                 && skipChar('L'))) {
493                             return Calendar.JULY;
494                         } else if (skipChar('n') || (!caseSensitive
495                                 && skipChar('N'))) {
496                             return Calendar.JUNE;
497                         } else {
498                             pos.setIndex(pos.getIndex() - 1);
499                         }
500                     } else if (skipPair('a', 'n') || (!caseSensitive
501                             && skipAlternativePair('a', 'A', 'n', 'N'))) {
502                         return Calendar.JANUARY;
503                     }
504                     break;
505                 case 'm':
506                     if (caseSensitive) {
507                         break;
508                     }
509                 case 'M':
510                     if (skipChar('a') || (!caseSensitive && skipChar('A'))) {
511                         if (skipChar('r') || (!caseSensitive
512                                 && skipChar('R'))) {
513                             return Calendar.MARCH;
514                         } else if (skipChar('y') || (!caseSensitive
515                                 && skipChar('Y'))) {
516                             return Calendar.MAY;
517                         } else {
518                             pos.setIndex(pos.getIndex() - 1);
519                         }
520                     }
521                     break;
522                 case 'a':
523                     if (caseSensitive) {
524                         break;
525                     }
526                 case 'A':
527                     if (skipPair('u', 'g') || (!caseSensitive
528                             && skipAlternativePair('u', 'U', 'g', 'G'))) {
529                         return Calendar.AUGUST;
530                     } else if (skipPair('p', 'r') || (!caseSensitive
531                             && skipAlternativePair('p', 'P', 'r', 'R'))) {
532                         return Calendar.APRIL;
533                     }
534                     break;
535                 case 'd':
536                     if (caseSensitive) {
537                         break;
538                     }
539                 case 'D':
540                     if (skipPair('e', 'c') || (!caseSensitive
541                             && skipAlternativePair('e', 'E', 'c', 'C'))) {
542                         return Calendar.DECEMBER;
543                     }
544                     break;
545                 case 'o':
546                     if (caseSensitive) {
547                         break;
548                     }
549                 case 'O':
550                     if (skipPair('c', 't') || (!caseSensitive
551                             && skipAlternativePair('c', 'C', 't', 'T'))) {
552                         return Calendar.OCTOBER;
553                     }
554                     break;
555                 case 's':
556                     if (caseSensitive) {
557                         break;
558                     }
559                 case 'S':
560                     if (skipPair('e', 'p') || (!caseSensitive
561                             && skipAlternativePair('e', 'E', 'p', 'P'))) {
562                         return Calendar.SEPTEMBER;
563                     }
564                     break;
565                 case 'n':
566                     if (caseSensitive) {
567                         break;
568                     }
569                 case 'N':
570                     if (skipPair('o', 'v') || (!caseSensitive
571                             && skipAlternativePair('o', 'O', 'v', 'V'))) {
572                         return Calendar.NOVEMBER;
573                     }
574                     break;
575                 case 'f':
576                     if (caseSensitive) {
577                         break;
578                     }
579                 case 'F':
580                     if (skipPair('e', 'b') || (!caseSensitive
581                             && skipAlternativePair('e', 'E', 'b', 'B'))) {
582                         return Calendar.FEBRUARY;
583                     }
584                     break;
585                 case INVALID_CHAR:
586                     throw new ParseException("Invalid month", pos.getIndex());
587             }
588             pos.setIndex(pos.getIndex() - 1);
589             throw new ParseException("Invalid month", pos.getIndex());
590         }
591
592         /**
593          * @return the number of minutes to be added to the time in the local
594          * time zone, in order to obtain the equivalent time in the UTC time
595          * zone. Returns 0 if the date-time contains no information about the
596          * local time zone.
597          */

598         final int parseZoneOffset() throws ParseException {
599             int sign = getChar();
600             if (sign == '+' || sign == '-') {
601                 int offset = parseAsciiDigits(4, 4, true);
602                 if (!isValidZoneOffset(offset)) {
603                     pos.setIndex(pos.getIndex() - 5);
604                     throw new ParseException("Invalid zone", pos.getIndex());
605                 }
606
607                 return ((sign == '+') ? -1 : 1)
608                         * (offset / 100 * 60 + offset % 100);
609             } else if (sign != INVALID_CHAR) {
610                 pos.setIndex(pos.getIndex() - 1);
611             }
612             throw new ParseException("Invalid zone", pos.getIndex());
613         }
614
615         boolean isValidZoneOffset(int offset) {
616             return (offset % 100) < 60;
617         }
618
619         final int parseAsciiDigits(int count) throws ParseException {
620             return parseAsciiDigits(count, count);
621         }
622
623         final int parseAsciiDigits(int min, int max) throws ParseException {
624             return parseAsciiDigits(min, max, false);
625         }
626
627         final int parseAsciiDigits(int min, int max, boolean isEOF)
628                 throws ParseException {
629             int result = 0;
630             int nbDigitsParsed = 0;
631             while (nbDigitsParsed < max && peekAsciiDigit()) {
632                 result = result * 10 + getAsciiDigit();
633                 nbDigitsParsed++;
634             }
635
636             if ((nbDigitsParsed < min)
637                     || (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) {
638                 pos.setIndex(pos.getIndex() - nbDigitsParsed);
639             } else {
640                 return result;
641             }
642
643             String range = (min == max)
644                     ? Integer.toString(min)
645                     : "between " + min + " and " + max;
646             throw new ParseException("Invalid input: expected "
647                     + range + " ASCII digits", pos.getIndex());
648         }
649
650         final void parseFoldingWhiteSpace() throws ParseException {
651             if (!skipFoldingWhiteSpace()) {
652                 throw new ParseException("Invalid input: expected FWS",
653                         pos.getIndex());
654             }
655         }
656
657         final void parseChar(char ch) throws ParseException {
658             if (!skipChar(ch)) {
659                 throw new ParseException("Invalid input: expected '" + ch + "'",
660                         pos.getIndex());
661             }
662         }
663
664         final int getAsciiDigit() {
665             int ch = getChar();
666             if ('0' <= ch && ch <= '9') {
667                 return Character.digit((char) ch, 10);
668             } else {
669                 if (ch != INVALID_CHAR) {
670                     pos.setIndex(pos.getIndex() - 1);
671                 }
672                 return INVALID_CHAR;
673             }
674         }
675
676         final int getChar() {
677             if (pos.getIndex() < text.length()) {
678                 char ch = text.charAt(pos.getIndex());
679                 pos.setIndex(pos.getIndex() + 1);
680                 return ch;
681             } else {
682                 return INVALID_CHAR;
683             }
684         }
685
686         boolean skipFoldingWhiteSpace() {
687             // fast paths: a single ASCII space or no FWS
688             if (skipChar(' ')) {
689                 if (!peekFoldingWhiteSpace()) {
690                     return true;
691                 } else {
692                     pos.setIndex(pos.getIndex() - 1);
693                 }
694             } else if (!peekFoldingWhiteSpace()) {
695                 return false;
696             }
697
698             // normal path
699             int startIndex = pos.getIndex();
700             if (skipWhiteSpace()) {
701                 while (skipNewline()) {
702                     if (!skipWhiteSpace()) {
703                         pos.setIndex(startIndex);
704                         return false;
705                     }
706                 }
707                 return true;
708             } else if (skipNewline() && skipWhiteSpace()) {
709                 return true;
710             } else {
711                 pos.setIndex(startIndex);
712                 return false;
713             }
714         }
715
716         final boolean skipWhiteSpace() {
717             int startIndex = pos.getIndex();
718             while (skipAlternative(' ', '\t')) { /* empty */ }
719             return pos.getIndex() > startIndex;
720         }
721
722         final boolean skipNewline() {
723             return skipPair('\r', '\n');
724         }
725
726         final boolean skipAlternativeTriple(
727                 char firstStandard, char firstAlternative,
728                 char secondStandard, char secondAlternative,
729                 char thirdStandard, char thirdAlternative
730         ) {
731             if (skipAlternativePair(firstStandard, firstAlternative,
732                     secondStandard, secondAlternative)) {
733                 if (skipAlternative(thirdStandard, thirdAlternative)) {
734                     return true;
735                 } else {
736                     pos.setIndex(pos.getIndex() - 2);
737                 }
738             }
739             return false;
740         }
741
742         final boolean skipAlternativePair(
743                 char firstStandard, char firstAlternative,
744                 char secondStandard, char secondAlternative
745         ) {
746             if (skipAlternative(firstStandard, firstAlternative)) {
747                 if (skipAlternative(secondStandard, secondAlternative)) {
748                     return true;
749                 } else {
750                     pos.setIndex(pos.getIndex() - 1);
751                 }
752             }
753             return false;
754         }
755
756         final boolean skipAlternative(char standard, char alternative) {
757             return skipChar(standard) || skipChar(alternative);
758         }
759
760         final boolean skipPair(char first, char second) {
761             if (skipChar(first)) {
762                 if (skipChar(second)) {
763                     return true;
764                 } else {
765                     pos.setIndex(pos.getIndex() - 1);
766                 }
767             }
768             return false;
769         }
770
771         final boolean skipChar(char ch) {
772             if (pos.getIndex() < text.length()
773                     && text.charAt(pos.getIndex()) == ch) {
774                 pos.setIndex(pos.getIndex() + 1);
775                 return true;
776             } else {
777                 return false;
778             }
779         }
780
781         final boolean peekAsciiDigit() {
782             return (pos.getIndex() < text.length()
783                     && '0' <= text.charAt(pos.getIndex())
784                     && text.charAt(pos.getIndex()) <= '9');
785         }
786
787         boolean peekFoldingWhiteSpace() {
788             return (pos.getIndex() < text.length()
789                     && (text.charAt(pos.getIndex()) == ' '
790                     || text.charAt(pos.getIndex()) == '\t'
791                     || text.charAt(pos.getIndex()) == '\r'));
792         }
793
794         final boolean peekChar(char ch) {
795             return (pos.getIndex() < text.length()
796                     && text.charAt(pos.getIndex()) == ch);
797         }
798
799     }
800
801     private class Rfc2822StrictParser extends AbstractDateParser {
802
803         Rfc2822StrictParser(String text, ParsePosition pos) {
804             super(text, pos);
805         }
806
807         @Override
808         Date tryParse() throws ParseException {
809             int dayName = parseOptionalBegin();
810
811             int day = parseDay();
812             int month = parseMonth();
813             int year = parseYear();
814
815             parseFoldingWhiteSpace();
816
817             int hour = parseHour();
818             parseChar(':');
819             int minute = parseMinute();
820             int second = (skipChar(':')) ? parseSecond() : 0;
821
822             parseFwsBetweenTimeOfDayAndZone();
823
824             int zone = parseZone();
825
826             try {
827                 return MailDateFormat.this.toDate(dayName, day, month, year,
828                         hour, minute, second, zone);
829             } catch (IllegalArgumentException e) {
830                 throw new ParseException("Invalid input: some of the calendar "
831                         + "fields have invalid values, or day-name is "
832                         + "inconsistent with date", pos.getIndex());
833             }
834         }
835
836         /**
837          * @return the java.util.Calendar constant for the parsed day name, or
838          * UNKNOWN_DAY_NAME iff the begin is missing
839          */

840         int parseOptionalBegin() throws ParseException {
841             int dayName;
842             if (!peekAsciiDigit()) {
843                 skipFoldingWhiteSpace();
844                 dayName = parseDayName();
845                 parseChar(',');
846             } else {
847                 dayName = UNKNOWN_DAY_NAME;
848             }
849             return dayName;
850         }
851
852         int parseDay() throws ParseException {
853             skipFoldingWhiteSpace();
854             return parseAsciiDigits(1, 2);
855         }
856
857         /**
858          * @return the java.util.Calendar constant for the parsed month name
859          */

860         int parseMonth() throws ParseException {
861             parseFwsInMonth();
862             int month = parseMonthName(isMonthNameCaseSensitive());
863             parseFwsInMonth();
864             return month;
865         }
866
867         void parseFwsInMonth() throws ParseException {
868             parseFoldingWhiteSpace();
869         }
870
871         boolean isMonthNameCaseSensitive() {
872             return true;
873         }
874
875         int parseYear() throws ParseException {
876             int year = parseAsciiDigits(4, MAX_YEAR_DIGITS);
877             if (year >= 1900) {
878                 return year;
879             } else {
880                 pos.setIndex(pos.getIndex() - 4);
881                 while (text.charAt(pos.getIndex() - 1) == '0') {
882                     pos.setIndex(pos.getIndex() - 1);
883                 }
884                 throw new ParseException("Invalid year", pos.getIndex());
885             }
886         }
887
888         int parseHour() throws ParseException {
889             return parseAsciiDigits(2);
890         }
891
892         int parseMinute() throws ParseException {
893             return parseAsciiDigits(2);
894         }
895
896         int parseSecond() throws ParseException {
897             return parseAsciiDigits(2);
898         }
899
900         void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
901             parseFoldingWhiteSpace();
902         }
903
904         int parseZone() throws ParseException {
905             return parseZoneOffset();
906         }
907
908     }
909
910     private class Rfc2822LenientParser extends Rfc2822StrictParser {
911
912         private Boolean hasDefaultFws;
913
914         Rfc2822LenientParser(String text, ParsePosition pos) {
915             super(text, pos);
916         }
917
918         @Override
919         int parseOptionalBegin() {
920             while (pos.getIndex() < text.length() && !peekAsciiDigit()) {
921                 pos.setIndex(pos.getIndex() + 1);
922             }
923
924             return UNKNOWN_DAY_NAME;
925         }
926
927         @Override
928         int parseDay() throws ParseException {
929             skipFoldingWhiteSpace();
930             return parseAsciiDigits(1, 3);
931         }
932
933         @Override
934         void parseFwsInMonth() throws ParseException {
935             // '-' is allowed to accomodate for the date format as specified in
936             // <a href="http://www.ietf.org/rfc/rfc3501.txt">RFC 3501</a>
937             if (hasDefaultFws == null) {
938                 hasDefaultFws = !skipChar('-');
939                 skipFoldingWhiteSpace();
940             } else if (hasDefaultFws) {
941                 skipFoldingWhiteSpace();
942             } else {
943                 parseChar('-');
944             }
945         }
946
947         @Override
948         boolean isMonthNameCaseSensitive() {
949             return false;
950         }
951
952         @Override
953         int parseYear() throws ParseException {
954             int year = parseAsciiDigits(1, MAX_YEAR_DIGITS);
955             if (year >= 1000) {
956                 return year;
957             } else if (year >= 50) {
958                 return year + 1900;
959             } else {
960                 return year + 2000;
961             }
962         }
963
964         @Override
965         int parseHour() throws ParseException {
966             return parseAsciiDigits(1, 2);
967         }
968
969         @Override
970         int parseMinute() throws ParseException {
971             return parseAsciiDigits(1, 2);
972         }
973
974         @Override
975         int parseSecond() throws ParseException {
976             return parseAsciiDigits(1, 2);
977         }
978
979         @Override
980         void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
981             skipFoldingWhiteSpace();
982         }
983
984         @Override
985         int parseZone() throws ParseException {
986             try {
987                 if (pos.getIndex() >= text.length()) {
988                     throw new ParseException("Missing zone", pos.getIndex());
989                 }
990
991                 if (peekChar('+') || peekChar('-')) {
992                     return parseZoneOffset();
993                 } else if (skipAlternativePair('U', 'u', 'T', 't')) {
994                     return 0;
995                 } else if (skipAlternativeTriple('G', 'g', 'M', 'm',
996                         'T', 't')) {
997                     return 0;
998                 } else {
999                     int hoursOffset;
1000                     if (skipAlternative('E', 'e')) {
1001                         hoursOffset = 4;
1002                     } else if (skipAlternative('C', 'c')) {
1003                         hoursOffset = 5;
1004                     } else if (skipAlternative('M', 'm')) {
1005                         hoursOffset = 6;
1006                     } else if (skipAlternative('P', 'p')) {
1007                         hoursOffset = 7;
1008                     } else {
1009                         throw new ParseException("Invalid zone",
1010                                 pos.getIndex());
1011                     }
1012                     if (skipAlternativePair('S', 's', 'T', 't')) {
1013                         hoursOffset += 1;
1014                     } else if (skipAlternativePair('D', 'd', 'T', 't')) {
1015                     } else {
1016                         pos.setIndex(pos.getIndex() - 1);
1017                         throw new ParseException("Invalid zone",
1018                                 pos.getIndex());
1019                     }
1020                     return hoursOffset * 60;
1021                 }
1022             } catch (ParseException e) {
1023                 if (LOGGER.isLoggable(Level.FINE)) {
1024                     LOGGER.log(Level.FINE, "No timezone? : '" + text + "'", e);
1025                 }
1026
1027                 return 0;
1028             }
1029         }
1030
1031         @Override
1032         boolean isValidZoneOffset(int offset) {
1033             return true;
1034         }
1035
1036         @Override
1037         boolean skipFoldingWhiteSpace() {
1038             boolean result = peekFoldingWhiteSpace();
1039
1040             skipLoop:
1041             while (pos.getIndex() < text.length()) {
1042                 switch (text.charAt(pos.getIndex())) {
1043                     case ' ':
1044                     case '\t':
1045                     case '\r':
1046                     case '\n':
1047                         pos.setIndex(pos.getIndex() + 1);
1048                         break;
1049                     default:
1050                         break skipLoop;
1051                 }
1052             }
1053
1054             return result;
1055         }
1056
1057         @Override
1058         boolean peekFoldingWhiteSpace() {
1059             return super.peekFoldingWhiteSpace()
1060                     || (pos.getIndex() < text.length()
1061                     && text.charAt(pos.getIndex()) == '\n');
1062         }
1063
1064     }
1065
1066 }
1067