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