Skip to content

Package: MailDateFormat$Rfc2822LenientParser

MailDateFormat$Rfc2822LenientParser

nameinstructionbranchcomplexitylinemethod
MailDateFormat.Rfc2822LenientParser(MailDateFormat, String, ParsePosition)
M: 9 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
isMonthNameCaseSensitive()
M: 2 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
isValidZoneOffset(int)
M: 2 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
parseDay()
M: 8 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
parseFwsBetweenTimeOfDayAndZone()
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
parseFwsInMonth()
M: 29 C: 0
0%
M: 6 C: 0
0%
M: 4 C: 0
0%
M: 7 C: 0
0%
M: 1 C: 0
0%
parseHour()
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
parseMinute()
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
parseOptionalBegin()
M: 21 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
parseSecond()
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
parseYear()
M: 21 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 6 C: 0
0%
M: 1 C: 0
0%
parseZone()
M: 145 C: 0
0%
M: 24 C: 0
0%
M: 13 C: 0
0%
M: 29 C: 0
0%
M: 1 C: 0
0%
peekFoldingWhiteSpace()
M: 22 C: 0
0%
M: 6 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
skipFoldingWhiteSpace()
M: 29 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 7 C: 0
0%
M: 1 C: 0
0%

Coverage

1: /*
2: * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
3: *
4: * This program and the accompanying materials are made available under the
5: * terms of the Eclipse Public License v. 2.0, which is available at
6: * http://www.eclipse.org/legal/epl-2.0.
7: *
8: * This Source Code may also be made available under the following Secondary
9: * Licenses when the conditions for such availability set forth in the
10: * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11: * version 2 with the GNU Classpath Exception, which is available at
12: * https://www.gnu.org/software/classpath/license.html.
13: *
14: * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15: */
16:
17: package jakarta.mail.internet;
18:
19: import java.io.IOException;
20: import java.io.ObjectInputStream;
21: import java.io.ObjectStreamException;
22: import java.text.DateFormatSymbols;
23: import java.text.FieldPosition;
24: import java.text.NumberFormat;
25: import java.text.ParseException;
26: import java.text.ParsePosition;
27: import java.text.SimpleDateFormat;
28: import java.util.Calendar;
29: import java.util.Date;
30: import java.util.Locale;
31: import java.util.TimeZone;
32: import java.util.logging.Level;
33: import java.util.logging.Logger;
34:
35: /**
36: * Formats and parses date specification based on
37: * <a href="http://www.ietf.org/rfc/rfc2822.txt" target="_top">RFC 2822</a>. <p>
38: *
39: * This class does not support methods that influence the format. It always
40: * formats the date based on the specification below.<p>
41: *
42: * 3.3. Date and Time Specification
43: * <p>
44: * Date and time occur in several header fields. This section specifies
45: * the syntax for a full date and time specification. Though folding
46: * white space is permitted throughout the date-time specification, it is
47: * RECOMMENDED that a single space be used in each place that FWS appears
48: * (whether it is required or optional); some older implementations may
49: * not interpret other occurrences of folding white space correctly.
50: * <pre>
51: * date-time = [ day-of-week "," ] date FWS time [CFWS]
52: *
53: * day-of-week = ([FWS] day-name) / obs-day-of-week
54: *
55: * day-name = "Mon" / "Tue" / "Wed" / "Thu" /
56: * "Fri" / "Sat" / "Sun"
57: *
58: * date = day month year
59: *
60: * year = 4*DIGIT / obs-year
61: *
62: * month = (FWS month-name FWS) / obs-month
63: *
64: * month-name = "Jan" / "Feb" / "Mar" / "Apr" /
65: * "May" / "Jun" / "Jul" / "Aug" /
66: * "Sep" / "Oct" / "Nov" / "Dec"
67: *
68: * day = ([FWS] 1*2DIGIT) / obs-day
69: *
70: * time = time-of-day FWS zone
71: *
72: * time-of-day = hour ":" minute [ ":" second ]
73: *
74: * hour = 2DIGIT / obs-hour
75: *
76: * minute = 2DIGIT / obs-minute
77: *
78: * second = 2DIGIT / obs-second
79: *
80: * zone = (( "+" / "-" ) 4DIGIT) / obs-zone
81: * </pre>
82: * The day is the numeric day of the month. The year is any numeric year
83: * 1900 or later.
84: * <p>
85: * The time-of-day specifies the number of hours, minutes, and optionally
86: * seconds since midnight of the date indicated.
87: * <p>
88: * The date and time-of-day SHOULD express local time.
89: * <p>
90: * The zone specifies the offset from Coordinated Universal Time (UTC,
91: * formerly referred to as "Greenwich Mean Time") that the date and
92: * time-of-day represent. The "+" or "-" indicates whether the
93: * time-of-day is ahead of (i.e., east of) or behind (i.e., west of)
94: * Universal Time. The first two digits indicate the number of hours
95: * difference from Universal Time, and the last two digits indicate the
96: * number of minutes difference from Universal Time. (Hence, +hhmm means
97: * +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The
98: * form "+0000" SHOULD be used to indicate a time zone at Universal Time.
99: * Though "-0000" also indicates Universal Time, it is used to indicate
100: * that the time was generated on a system that may be in a local time
101: * zone other than Universal Time and therefore indicates that the
102: * date-time contains no information about the local time zone.
103: * <p>
104: * A date-time specification MUST be semantically valid. That is, the
105: * day-of-the-week (if included) MUST be the day implied by the date, the
106: * numeric day-of-month MUST be between 1 and the number of days allowed
107: * for the specified month (in the specified year), the time-of-day MUST
108: * be in the range 00:00:00 through 23:59:60 (the number of seconds
109: * allowing for a leap second; see [STD12]), and the zone MUST be within
110: * the range -9959 through +9959.
111: *
112: * <h2><a id="synchronization">Synchronization</a></h2>
113: *
114: * <p>
115: * Date formats are not synchronized.
116: * It is recommended to create separate format instances for each thread.
117: * If multiple threads access a format concurrently, it must be synchronized
118: * externally.
119: *
120: * @author Anthony Vanelverdinghe
121: * @author Max Spivak
122: * @since JavaMail 1.2
123: */
124: public class MailDateFormat extends SimpleDateFormat {
125:
126: private static final long serialVersionUID = -8148227605210628779L;
127: private static final String PATTERN = "EEE, d MMM yyyy HH:mm:ss Z (z)";
128:
129: private static final Logger LOGGER = Logger.getLogger(MailDateFormat.class.getName());
130:
131: private static final int UNKNOWN_DAY_NAME = -1;
132: private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
133: private static final int LEAP_SECOND = 60;
134:
135: /**
136: * Create a new date format for the RFC2822 specification with lenient
137: * parsing.
138: */
139: public MailDateFormat() {
140: super(PATTERN, Locale.US);
141: }
142:
143: /**
144: * Allows to serialize instances such that they are deserializable with the
145: * previous implementation.
146: *
147: * @return the object to be serialized
148: * @throws ObjectStreamException never
149: */
150: private Object writeReplace() throws ObjectStreamException {
151: MailDateFormat fmt = new MailDateFormat();
152: fmt.superApplyPattern("EEE, d MMM yyyy HH:mm:ss 'XXXXX' (z)");
153: fmt.setTimeZone(getTimeZone());
154: return fmt;
155: }
156:
157: /**
158: * Allows to deserialize instances that were serialized with the previous
159: * implementation.
160: *
161: * @param in the stream containing the serialized object
162: * @throws IOException on read failures
163: * @throws ClassNotFoundException never
164: */
165: private void readObject(ObjectInputStream in)
166: throws IOException, ClassNotFoundException {
167: in.defaultReadObject();
168: super.applyPattern(PATTERN);
169: }
170:
171: /**
172: * Overrides Cloneable.
173: *
174: * @return a clone of this instance
175: * @since JavaMail 1.6
176: */
177: @Override
178: public MailDateFormat clone() {
179: return (MailDateFormat) super.clone();
180: }
181:
182: /**
183: * Formats the given date in the format specified by
184: * RFC 2822 in the current TimeZone.
185: *
186: * @param date the Date object
187: * @param dateStrBuf the formatted string
188: * @param fieldPosition the current field position
189: * @return StringBuffer the formatted String
190: * @since JavaMail 1.2
191: */
192: @Override
193: public StringBuffer format(Date date, StringBuffer dateStrBuf,
194: FieldPosition fieldPosition) {
195: return super.format(date, dateStrBuf, fieldPosition);
196: }
197:
198: /**
199: * Parses the given date in the format specified by
200: * RFC 2822.
201: * <ul>
202: * <li>With strict parsing, obs-* tokens are unsupported. Lenient parsing
203: * supports obs-year and obs-zone, with the exception of the 1-character
204: * military time zones.
205: * <li>The optional CFWS token at the end is not parsed.
206: * <li>RFC 2822 specifies that a zone of "-0000" indicates that the
207: * date-time contains no information about the local time zone. This class
208: * uses the UTC time zone in this case.
209: * </ul>
210: *
211: * @param text the formatted date to be parsed
212: * @param pos the current parse position
213: * @return Date the parsed date. In case of error, returns null.
214: * @since JavaMail 1.2
215: */
216: @Override
217: public Date parse(String text, ParsePosition pos) {
218: if (text == null || pos == null) {
219: throw new NullPointerException();
220: } else if (0 > pos.getIndex() || pos.getIndex() >= text.length()) {
221: return null;
222: }
223:
224: return isLenient()
225: ? new Rfc2822LenientParser(text, pos).parse()
226: : new Rfc2822StrictParser(text, pos).parse();
227: }
228:
229: /**
230: * This method always throws an UnsupportedOperationException and should not
231: * be used because RFC 2822 mandates a specific calendar.
232: *
233: * @throws UnsupportedOperationException if this method is invoked
234: */
235: @Override
236: public void setCalendar(Calendar newCalendar) {
237: throw new UnsupportedOperationException("Method "
238: + "setCalendar() shouldn't be called");
239: }
240:
241: /**
242: * This method always throws an UnsupportedOperationException and should not
243: * be used because RFC 2822 mandates a specific number format.
244: *
245: * @throws UnsupportedOperationException if this method is invoked
246: */
247: @Override
248: public void setNumberFormat(NumberFormat newNumberFormat) {
249: throw new UnsupportedOperationException("Method "
250: + "setNumberFormat() shouldn't be called");
251: }
252:
253: /**
254: * This method always throws an UnsupportedOperationException and should not
255: * be used because RFC 2822 mandates a specific pattern.
256: *
257: * @throws UnsupportedOperationException if this method is invoked
258: * @since JavaMail 1.6
259: */
260: @Override
261: public void applyLocalizedPattern(String pattern) {
262: throw new UnsupportedOperationException("Method "
263: + "applyLocalizedPattern() shouldn't be called");
264: }
265:
266: /**
267: * This method always throws an UnsupportedOperationException and should not
268: * be used because RFC 2822 mandates a specific pattern.
269: *
270: * @throws UnsupportedOperationException if this method is invoked
271: * @since JavaMail 1.6
272: */
273: @Override
274: public void applyPattern(String pattern) {
275: throw new UnsupportedOperationException("Method "
276: + "applyPattern() shouldn't be called");
277: }
278:
279: /**
280: * This method allows serialization to change the pattern.
281: */
282: private void superApplyPattern(String pattern) {
283: super.applyPattern(pattern);
284: }
285:
286: /**
287: * This method always throws an UnsupportedOperationException and should not
288: * be used because RFC 2822 mandates another strategy for interpreting
289: * 2-digits years.
290: *
291: * @return the start of the 100-year period into which two digit years are
292: * parsed
293: * @throws UnsupportedOperationException if this method is invoked
294: * @since JavaMail 1.6
295: */
296: @Override
297: public Date get2DigitYearStart() {
298: throw new UnsupportedOperationException("Method "
299: + "get2DigitYearStart() shouldn't be called");
300: }
301:
302: /**
303: * This method always throws an UnsupportedOperationException and should not
304: * be used because RFC 2822 mandates another strategy for interpreting
305: * 2-digits years.
306: *
307: * @throws UnsupportedOperationException if this method is invoked
308: * @since JavaMail 1.6
309: */
310: @Override
311: public void set2DigitYearStart(Date startDate) {
312: throw new UnsupportedOperationException("Method "
313: + "set2DigitYearStart() shouldn't be called");
314: }
315:
316: /**
317: * This method always throws an UnsupportedOperationException and should not
318: * be used because RFC 2822 mandates specific date format symbols.
319: *
320: * @throws UnsupportedOperationException if this method is invoked
321: * @since JavaMail 1.6
322: */
323: @Override
324: public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
325: throw new UnsupportedOperationException("Method "
326: + "setDateFormatSymbols() shouldn't be called");
327: }
328:
329: /**
330: * Returns the date, as specified by the parameters.
331: *
332: * @return the date, as specified by the parameters
333: * @throws IllegalArgumentException if this instance's Calendar is
334: * non-lenient and any of the parameters have invalid values, or if dayName
335: * is not consistent with day-month-year
336: */
337: private Date toDate(int dayName, int day, int month, int year,
338: int hour, int minute, int second, int zone) {
339: if (second == LEAP_SECOND) {
340: second = 59;
341: }
342:
343: TimeZone tz = calendar.getTimeZone();
344: try {
345: calendar.setTimeZone(UTC);
346: calendar.clear();
347: calendar.set(year, month, day, hour, minute, second);
348:
349: if (dayName == UNKNOWN_DAY_NAME
350: || dayName == calendar.get(Calendar.DAY_OF_WEEK)) {
351: calendar.add(Calendar.MINUTE, zone);
352: return calendar.getTime();
353: } else {
354: throw new IllegalArgumentException("Inconsistent day-name");
355: }
356: } finally {
357: calendar.setTimeZone(tz);
358: }
359: }
360:
361: /**
362: * This class provides the building blocks for date parsing.
363: * <p>
364: * It has the following invariants:
365: * <ul>
366: * <li>no exceptions are thrown, except for java.text.ParseException from
367: * parse* methods
368: * <li>when parse* throws ParseException OR get* returns INVALID_CHAR OR
369: * skip* returns false OR peek* is invoked, then pos.getIndex() on method
370: * exit is the same as it was on method entry
371: * </ul>
372: */
373: private static abstract class AbstractDateParser {
374:
375: static final int INVALID_CHAR = -1;
376: static final int MAX_YEAR_DIGITS = 8; // guarantees that:
377: // year < new GregorianCalendar().getMaximum(Calendar.YEAR)
378:
379: final String text;
380: final ParsePosition pos;
381:
382: AbstractDateParser(String text, ParsePosition pos) {
383: this.text = text;
384: this.pos = pos;
385: }
386:
387: final Date parse() {
388: int startPosition = pos.getIndex();
389: try {
390: return tryParse();
391: } catch (Exception e) { // == ParseException | RuntimeException e
392: if (LOGGER.isLoggable(Level.FINE)) {
393: LOGGER.log(Level.FINE, "Bad date: '" + text + "'", e);
394: }
395: pos.setErrorIndex(pos.getIndex());
396: pos.setIndex(startPosition);
397: return null;
398: }
399: }
400:
401: abstract Date tryParse() throws ParseException;
402:
403: /**
404: * @return the java.util.Calendar constant for the parsed day name
405: */
406: final int parseDayName() throws ParseException {
407: switch (getChar()) {
408: case 'S':
409: if (skipPair('u', 'n')) {
410: return Calendar.SUNDAY;
411: } else if (skipPair('a', 't')) {
412: return Calendar.SATURDAY;
413: }
414: break;
415: case 'T':
416: if (skipPair('u', 'e')) {
417: return Calendar.TUESDAY;
418: } else if (skipPair('h', 'u')) {
419: return Calendar.THURSDAY;
420: }
421: break;
422: case 'M':
423: if (skipPair('o', 'n')) {
424: return Calendar.MONDAY;
425: }
426: break;
427: case 'W':
428: if (skipPair('e', 'd')) {
429: return Calendar.WEDNESDAY;
430: }
431: break;
432: case 'F':
433: if (skipPair('r', 'i')) {
434: return Calendar.FRIDAY;
435: }
436: break;
437: case INVALID_CHAR:
438: throw new ParseException("Invalid day-name",
439: pos.getIndex());
440: }
441: pos.setIndex(pos.getIndex() - 1);
442: throw new ParseException("Invalid day-name", pos.getIndex());
443: }
444:
445: /**
446: * @return the java.util.Calendar constant for the parsed month name
447: */
448: @SuppressWarnings("fallthrough")
449: final int parseMonthName(boolean caseSensitive) throws ParseException {
450: switch (getChar()) {
451: case 'j':
452: if (caseSensitive) {
453: break;
454: }
455: case 'J':
456: if (skipChar('u') || (!caseSensitive && skipChar('U'))) {
457: if (skipChar('l') || (!caseSensitive
458: && skipChar('L'))) {
459: return Calendar.JULY;
460: } else if (skipChar('n') || (!caseSensitive
461: && skipChar('N'))) {
462: return Calendar.JUNE;
463: } else {
464: pos.setIndex(pos.getIndex() - 1);
465: }
466: } else if (skipPair('a', 'n') || (!caseSensitive
467: && skipAlternativePair('a', 'A', 'n', 'N'))) {
468: return Calendar.JANUARY;
469: }
470: break;
471: case 'm':
472: if (caseSensitive) {
473: break;
474: }
475: case 'M':
476: if (skipChar('a') || (!caseSensitive && skipChar('A'))) {
477: if (skipChar('r') || (!caseSensitive
478: && skipChar('R'))) {
479: return Calendar.MARCH;
480: } else if (skipChar('y') || (!caseSensitive
481: && skipChar('Y'))) {
482: return Calendar.MAY;
483: } else {
484: pos.setIndex(pos.getIndex() - 1);
485: }
486: }
487: break;
488: case 'a':
489: if (caseSensitive) {
490: break;
491: }
492: case 'A':
493: if (skipPair('u', 'g') || (!caseSensitive
494: && skipAlternativePair('u', 'U', 'g', 'G'))) {
495: return Calendar.AUGUST;
496: } else if (skipPair('p', 'r') || (!caseSensitive
497: && skipAlternativePair('p', 'P', 'r', 'R'))) {
498: return Calendar.APRIL;
499: }
500: break;
501: case 'd':
502: if (caseSensitive) {
503: break;
504: }
505: case 'D':
506: if (skipPair('e', 'c') || (!caseSensitive
507: && skipAlternativePair('e', 'E', 'c', 'C'))) {
508: return Calendar.DECEMBER;
509: }
510: break;
511: case 'o':
512: if (caseSensitive) {
513: break;
514: }
515: case 'O':
516: if (skipPair('c', 't') || (!caseSensitive
517: && skipAlternativePair('c', 'C', 't', 'T'))) {
518: return Calendar.OCTOBER;
519: }
520: break;
521: case 's':
522: if (caseSensitive) {
523: break;
524: }
525: case 'S':
526: if (skipPair('e', 'p') || (!caseSensitive
527: && skipAlternativePair('e', 'E', 'p', 'P'))) {
528: return Calendar.SEPTEMBER;
529: }
530: break;
531: case 'n':
532: if (caseSensitive) {
533: break;
534: }
535: case 'N':
536: if (skipPair('o', 'v') || (!caseSensitive
537: && skipAlternativePair('o', 'O', 'v', 'V'))) {
538: return Calendar.NOVEMBER;
539: }
540: break;
541: case 'f':
542: if (caseSensitive) {
543: break;
544: }
545: case 'F':
546: if (skipPair('e', 'b') || (!caseSensitive
547: && skipAlternativePair('e', 'E', 'b', 'B'))) {
548: return Calendar.FEBRUARY;
549: }
550: break;
551: case INVALID_CHAR:
552: throw new ParseException("Invalid month", pos.getIndex());
553: }
554: pos.setIndex(pos.getIndex() - 1);
555: throw new ParseException("Invalid month", pos.getIndex());
556: }
557:
558: /**
559: * @return the number of minutes to be added to the time in the local
560: * time zone, in order to obtain the equivalent time in the UTC time
561: * zone. Returns 0 if the date-time contains no information about the
562: * local time zone.
563: */
564: final int parseZoneOffset() throws ParseException {
565: int sign = getChar();
566: if (sign == '+' || sign == '-') {
567: int offset = parseAsciiDigits(4, 4, true);
568: if (!isValidZoneOffset(offset)) {
569: pos.setIndex(pos.getIndex() - 5);
570: throw new ParseException("Invalid zone", pos.getIndex());
571: }
572:
573: return ((sign == '+') ? -1 : 1)
574: * (offset / 100 * 60 + offset % 100);
575: } else if (sign != INVALID_CHAR) {
576: pos.setIndex(pos.getIndex() - 1);
577: }
578: throw new ParseException("Invalid zone", pos.getIndex());
579: }
580:
581: boolean isValidZoneOffset(int offset) {
582: return (offset % 100) < 60;
583: }
584:
585: final int parseAsciiDigits(int count) throws ParseException {
586: return parseAsciiDigits(count, count);
587: }
588:
589: final int parseAsciiDigits(int min, int max) throws ParseException {
590: return parseAsciiDigits(min, max, false);
591: }
592:
593: final int parseAsciiDigits(int min, int max, boolean isEOF)
594: throws ParseException {
595: int result = 0;
596: int nbDigitsParsed = 0;
597: while (nbDigitsParsed < max && peekAsciiDigit()) {
598: result = result * 10 + getAsciiDigit();
599: nbDigitsParsed++;
600: }
601:
602: if ((nbDigitsParsed < min)
603: || (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) {
604: pos.setIndex(pos.getIndex() - nbDigitsParsed);
605: } else {
606: return result;
607: }
608:
609: String range = (min == max)
610: ? Integer.toString(min)
611: : "between " + min + " and " + max;
612: throw new ParseException("Invalid input: expected "
613: + range + " ASCII digits", pos.getIndex());
614: }
615:
616: final void parseFoldingWhiteSpace() throws ParseException {
617: if (!skipFoldingWhiteSpace()) {
618: throw new ParseException("Invalid input: expected FWS",
619: pos.getIndex());
620: }
621: }
622:
623: final void parseChar(char ch) throws ParseException {
624: if (!skipChar(ch)) {
625: throw new ParseException("Invalid input: expected '" + ch + "'",
626: pos.getIndex());
627: }
628: }
629:
630: final int getAsciiDigit() {
631: int ch = getChar();
632: if ('0' <= ch && ch <= '9') {
633: return Character.digit((char) ch, 10);
634: } else {
635: if (ch != INVALID_CHAR) {
636: pos.setIndex(pos.getIndex() - 1);
637: }
638: return INVALID_CHAR;
639: }
640: }
641:
642: final int getChar() {
643: if (pos.getIndex() < text.length()) {
644: char ch = text.charAt(pos.getIndex());
645: pos.setIndex(pos.getIndex() + 1);
646: return ch;
647: } else {
648: return INVALID_CHAR;
649: }
650: }
651:
652: boolean skipFoldingWhiteSpace() {
653: // fast paths: a single ASCII space or no FWS
654: if (skipChar(' ')) {
655: if (!peekFoldingWhiteSpace()) {
656: return true;
657: } else {
658: pos.setIndex(pos.getIndex() - 1);
659: }
660: } else if (!peekFoldingWhiteSpace()) {
661: return false;
662: }
663:
664: // normal path
665: int startIndex = pos.getIndex();
666: if (skipWhiteSpace()) {
667: while (skipNewline()) {
668: if (!skipWhiteSpace()) {
669: pos.setIndex(startIndex);
670: return false;
671: }
672: }
673: return true;
674: } else if (skipNewline() && skipWhiteSpace()) {
675: return true;
676: } else {
677: pos.setIndex(startIndex);
678: return false;
679: }
680: }
681:
682: final boolean skipWhiteSpace() {
683: int startIndex = pos.getIndex();
684: while (skipAlternative(' ', '\t')) { /* empty */ }
685: return pos.getIndex() > startIndex;
686: }
687:
688: final boolean skipNewline() {
689: return skipPair('\r', '\n');
690: }
691:
692: final boolean skipAlternativeTriple(
693: char firstStandard, char firstAlternative,
694: char secondStandard, char secondAlternative,
695: char thirdStandard, char thirdAlternative
696: ) {
697: if (skipAlternativePair(firstStandard, firstAlternative,
698: secondStandard, secondAlternative)) {
699: if (skipAlternative(thirdStandard, thirdAlternative)) {
700: return true;
701: } else {
702: pos.setIndex(pos.getIndex() - 2);
703: }
704: }
705: return false;
706: }
707:
708: final boolean skipAlternativePair(
709: char firstStandard, char firstAlternative,
710: char secondStandard, char secondAlternative
711: ) {
712: if (skipAlternative(firstStandard, firstAlternative)) {
713: if (skipAlternative(secondStandard, secondAlternative)) {
714: return true;
715: } else {
716: pos.setIndex(pos.getIndex() - 1);
717: }
718: }
719: return false;
720: }
721:
722: final boolean skipAlternative(char standard, char alternative) {
723: return skipChar(standard) || skipChar(alternative);
724: }
725:
726: final boolean skipPair(char first, char second) {
727: if (skipChar(first)) {
728: if (skipChar(second)) {
729: return true;
730: } else {
731: pos.setIndex(pos.getIndex() - 1);
732: }
733: }
734: return false;
735: }
736:
737: final boolean skipChar(char ch) {
738: if (pos.getIndex() < text.length()
739: && text.charAt(pos.getIndex()) == ch) {
740: pos.setIndex(pos.getIndex() + 1);
741: return true;
742: } else {
743: return false;
744: }
745: }
746:
747: final boolean peekAsciiDigit() {
748: return (pos.getIndex() < text.length()
749: && '0' <= text.charAt(pos.getIndex())
750: && text.charAt(pos.getIndex()) <= '9');
751: }
752:
753: boolean peekFoldingWhiteSpace() {
754: return (pos.getIndex() < text.length()
755: && (text.charAt(pos.getIndex()) == ' '
756: || text.charAt(pos.getIndex()) == '\t'
757: || text.charAt(pos.getIndex()) == '\r'));
758: }
759:
760: final boolean peekChar(char ch) {
761: return (pos.getIndex() < text.length()
762: && text.charAt(pos.getIndex()) == ch);
763: }
764:
765: }
766:
767: private class Rfc2822StrictParser extends AbstractDateParser {
768:
769: Rfc2822StrictParser(String text, ParsePosition pos) {
770: super(text, pos);
771: }
772:
773: @Override
774: Date tryParse() throws ParseException {
775: int dayName = parseOptionalBegin();
776:
777: int day = parseDay();
778: int month = parseMonth();
779: int year = parseYear();
780:
781: parseFoldingWhiteSpace();
782:
783: int hour = parseHour();
784: parseChar(':');
785: int minute = parseMinute();
786: int second = (skipChar(':')) ? parseSecond() : 0;
787:
788: parseFwsBetweenTimeOfDayAndZone();
789:
790: int zone = parseZone();
791:
792: try {
793: return MailDateFormat.this.toDate(dayName, day, month, year,
794: hour, minute, second, zone);
795: } catch (IllegalArgumentException e) {
796: throw new ParseException("Invalid input: some of the calendar "
797: + "fields have invalid values, or day-name is "
798: + "inconsistent with date", pos.getIndex());
799: }
800: }
801:
802: /**
803: * @return the java.util.Calendar constant for the parsed day name, or
804: * UNKNOWN_DAY_NAME iff the begin is missing
805: */
806: int parseOptionalBegin() throws ParseException {
807: int dayName;
808: if (!peekAsciiDigit()) {
809: skipFoldingWhiteSpace();
810: dayName = parseDayName();
811: parseChar(',');
812: } else {
813: dayName = UNKNOWN_DAY_NAME;
814: }
815: return dayName;
816: }
817:
818: int parseDay() throws ParseException {
819: skipFoldingWhiteSpace();
820: return parseAsciiDigits(1, 2);
821: }
822:
823: /**
824: * @return the java.util.Calendar constant for the parsed month name
825: */
826: int parseMonth() throws ParseException {
827: parseFwsInMonth();
828: int month = parseMonthName(isMonthNameCaseSensitive());
829: parseFwsInMonth();
830: return month;
831: }
832:
833: void parseFwsInMonth() throws ParseException {
834: parseFoldingWhiteSpace();
835: }
836:
837: boolean isMonthNameCaseSensitive() {
838: return true;
839: }
840:
841: int parseYear() throws ParseException {
842: int year = parseAsciiDigits(4, MAX_YEAR_DIGITS);
843: if (year >= 1900) {
844: return year;
845: } else {
846: pos.setIndex(pos.getIndex() - 4);
847: while (text.charAt(pos.getIndex() - 1) == '0') {
848: pos.setIndex(pos.getIndex() - 1);
849: }
850: throw new ParseException("Invalid year", pos.getIndex());
851: }
852: }
853:
854: int parseHour() throws ParseException {
855: return parseAsciiDigits(2);
856: }
857:
858: int parseMinute() throws ParseException {
859: return parseAsciiDigits(2);
860: }
861:
862: int parseSecond() throws ParseException {
863: return parseAsciiDigits(2);
864: }
865:
866: void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
867: parseFoldingWhiteSpace();
868: }
869:
870: int parseZone() throws ParseException {
871: return parseZoneOffset();
872: }
873:
874: }
875:
876: private class Rfc2822LenientParser extends Rfc2822StrictParser {
877:
878: private Boolean hasDefaultFws;
879:
880: Rfc2822LenientParser(String text, ParsePosition pos) {
881: super(text, pos);
882: }
883:
884: @Override
885: int parseOptionalBegin() {
886:• while (pos.getIndex() < text.length() && !peekAsciiDigit()) {
887: pos.setIndex(pos.getIndex() + 1);
888: }
889:
890: return UNKNOWN_DAY_NAME;
891: }
892:
893: @Override
894: int parseDay() throws ParseException {
895: skipFoldingWhiteSpace();
896: return parseAsciiDigits(1, 3);
897: }
898:
899: @Override
900: void parseFwsInMonth() throws ParseException {
901: // '-' is allowed to accomodate for the date format as specified in
902: // <a href="http://www.ietf.org/rfc/rfc3501.txt">RFC 3501</a>
903:• if (hasDefaultFws == null) {
904:• hasDefaultFws = !skipChar('-');
905: skipFoldingWhiteSpace();
906:• } else if (hasDefaultFws) {
907: skipFoldingWhiteSpace();
908: } else {
909: parseChar('-');
910: }
911: }
912:
913: @Override
914: boolean isMonthNameCaseSensitive() {
915: return false;
916: }
917:
918: @Override
919: int parseYear() throws ParseException {
920: int year = parseAsciiDigits(1, MAX_YEAR_DIGITS);
921:• if (year >= 1000) {
922: return year;
923:• } else if (year >= 50) {
924: return year + 1900;
925: } else {
926: return year + 2000;
927: }
928: }
929:
930: @Override
931: int parseHour() throws ParseException {
932: return parseAsciiDigits(1, 2);
933: }
934:
935: @Override
936: int parseMinute() throws ParseException {
937: return parseAsciiDigits(1, 2);
938: }
939:
940: @Override
941: int parseSecond() throws ParseException {
942: return parseAsciiDigits(1, 2);
943: }
944:
945: @Override
946: void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
947: skipFoldingWhiteSpace();
948: }
949:
950: @Override
951: int parseZone() throws ParseException {
952: try {
953:• if (pos.getIndex() >= text.length()) {
954: throw new ParseException("Missing zone", pos.getIndex());
955: }
956:
957:• if (peekChar('+') || peekChar('-')) {
958: return parseZoneOffset();
959:• } else if (skipAlternativePair('U', 'u', 'T', 't')) {
960: return 0;
961:• } else if (skipAlternativeTriple('G', 'g', 'M', 'm',
962: 'T', 't')) {
963: return 0;
964: } else {
965: int hoursOffset;
966:• if (skipAlternative('E', 'e')) {
967: hoursOffset = 4;
968:• } else if (skipAlternative('C', 'c')) {
969: hoursOffset = 5;
970:• } else if (skipAlternative('M', 'm')) {
971: hoursOffset = 6;
972:• } else if (skipAlternative('P', 'p')) {
973: hoursOffset = 7;
974: } else {
975: throw new ParseException("Invalid zone",
976: pos.getIndex());
977: }
978:• if (skipAlternativePair('S', 's', 'T', 't')) {
979: hoursOffset += 1;
980:• } else if (skipAlternativePair('D', 'd', 'T', 't')) {
981: } else {
982: pos.setIndex(pos.getIndex() - 1);
983: throw new ParseException("Invalid zone",
984: pos.getIndex());
985: }
986: return hoursOffset * 60;
987: }
988: } catch (ParseException e) {
989:• if (LOGGER.isLoggable(Level.FINE)) {
990: LOGGER.log(Level.FINE, "No timezone? : '" + text + "'", e);
991: }
992:
993: return 0;
994: }
995: }
996:
997: @Override
998: boolean isValidZoneOffset(int offset) {
999: return true;
1000: }
1001:
1002: @Override
1003: boolean skipFoldingWhiteSpace() {
1004: boolean result = peekFoldingWhiteSpace();
1005:
1006: skipLoop:
1007:• while (pos.getIndex() < text.length()) {
1008:• switch (text.charAt(pos.getIndex())) {
1009: case ' ':
1010: case '\t':
1011: case '\r':
1012: case '\n':
1013: pos.setIndex(pos.getIndex() + 1);
1014: break;
1015: default:
1016: break skipLoop;
1017: }
1018: }
1019:
1020: return result;
1021: }
1022:
1023: @Override
1024: boolean peekFoldingWhiteSpace() {
1025:• return super.peekFoldingWhiteSpace()
1026:• || (pos.getIndex() < text.length()
1027:• && text.charAt(pos.getIndex()) == '\n');
1028: }
1029:
1030: }
1031:
1032: }