Skip to content

Package: MailDateFormat

MailDateFormat

nameinstructionbranchcomplexitylinemethod
MailDateFormat()
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
applyLocalizedPattern(String)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
applyPattern(String)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
clone()
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
format(Date, StringBuffer, FieldPosition)
M: 0 C: 6
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
get2DigitYearStart()
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
parse(String, ParsePosition)
M: 0 C: 38
100%
M: 0 C: 10
100%
M: 0 C: 6
100%
M: 0 C: 7
100%
M: 0 C: 1
100%
readObject(ObjectInputStream)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
set2DigitYearStart(Date)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
setCalendar(Calendar)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
setDateFormatSymbols(DateFormatSymbols)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
setNumberFormat(NumberFormat)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
static {...}
M: 0 C: 8
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
superApplyPattern(String)
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%
toDate(int, int, int, int, int, int, int, int)
M: 0 C: 54
100%
M: 1 C: 5
83%
M: 1 C: 3
75%
M: 0 C: 12
100%
M: 0 C: 1
100%
writeReplace()
M: 13 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 4 C: 0
0%
M: 1 C: 0
0%

Coverage

1: /*
2: * Copyright (c) 1997, 2021 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: * @param dayName
333: * @param day
334: * @param month
335: * @param year
336: * @param hour
337: * @param minute
338: * @param second
339: * @param zone
340: * @return the date, as specified by the parameters
341: * @throws IllegalArgumentException if this instance's Calendar is
342: * non-lenient and any of the parameters have invalid values, or if dayName
343: * is not consistent with day-month-year
344: */
345: private Date toDate(int dayName, int day, int month, int year,
346: int hour, int minute, int second, int zone) {
347:• if (second == LEAP_SECOND) {
348: second = 59;
349: }
350:
351: TimeZone tz = calendar.getTimeZone();
352: try {
353: calendar.setTimeZone(UTC);
354: calendar.clear();
355: calendar.set(year, month, day, hour, minute, second);
356:
357:• if (dayName == UNKNOWN_DAY_NAME
358:• || dayName == calendar.get(Calendar.DAY_OF_WEEK)) {
359: calendar.add(Calendar.MINUTE, zone);
360: return calendar.getTime();
361: } else {
362: throw new IllegalArgumentException("Inconsistent day-name");
363: }
364: } finally {
365: calendar.setTimeZone(tz);
366: }
367: }
368:
369: /**
370: * This class provides the building blocks for date parsing.
371: * <p>
372: * It has the following invariants:
373: * <ul>
374: * <li>no exceptions are thrown, except for java.text.ParseException from
375: * parse* methods
376: * <li>when parse* throws ParseException OR get* returns INVALID_CHAR OR
377: * skip* returns false OR peek* is invoked, then pos.getIndex() on method
378: * exit is the same as it was on method entry
379: * </ul>
380: */
381: private static abstract class AbstractDateParser {
382:
383: static final int INVALID_CHAR = -1;
384: static final int MAX_YEAR_DIGITS = 8; // guarantees that:
385: // year < new GregorianCalendar().getMaximum(Calendar.YEAR)
386:
387: final String text;
388: final ParsePosition pos;
389:
390: AbstractDateParser(String text, ParsePosition pos) {
391: this.text = text;
392: this.pos = pos;
393: }
394:
395: final Date parse() {
396: int startPosition = pos.getIndex();
397: try {
398: return tryParse();
399: } catch (Exception e) { // == ParseException | RuntimeException e
400: if (LOGGER.isLoggable(Level.FINE)) {
401: LOGGER.log(Level.FINE, "Bad date: '" + text + "'", e);
402: }
403: pos.setErrorIndex(pos.getIndex());
404: pos.setIndex(startPosition);
405: return null;
406: }
407: }
408:
409: abstract Date tryParse() throws ParseException;
410:
411: /**
412: * @return the java.util.Calendar constant for the parsed day name
413: */
414: final int parseDayName() throws ParseException {
415: switch (getChar()) {
416: case 'S':
417: if (skipPair('u', 'n')) {
418: return Calendar.SUNDAY;
419: } else if (skipPair('a', 't')) {
420: return Calendar.SATURDAY;
421: }
422: break;
423: case 'T':
424: if (skipPair('u', 'e')) {
425: return Calendar.TUESDAY;
426: } else if (skipPair('h', 'u')) {
427: return Calendar.THURSDAY;
428: }
429: break;
430: case 'M':
431: if (skipPair('o', 'n')) {
432: return Calendar.MONDAY;
433: }
434: break;
435: case 'W':
436: if (skipPair('e', 'd')) {
437: return Calendar.WEDNESDAY;
438: }
439: break;
440: case 'F':
441: if (skipPair('r', 'i')) {
442: return Calendar.FRIDAY;
443: }
444: break;
445: case INVALID_CHAR:
446: throw new ParseException("Invalid day-name",
447: pos.getIndex());
448: }
449: pos.setIndex(pos.getIndex() - 1);
450: throw new ParseException("Invalid day-name", pos.getIndex());
451: }
452:
453: /**
454: * @return the java.util.Calendar constant for the parsed month name
455: */
456: @SuppressWarnings("fallthrough")
457: final int parseMonthName(boolean caseSensitive) throws ParseException {
458: switch (getChar()) {
459: case 'j':
460: if (caseSensitive) {
461: break;
462: }
463: case 'J':
464: if (skipChar('u') || (!caseSensitive && skipChar('U'))) {
465: if (skipChar('l') || (!caseSensitive
466: && skipChar('L'))) {
467: return Calendar.JULY;
468: } else if (skipChar('n') || (!caseSensitive
469: && skipChar('N'))) {
470: return Calendar.JUNE;
471: } else {
472: pos.setIndex(pos.getIndex() - 1);
473: }
474: } else if (skipPair('a', 'n') || (!caseSensitive
475: && skipAlternativePair('a', 'A', 'n', 'N'))) {
476: return Calendar.JANUARY;
477: }
478: break;
479: case 'm':
480: if (caseSensitive) {
481: break;
482: }
483: case 'M':
484: if (skipChar('a') || (!caseSensitive && skipChar('A'))) {
485: if (skipChar('r') || (!caseSensitive
486: && skipChar('R'))) {
487: return Calendar.MARCH;
488: } else if (skipChar('y') || (!caseSensitive
489: && skipChar('Y'))) {
490: return Calendar.MAY;
491: } else {
492: pos.setIndex(pos.getIndex() - 1);
493: }
494: }
495: break;
496: case 'a':
497: if (caseSensitive) {
498: break;
499: }
500: case 'A':
501: if (skipPair('u', 'g') || (!caseSensitive
502: && skipAlternativePair('u', 'U', 'g', 'G'))) {
503: return Calendar.AUGUST;
504: } else if (skipPair('p', 'r') || (!caseSensitive
505: && skipAlternativePair('p', 'P', 'r', 'R'))) {
506: return Calendar.APRIL;
507: }
508: break;
509: case 'd':
510: if (caseSensitive) {
511: break;
512: }
513: case 'D':
514: if (skipPair('e', 'c') || (!caseSensitive
515: && skipAlternativePair('e', 'E', 'c', 'C'))) {
516: return Calendar.DECEMBER;
517: }
518: break;
519: case 'o':
520: if (caseSensitive) {
521: break;
522: }
523: case 'O':
524: if (skipPair('c', 't') || (!caseSensitive
525: && skipAlternativePair('c', 'C', 't', 'T'))) {
526: return Calendar.OCTOBER;
527: }
528: break;
529: case 's':
530: if (caseSensitive) {
531: break;
532: }
533: case 'S':
534: if (skipPair('e', 'p') || (!caseSensitive
535: && skipAlternativePair('e', 'E', 'p', 'P'))) {
536: return Calendar.SEPTEMBER;
537: }
538: break;
539: case 'n':
540: if (caseSensitive) {
541: break;
542: }
543: case 'N':
544: if (skipPair('o', 'v') || (!caseSensitive
545: && skipAlternativePair('o', 'O', 'v', 'V'))) {
546: return Calendar.NOVEMBER;
547: }
548: break;
549: case 'f':
550: if (caseSensitive) {
551: break;
552: }
553: case 'F':
554: if (skipPair('e', 'b') || (!caseSensitive
555: && skipAlternativePair('e', 'E', 'b', 'B'))) {
556: return Calendar.FEBRUARY;
557: }
558: break;
559: case INVALID_CHAR:
560: throw new ParseException("Invalid month", pos.getIndex());
561: }
562: pos.setIndex(pos.getIndex() - 1);
563: throw new ParseException("Invalid month", pos.getIndex());
564: }
565:
566: /**
567: * @return the number of minutes to be added to the time in the local
568: * time zone, in order to obtain the equivalent time in the UTC time
569: * zone. Returns 0 if the date-time contains no information about the
570: * local time zone.
571: */
572: final int parseZoneOffset() throws ParseException {
573: int sign = getChar();
574: if (sign == '+' || sign == '-') {
575: int offset = parseAsciiDigits(4, 4, true);
576: if (!isValidZoneOffset(offset)) {
577: pos.setIndex(pos.getIndex() - 5);
578: throw new ParseException("Invalid zone", pos.getIndex());
579: }
580:
581: return ((sign == '+') ? -1 : 1)
582: * (offset / 100 * 60 + offset % 100);
583: } else if (sign != INVALID_CHAR) {
584: pos.setIndex(pos.getIndex() - 1);
585: }
586: throw new ParseException("Invalid zone", pos.getIndex());
587: }
588:
589: boolean isValidZoneOffset(int offset) {
590: return (offset % 100) < 60;
591: }
592:
593: final int parseAsciiDigits(int count) throws ParseException {
594: return parseAsciiDigits(count, count);
595: }
596:
597: final int parseAsciiDigits(int min, int max) throws ParseException {
598: return parseAsciiDigits(min, max, false);
599: }
600:
601: final int parseAsciiDigits(int min, int max, boolean isEOF)
602: throws ParseException {
603: int result = 0;
604: int nbDigitsParsed = 0;
605: while (nbDigitsParsed < max && peekAsciiDigit()) {
606: result = result * 10 + getAsciiDigit();
607: nbDigitsParsed++;
608: }
609:
610: if ((nbDigitsParsed < min)
611: || (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) {
612: pos.setIndex(pos.getIndex() - nbDigitsParsed);
613: } else {
614: return result;
615: }
616:
617: String range = (min == max)
618: ? Integer.toString(min)
619: : "between " + min + " and " + max;
620: throw new ParseException("Invalid input: expected "
621: + range + " ASCII digits", pos.getIndex());
622: }
623:
624: final void parseFoldingWhiteSpace() throws ParseException {
625: if (!skipFoldingWhiteSpace()) {
626: throw new ParseException("Invalid input: expected FWS",
627: pos.getIndex());
628: }
629: }
630:
631: final void parseChar(char ch) throws ParseException {
632: if (!skipChar(ch)) {
633: throw new ParseException("Invalid input: expected '" + ch + "'",
634: pos.getIndex());
635: }
636: }
637:
638: final int getAsciiDigit() {
639: int ch = getChar();
640: if ('0' <= ch && ch <= '9') {
641: return Character.digit((char) ch, 10);
642: } else {
643: if (ch != INVALID_CHAR) {
644: pos.setIndex(pos.getIndex() - 1);
645: }
646: return INVALID_CHAR;
647: }
648: }
649:
650: final int getChar() {
651: if (pos.getIndex() < text.length()) {
652: char ch = text.charAt(pos.getIndex());
653: pos.setIndex(pos.getIndex() + 1);
654: return ch;
655: } else {
656: return INVALID_CHAR;
657: }
658: }
659:
660: boolean skipFoldingWhiteSpace() {
661: // fast paths: a single ASCII space or no FWS
662: if (skipChar(' ')) {
663: if (!peekFoldingWhiteSpace()) {
664: return true;
665: } else {
666: pos.setIndex(pos.getIndex() - 1);
667: }
668: } else if (!peekFoldingWhiteSpace()) {
669: return false;
670: }
671:
672: // normal path
673: int startIndex = pos.getIndex();
674: if (skipWhiteSpace()) {
675: while (skipNewline()) {
676: if (!skipWhiteSpace()) {
677: pos.setIndex(startIndex);
678: return false;
679: }
680: }
681: return true;
682: } else if (skipNewline() && skipWhiteSpace()) {
683: return true;
684: } else {
685: pos.setIndex(startIndex);
686: return false;
687: }
688: }
689:
690: final boolean skipWhiteSpace() {
691: int startIndex = pos.getIndex();
692: while (skipAlternative(' ', '\t')) { /* empty */ }
693: return pos.getIndex() > startIndex;
694: }
695:
696: final boolean skipNewline() {
697: return skipPair('\r', '\n');
698: }
699:
700: final boolean skipAlternativeTriple(
701: char firstStandard, char firstAlternative,
702: char secondStandard, char secondAlternative,
703: char thirdStandard, char thirdAlternative
704: ) {
705: if (skipAlternativePair(firstStandard, firstAlternative,
706: secondStandard, secondAlternative)) {
707: if (skipAlternative(thirdStandard, thirdAlternative)) {
708: return true;
709: } else {
710: pos.setIndex(pos.getIndex() - 2);
711: }
712: }
713: return false;
714: }
715:
716: final boolean skipAlternativePair(
717: char firstStandard, char firstAlternative,
718: char secondStandard, char secondAlternative
719: ) {
720: if (skipAlternative(firstStandard, firstAlternative)) {
721: if (skipAlternative(secondStandard, secondAlternative)) {
722: return true;
723: } else {
724: pos.setIndex(pos.getIndex() - 1);
725: }
726: }
727: return false;
728: }
729:
730: final boolean skipAlternative(char standard, char alternative) {
731: return skipChar(standard) || skipChar(alternative);
732: }
733:
734: final boolean skipPair(char first, char second) {
735: if (skipChar(first)) {
736: if (skipChar(second)) {
737: return true;
738: } else {
739: pos.setIndex(pos.getIndex() - 1);
740: }
741: }
742: return false;
743: }
744:
745: final boolean skipChar(char ch) {
746: if (pos.getIndex() < text.length()
747: && text.charAt(pos.getIndex()) == ch) {
748: pos.setIndex(pos.getIndex() + 1);
749: return true;
750: } else {
751: return false;
752: }
753: }
754:
755: final boolean peekAsciiDigit() {
756: return (pos.getIndex() < text.length()
757: && '0' <= text.charAt(pos.getIndex())
758: && text.charAt(pos.getIndex()) <= '9');
759: }
760:
761: boolean peekFoldingWhiteSpace() {
762: return (pos.getIndex() < text.length()
763: && (text.charAt(pos.getIndex()) == ' '
764: || text.charAt(pos.getIndex()) == '\t'
765: || text.charAt(pos.getIndex()) == '\r'));
766: }
767:
768: final boolean peekChar(char ch) {
769: return (pos.getIndex() < text.length()
770: && text.charAt(pos.getIndex()) == ch);
771: }
772:
773: }
774:
775: private class Rfc2822StrictParser extends AbstractDateParser {
776:
777: Rfc2822StrictParser(String text, ParsePosition pos) {
778: super(text, pos);
779: }
780:
781: @Override
782: Date tryParse() throws ParseException {
783: int dayName = parseOptionalBegin();
784:
785: int day = parseDay();
786: int month = parseMonth();
787: int year = parseYear();
788:
789: parseFoldingWhiteSpace();
790:
791: int hour = parseHour();
792: parseChar(':');
793: int minute = parseMinute();
794: int second = (skipChar(':')) ? parseSecond() : 0;
795:
796: parseFwsBetweenTimeOfDayAndZone();
797:
798: int zone = parseZone();
799:
800: try {
801: return MailDateFormat.this.toDate(dayName, day, month, year,
802: hour, minute, second, zone);
803: } catch (IllegalArgumentException e) {
804: throw new ParseException("Invalid input: some of the calendar "
805: + "fields have invalid values, or day-name is "
806: + "inconsistent with date", pos.getIndex());
807: }
808: }
809:
810: /**
811: * @return the java.util.Calendar constant for the parsed day name, or
812: * UNKNOWN_DAY_NAME iff the begin is missing
813: */
814: int parseOptionalBegin() throws ParseException {
815: int dayName;
816: if (!peekAsciiDigit()) {
817: skipFoldingWhiteSpace();
818: dayName = parseDayName();
819: parseChar(',');
820: } else {
821: dayName = UNKNOWN_DAY_NAME;
822: }
823: return dayName;
824: }
825:
826: int parseDay() throws ParseException {
827: skipFoldingWhiteSpace();
828: return parseAsciiDigits(1, 2);
829: }
830:
831: /**
832: * @return the java.util.Calendar constant for the parsed month name
833: */
834: int parseMonth() throws ParseException {
835: parseFwsInMonth();
836: int month = parseMonthName(isMonthNameCaseSensitive());
837: parseFwsInMonth();
838: return month;
839: }
840:
841: void parseFwsInMonth() throws ParseException {
842: parseFoldingWhiteSpace();
843: }
844:
845: boolean isMonthNameCaseSensitive() {
846: return true;
847: }
848:
849: int parseYear() throws ParseException {
850: int year = parseAsciiDigits(4, MAX_YEAR_DIGITS);
851: if (year >= 1900) {
852: return year;
853: } else {
854: pos.setIndex(pos.getIndex() - 4);
855: while (text.charAt(pos.getIndex() - 1) == '0') {
856: pos.setIndex(pos.getIndex() - 1);
857: }
858: throw new ParseException("Invalid year", pos.getIndex());
859: }
860: }
861:
862: int parseHour() throws ParseException {
863: return parseAsciiDigits(2);
864: }
865:
866: int parseMinute() throws ParseException {
867: return parseAsciiDigits(2);
868: }
869:
870: int parseSecond() throws ParseException {
871: return parseAsciiDigits(2);
872: }
873:
874: void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
875: parseFoldingWhiteSpace();
876: }
877:
878: int parseZone() throws ParseException {
879: return parseZoneOffset();
880: }
881:
882: }
883:
884: private class Rfc2822LenientParser extends Rfc2822StrictParser {
885:
886: private Boolean hasDefaultFws;
887:
888: Rfc2822LenientParser(String text, ParsePosition pos) {
889: super(text, pos);
890: }
891:
892: @Override
893: int parseOptionalBegin() {
894: while (pos.getIndex() < text.length() && !peekAsciiDigit()) {
895: pos.setIndex(pos.getIndex() + 1);
896: }
897:
898: return UNKNOWN_DAY_NAME;
899: }
900:
901: @Override
902: int parseDay() throws ParseException {
903: skipFoldingWhiteSpace();
904: return parseAsciiDigits(1, 3);
905: }
906:
907: @Override
908: void parseFwsInMonth() throws ParseException {
909: // '-' is allowed to accomodate for the date format as specified in
910: // <a href="http://www.ietf.org/rfc/rfc3501.txt">RFC 3501</a>
911: if (hasDefaultFws == null) {
912: hasDefaultFws = !skipChar('-');
913: skipFoldingWhiteSpace();
914: } else if (hasDefaultFws) {
915: skipFoldingWhiteSpace();
916: } else {
917: parseChar('-');
918: }
919: }
920:
921: @Override
922: boolean isMonthNameCaseSensitive() {
923: return false;
924: }
925:
926: @Override
927: int parseYear() throws ParseException {
928: int year = parseAsciiDigits(1, MAX_YEAR_DIGITS);
929: if (year >= 1000) {
930: return year;
931: } else if (year >= 50) {
932: return year + 1900;
933: } else {
934: return year + 2000;
935: }
936: }
937:
938: @Override
939: int parseHour() throws ParseException {
940: return parseAsciiDigits(1, 2);
941: }
942:
943: @Override
944: int parseMinute() throws ParseException {
945: return parseAsciiDigits(1, 2);
946: }
947:
948: @Override
949: int parseSecond() throws ParseException {
950: return parseAsciiDigits(1, 2);
951: }
952:
953: @Override
954: void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
955: skipFoldingWhiteSpace();
956: }
957:
958: @Override
959: int parseZone() throws ParseException {
960: try {
961: if (pos.getIndex() >= text.length()) {
962: throw new ParseException("Missing zone", pos.getIndex());
963: }
964:
965: if (peekChar('+') || peekChar('-')) {
966: return parseZoneOffset();
967: } else if (skipAlternativePair('U', 'u', 'T', 't')) {
968: return 0;
969: } else if (skipAlternativeTriple('G', 'g', 'M', 'm',
970: 'T', 't')) {
971: return 0;
972: } else {
973: int hoursOffset;
974: if (skipAlternative('E', 'e')) {
975: hoursOffset = 4;
976: } else if (skipAlternative('C', 'c')) {
977: hoursOffset = 5;
978: } else if (skipAlternative('M', 'm')) {
979: hoursOffset = 6;
980: } else if (skipAlternative('P', 'p')) {
981: hoursOffset = 7;
982: } else {
983: throw new ParseException("Invalid zone",
984: pos.getIndex());
985: }
986: if (skipAlternativePair('S', 's', 'T', 't')) {
987: hoursOffset += 1;
988: } else if (skipAlternativePair('D', 'd', 'T', 't')) {
989: } else {
990: pos.setIndex(pos.getIndex() - 1);
991: throw new ParseException("Invalid zone",
992: pos.getIndex());
993: }
994: return hoursOffset * 60;
995: }
996: } catch (ParseException e) {
997: if (LOGGER.isLoggable(Level.FINE)) {
998: LOGGER.log(Level.FINE, "No timezone? : '" + text + "'", e);
999: }
1000:
1001: return 0;
1002: }
1003: }
1004:
1005: @Override
1006: boolean isValidZoneOffset(int offset) {
1007: return true;
1008: }
1009:
1010: @Override
1011: boolean skipFoldingWhiteSpace() {
1012: boolean result = peekFoldingWhiteSpace();
1013:
1014: skipLoop:
1015: while (pos.getIndex() < text.length()) {
1016: switch (text.charAt(pos.getIndex())) {
1017: case ' ':
1018: case '\t':
1019: case '\r':
1020: case '\n':
1021: pos.setIndex(pos.getIndex() + 1);
1022: break;
1023: default:
1024: break skipLoop;
1025: }
1026: }
1027:
1028: return result;
1029: }
1030:
1031: @Override
1032: boolean peekFoldingWhiteSpace() {
1033: return super.peekFoldingWhiteSpace()
1034: || (pos.getIndex() < text.length()
1035: && text.charAt(pos.getIndex()) == '\n');
1036: }
1037:
1038: }
1039:
1040: }