/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://oss.oracle.com/licenses/CDDL+GPL-1.1
* or LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package javax.mail.internet;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamException;
import java.util.Date;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.text.NumberFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.text.ParseException;
import com.sun.mail.util.MailLogger;
Formats and parses date specification based on
RFC 2822.
This class does not support methods that influence the format. It always
formats the date based on the specification below.
3.3. Date and Time Specification
Date and time occur in several header fields. This section specifies
the syntax for a full date and time specification. Though folding
white space is permitted throughout the date-time specification, it is
RECOMMENDED that a single space be used in each place that FWS appears
(whether it is required or optional); some older implementations may
not interpret other occurrences of folding white space correctly.
date-time = [ day-of-week "," ] date FWS time [CFWS]
day-of-week = ([FWS] day-name) / obs-day-of-week
day-name = "Mon" / "Tue" / "Wed" / "Thu" /
"Fri" / "Sat" / "Sun"
date = day month year
year = 4*DIGIT / obs-year
month = (FWS month-name FWS) / obs-month
month-name = "Jan" / "Feb" / "Mar" / "Apr" /
"May" / "Jun" / "Jul" / "Aug" /
"Sep" / "Oct" / "Nov" / "Dec"
day = ([FWS] 1*2DIGIT) / obs-day
time = time-of-day FWS zone
time-of-day = hour ":" minute [ ":" second ]
hour = 2DIGIT / obs-hour
minute = 2DIGIT / obs-minute
second = 2DIGIT / obs-second
zone = (( "+" / "-" ) 4DIGIT) / obs-zone
The day is the numeric day of the month. The year is any numeric year
1900 or later.
The time-of-day specifies the number of hours, minutes, and optionally
seconds since midnight of the date indicated.
The date and time-of-day SHOULD express local time.
The zone specifies the offset from Coordinated Universal Time (UTC,
formerly referred to as "Greenwich Mean Time") that the date and
time-of-day represent. The "+" or "-" indicates whether the
time-of-day is ahead of (i.e., east of) or behind (i.e., west of)
Universal Time. The first two digits indicate the number of hours
difference from Universal Time, and the last two digits indicate the
number of minutes difference from Universal Time. (Hence, +hhmm means
+(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The
form "+0000" SHOULD be used to indicate a time zone at Universal Time.
Though "-0000" also indicates Universal Time, it is used to indicate
that the time was generated on a system that may be in a local time
zone other than Universal Time and therefore indicates that the
date-time contains no information about the local time zone.
A date-time specification MUST be semantically valid. That is, the
day-of-the-week (if included) MUST be the day implied by the date, the
numeric day-of-month MUST be between 1 and the number of days allowed
for the specified month (in the specified year), the time-of-day MUST
be in the range 00:00:00 through 23:59:60 (the number of seconds
allowing for a leap second; see [STD12]), and the zone MUST be within
the range -9959 through +9959.
Synchronization
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized
externally.
Author: Anthony Vanelverdinghe, Max Spivak Since: JavaMail 1.2
/**
* Formats and parses date specification based on
* <a href="http://www.ietf.org/rfc/rfc2822.txt" target="_top">RFC 2822</a>. <p>
*
* This class does not support methods that influence the format. It always
* formats the date based on the specification below.<p>
*
* 3.3. Date and Time Specification
* <p>
* Date and time occur in several header fields. This section specifies
* the syntax for a full date and time specification. Though folding
* white space is permitted throughout the date-time specification, it is
* RECOMMENDED that a single space be used in each place that FWS appears
* (whether it is required or optional); some older implementations may
* not interpret other occurrences of folding white space correctly.
* <pre>
* date-time = [ day-of-week "," ] date FWS time [CFWS]
*
* day-of-week = ([FWS] day-name) / obs-day-of-week
*
* day-name = "Mon" / "Tue" / "Wed" / "Thu" /
* "Fri" / "Sat" / "Sun"
*
* date = day month year
*
* year = 4*DIGIT / obs-year
*
* month = (FWS month-name FWS) / obs-month
*
* month-name = "Jan" / "Feb" / "Mar" / "Apr" /
* "May" / "Jun" / "Jul" / "Aug" /
* "Sep" / "Oct" / "Nov" / "Dec"
*
* day = ([FWS] 1*2DIGIT) / obs-day
*
* time = time-of-day FWS zone
*
* time-of-day = hour ":" minute [ ":" second ]
*
* hour = 2DIGIT / obs-hour
*
* minute = 2DIGIT / obs-minute
*
* second = 2DIGIT / obs-second
*
* zone = (( "+" / "-" ) 4DIGIT) / obs-zone
* </pre>
* The day is the numeric day of the month. The year is any numeric year
* 1900 or later.
* <p>
* The time-of-day specifies the number of hours, minutes, and optionally
* seconds since midnight of the date indicated.
* <p>
* The date and time-of-day SHOULD express local time.
* <p>
* The zone specifies the offset from Coordinated Universal Time (UTC,
* formerly referred to as "Greenwich Mean Time") that the date and
* time-of-day represent. The "+" or "-" indicates whether the
* time-of-day is ahead of (i.e., east of) or behind (i.e., west of)
* Universal Time. The first two digits indicate the number of hours
* difference from Universal Time, and the last two digits indicate the
* number of minutes difference from Universal Time. (Hence, +hhmm means
* +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The
* form "+0000" SHOULD be used to indicate a time zone at Universal Time.
* Though "-0000" also indicates Universal Time, it is used to indicate
* that the time was generated on a system that may be in a local time
* zone other than Universal Time and therefore indicates that the
* date-time contains no information about the local time zone.
* <p>
* A date-time specification MUST be semantically valid. That is, the
* day-of-the-week (if included) MUST be the day implied by the date, the
* numeric day-of-month MUST be between 1 and the number of days allowed
* for the specified month (in the specified year), the time-of-day MUST
* be in the range 00:00:00 through 23:59:60 (the number of seconds
* allowing for a leap second; see [STD12]), and the zone MUST be within
* the range -9959 through +9959.
*
* <h3><a name="synchronization">Synchronization</a></h3>
*
* <p>
* Date formats are not synchronized.
* It is recommended to create separate format instances for each thread.
* If multiple threads access a format concurrently, it must be synchronized
* externally.
*
* @author Anthony Vanelverdinghe
* @author Max Spivak
* @since JavaMail 1.2
*/
public class MailDateFormat extends SimpleDateFormat {
private static final long serialVersionUID = -8148227605210628779L;
private static final String PATTERN = "EEE, d MMM yyyy HH:mm:ss Z (z)";
private static final MailLogger LOGGER = new MailLogger(
MailDateFormat.class, "DEBUG", false, System.out);
private static final int UNKNOWN_DAY_NAME = -1;
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
private static final int LEAP_SECOND = 60;
Create a new date format for the RFC2822 specification with lenient
parsing.
/**
* Create a new date format for the RFC2822 specification with lenient
* parsing.
*/
public MailDateFormat() {
super(PATTERN, Locale.US);
}
Allows to serialize instances such that they are deserializable with the
previous implementation.
Throws: - ObjectStreamException – never
Returns: the object to be serialized
/**
* Allows to serialize instances such that they are deserializable with the
* previous implementation.
*
* @return the object to be serialized
* @throws ObjectStreamException never
*/
private Object writeReplace() throws ObjectStreamException {
MailDateFormat fmt = new MailDateFormat();
fmt.superApplyPattern("EEE, d MMM yyyy HH:mm:ss 'XXXXX' (z)");
fmt.setTimeZone(getTimeZone());
return fmt;
}
Allows to deserialize instances that were serialized with the previous
implementation.
Params: - in – the stream containing the serialized object
Throws: - IOException – on read failures
- ClassNotFoundException – never
/**
* Allows to deserialize instances that were serialized with the previous
* implementation.
*
* @param in the stream containing the serialized object
* @throws IOException on read failures
* @throws ClassNotFoundException never
*/
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
super.applyPattern(PATTERN);
}
Overrides Cloneable.
Returns: a clone of this instance Since: JavaMail 1.6
/**
* Overrides Cloneable.
*
* @return a clone of this instance
* @since JavaMail 1.6
*/
@Override
public MailDateFormat clone() {
return (MailDateFormat) super.clone();
}
Formats the given date in the format specified by
RFC 2822 in the current TimeZone.
Params: - date – the Date object
- dateStrBuf – the formatted string
- fieldPosition – the current field position
Returns: StringBuffer the formatted String Since: JavaMail 1.2
/**
* Formats the given date in the format specified by
* RFC 2822 in the current TimeZone.
*
* @param date the Date object
* @param dateStrBuf the formatted string
* @param fieldPosition the current field position
* @return StringBuffer the formatted String
* @since JavaMail 1.2
*/
@Override
public StringBuffer format(Date date, StringBuffer dateStrBuf,
FieldPosition fieldPosition) {
return super.format(date, dateStrBuf, fieldPosition);
}
Parses the given date in the format specified by
RFC 2822.
- With strict parsing, obs-* tokens are unsupported. Lenient parsing
supports obs-year and obs-zone, with the exception of the 1-character
military time zones.
- The optional CFWS token at the end is not parsed.
- RFC 2822 specifies that a zone of "-0000" indicates that the
date-time contains no information about the local time zone. This class
uses the UTC time zone in this case.
Params: - text – the formatted date to be parsed
- pos – the current parse position
Returns: Date the parsed date. In case of error, returns null. Since: JavaMail 1.2
/**
* Parses the given date in the format specified by
* RFC 2822.
* <ul>
* <li>With strict parsing, obs-* tokens are unsupported. Lenient parsing
* supports obs-year and obs-zone, with the exception of the 1-character
* military time zones.
* <li>The optional CFWS token at the end is not parsed.
* <li>RFC 2822 specifies that a zone of "-0000" indicates that the
* date-time contains no information about the local time zone. This class
* uses the UTC time zone in this case.
* </ul>
*
* @param text the formatted date to be parsed
* @param pos the current parse position
* @return Date the parsed date. In case of error, returns null.
* @since JavaMail 1.2
*/
@Override
public Date parse(String text, ParsePosition pos) {
if (text == null || pos == null) {
throw new NullPointerException();
} else if (0 > pos.getIndex() || pos.getIndex() >= text.length()) {
return null;
}
return isLenient()
? new Rfc2822LenientParser(text, pos).parse()
: new Rfc2822StrictParser(text, pos).parse();
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates a specific calendar.
Throws: - UnsupportedOperationException – if this method is invoked
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates a specific calendar.
*
* @throws UnsupportedOperationException if this method is invoked
*/
@Override
public void setCalendar(Calendar newCalendar) {
throw new UnsupportedOperationException("Method "
+ "setCalendar() shouldn't be called");
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates a specific number format.
Throws: - UnsupportedOperationException – if this method is invoked
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates a specific number format.
*
* @throws UnsupportedOperationException if this method is invoked
*/
@Override
public void setNumberFormat(NumberFormat newNumberFormat) {
throw new UnsupportedOperationException("Method "
+ "setNumberFormat() shouldn't be called");
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates a specific pattern.
Throws: - UnsupportedOperationException – if this method is invoked
Since: JavaMail 1.6
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates a specific pattern.
*
* @throws UnsupportedOperationException if this method is invoked
* @since JavaMail 1.6
*/
@Override
public void applyLocalizedPattern(String pattern) {
throw new UnsupportedOperationException("Method "
+ "applyLocalizedPattern() shouldn't be called");
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates a specific pattern.
Throws: - UnsupportedOperationException – if this method is invoked
Since: JavaMail 1.6
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates a specific pattern.
*
* @throws UnsupportedOperationException if this method is invoked
* @since JavaMail 1.6
*/
@Override
public void applyPattern(String pattern) {
throw new UnsupportedOperationException("Method "
+ "applyPattern() shouldn't be called");
}
This method allows serialization to change the pattern.
/**
* This method allows serialization to change the pattern.
*/
private void superApplyPattern(String pattern) {
super.applyPattern(pattern);
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates another strategy for interpreting
2-digits years.
Throws: - UnsupportedOperationException – if this method is invoked
Returns: the start of the 100-year period into which two digit years are
parsed Since: JavaMail 1.6
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates another strategy for interpreting
* 2-digits years.
*
* @return the start of the 100-year period into which two digit years are
* parsed
* @throws UnsupportedOperationException if this method is invoked
* @since JavaMail 1.6
*/
@Override
public Date get2DigitYearStart() {
throw new UnsupportedOperationException("Method "
+ "get2DigitYearStart() shouldn't be called");
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates another strategy for interpreting
2-digits years.
Throws: - UnsupportedOperationException – if this method is invoked
Since: JavaMail 1.6
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates another strategy for interpreting
* 2-digits years.
*
* @throws UnsupportedOperationException if this method is invoked
* @since JavaMail 1.6
*/
@Override
public void set2DigitYearStart(Date startDate) {
throw new UnsupportedOperationException("Method "
+ "set2DigitYearStart() shouldn't be called");
}
This method always throws an UnsupportedOperationException and should not
be used because RFC 2822 mandates specific date format symbols.
Throws: - UnsupportedOperationException – if this method is invoked
Since: JavaMail 1.6
/**
* This method always throws an UnsupportedOperationException and should not
* be used because RFC 2822 mandates specific date format symbols.
*
* @throws UnsupportedOperationException if this method is invoked
* @since JavaMail 1.6
*/
@Override
public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
throw new UnsupportedOperationException("Method "
+ "setDateFormatSymbols() shouldn't be called");
}
Returns the date, as specified by the parameters.
Params: - dayName –
- day –
- month –
- year –
- hour –
- minute –
- second –
- zone –
Throws: - IllegalArgumentException – if this instance's Calendar is
non-lenient and any of the parameters have invalid values, or if dayName
is not consistent with day-month-year
Returns: the date, as specified by the parameters
/**
* Returns the date, as specified by the parameters.
*
* @param dayName
* @param day
* @param month
* @param year
* @param hour
* @param minute
* @param second
* @param zone
* @return the date, as specified by the parameters
* @throws IllegalArgumentException if this instance's Calendar is
* non-lenient and any of the parameters have invalid values, or if dayName
* is not consistent with day-month-year
*/
private Date toDate(int dayName, int day, int month, int year,
int hour, int minute, int second, int zone) {
if (second == LEAP_SECOND) {
second = 59;
}
TimeZone tz = calendar.getTimeZone();
try {
calendar.setTimeZone(UTC);
calendar.clear();
calendar.set(year, month, day, hour, minute, second);
if (dayName == UNKNOWN_DAY_NAME
|| dayName == calendar.get(Calendar.DAY_OF_WEEK)) {
calendar.add(Calendar.MINUTE, zone);
return calendar.getTime();
} else {
throw new IllegalArgumentException("Inconsistent day-name");
}
} finally {
calendar.setTimeZone(tz);
}
}
This class provides the building blocks for date parsing.
It has the following invariants:
- no exceptions are thrown, except for java.text.ParseException from
parse* methods
- when parse* throws ParseException OR get* returns INVALID_CHAR OR
skip* returns false OR peek* is invoked, then pos.getIndex() on method
exit is the same as it was on method entry
/**
* This class provides the building blocks for date parsing.
* <p>
* It has the following invariants:
* <ul>
* <li>no exceptions are thrown, except for java.text.ParseException from
* parse* methods
* <li>when parse* throws ParseException OR get* returns INVALID_CHAR OR
* skip* returns false OR peek* is invoked, then pos.getIndex() on method
* exit is the same as it was on method entry
* </ul>
*/
private static abstract class AbstractDateParser {
static final int INVALID_CHAR = -1;
static final int MAX_YEAR_DIGITS = 8; // guarantees that:
// year < new GregorianCalendar().getMaximum(Calendar.YEAR)
final String text;
final ParsePosition pos;
AbstractDateParser(String text, ParsePosition pos) {
this.text = text;
this.pos = pos;
}
final Date parse() {
int startPosition = pos.getIndex();
try {
return tryParse();
} catch (Exception e) { // == ParseException | RuntimeException e
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Bad date: '" + text + "'", e);
}
pos.setErrorIndex(pos.getIndex());
pos.setIndex(startPosition);
return null;
}
}
abstract Date tryParse() throws ParseException;
Returns: the java.util.Calendar constant for the parsed day name
/**
* @return the java.util.Calendar constant for the parsed day name
*/
final int parseDayName() throws ParseException {
switch (getChar()) {
case 'S':
if (skipPair('u', 'n')) {
return Calendar.SUNDAY;
} else if (skipPair('a', 't')) {
return Calendar.SATURDAY;
}
break;
case 'T':
if (skipPair('u', 'e')) {
return Calendar.TUESDAY;
} else if (skipPair('h', 'u')) {
return Calendar.THURSDAY;
}
break;
case 'M':
if (skipPair('o', 'n')) {
return Calendar.MONDAY;
}
break;
case 'W':
if (skipPair('e', 'd')) {
return Calendar.WEDNESDAY;
}
break;
case 'F':
if (skipPair('r', 'i')) {
return Calendar.FRIDAY;
}
break;
case INVALID_CHAR:
throw new ParseException("Invalid day-name",
pos.getIndex());
}
pos.setIndex(pos.getIndex() - 1);
throw new ParseException("Invalid day-name", pos.getIndex());
}
Returns: the java.util.Calendar constant for the parsed month name
/**
* @return the java.util.Calendar constant for the parsed month name
*/
@SuppressWarnings("fallthrough")
final int parseMonthName(boolean caseSensitive) throws ParseException {
switch (getChar()) {
case 'j':
if (caseSensitive) {
break;
}
case 'J':
if (skipChar('u') || (!caseSensitive && skipChar('U'))) {
if (skipChar('l') || (!caseSensitive
&& skipChar('L'))) {
return Calendar.JULY;
} else if (skipChar('n') || (!caseSensitive
&& skipChar('N'))) {
return Calendar.JUNE;
} else {
pos.setIndex(pos.getIndex() - 1);
}
} else if (skipPair('a', 'n') || (!caseSensitive
&& skipAlternativePair('a', 'A', 'n', 'N'))) {
return Calendar.JANUARY;
}
break;
case 'm':
if (caseSensitive) {
break;
}
case 'M':
if (skipChar('a') || (!caseSensitive && skipChar('A'))) {
if (skipChar('r') || (!caseSensitive
&& skipChar('R'))) {
return Calendar.MARCH;
} else if (skipChar('y') || (!caseSensitive
&& skipChar('Y'))) {
return Calendar.MAY;
} else {
pos.setIndex(pos.getIndex() - 1);
}
}
break;
case 'a':
if (caseSensitive) {
break;
}
case 'A':
if (skipPair('u', 'g') || (!caseSensitive
&& skipAlternativePair('u', 'U', 'g', 'G'))) {
return Calendar.AUGUST;
} else if (skipPair('p', 'r') || (!caseSensitive
&& skipAlternativePair('p', 'P', 'r', 'R'))) {
return Calendar.APRIL;
}
break;
case 'd':
if (caseSensitive) {
break;
}
case 'D':
if (skipPair('e', 'c') || (!caseSensitive
&& skipAlternativePair('e', 'E', 'c', 'C'))) {
return Calendar.DECEMBER;
}
break;
case 'o':
if (caseSensitive) {
break;
}
case 'O':
if (skipPair('c', 't') || (!caseSensitive
&& skipAlternativePair('c', 'C', 't', 'T'))) {
return Calendar.OCTOBER;
}
break;
case 's':
if (caseSensitive) {
break;
}
case 'S':
if (skipPair('e', 'p') || (!caseSensitive
&& skipAlternativePair('e', 'E', 'p', 'P'))) {
return Calendar.SEPTEMBER;
}
break;
case 'n':
if (caseSensitive) {
break;
}
case 'N':
if (skipPair('o', 'v') || (!caseSensitive
&& skipAlternativePair('o', 'O', 'v', 'V'))) {
return Calendar.NOVEMBER;
}
break;
case 'f':
if (caseSensitive) {
break;
}
case 'F':
if (skipPair('e', 'b') || (!caseSensitive
&& skipAlternativePair('e', 'E', 'b', 'B'))) {
return Calendar.FEBRUARY;
}
break;
case INVALID_CHAR:
throw new ParseException("Invalid month", pos.getIndex());
}
pos.setIndex(pos.getIndex() - 1);
throw new ParseException("Invalid month", pos.getIndex());
}
Returns: the number of minutes to be added to the time in the local
time zone, in order to obtain the equivalent time in the UTC time
zone. Returns 0 if the date-time contains no information about the
local time zone.
/**
* @return the number of minutes to be added to the time in the local
* time zone, in order to obtain the equivalent time in the UTC time
* zone. Returns 0 if the date-time contains no information about the
* local time zone.
*/
final int parseZoneOffset() throws ParseException {
int sign = getChar();
if (sign == '+' || sign == '-') {
int offset = parseAsciiDigits(4, 4, true);
if (!isValidZoneOffset(offset)) {
pos.setIndex(pos.getIndex() - 5);
throw new ParseException("Invalid zone", pos.getIndex());
}
return ((sign == '+') ? -1 : 1)
* (offset / 100 * 60 + offset % 100);
} else if (sign != INVALID_CHAR) {
pos.setIndex(pos.getIndex() - 1);
}
throw new ParseException("Invalid zone", pos.getIndex());
}
boolean isValidZoneOffset(int offset) {
return (offset % 100) < 60;
}
final int parseAsciiDigits(int count) throws ParseException {
return parseAsciiDigits(count, count);
}
final int parseAsciiDigits(int min, int max) throws ParseException {
return parseAsciiDigits(min, max, false);
}
final int parseAsciiDigits(int min, int max, boolean isEOF)
throws ParseException {
int result = 0;
int nbDigitsParsed = 0;
while (nbDigitsParsed < max && peekAsciiDigit()) {
result = result * 10 + getAsciiDigit();
nbDigitsParsed++;
}
if ((nbDigitsParsed < min)
|| (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) {
pos.setIndex(pos.getIndex() - nbDigitsParsed);
} else {
return result;
}
String range = (min == max)
? Integer.toString(min)
: "between " + min + " and " + max;
throw new ParseException("Invalid input: expected "
+ range + " ASCII digits", pos.getIndex());
}
final void parseFoldingWhiteSpace() throws ParseException {
if (!skipFoldingWhiteSpace()) {
throw new ParseException("Invalid input: expected FWS",
pos.getIndex());
}
}
final void parseChar(char ch) throws ParseException {
if (!skipChar(ch)) {
throw new ParseException("Invalid input: expected '" + ch + "'",
pos.getIndex());
}
}
final int getAsciiDigit() {
int ch = getChar();
if ('0' <= ch && ch <= '9') {
return Character.digit((char) ch, 10);
} else {
if (ch != INVALID_CHAR) {
pos.setIndex(pos.getIndex() - 1);
}
return INVALID_CHAR;
}
}
final int getChar() {
if (pos.getIndex() < text.length()) {
char ch = text.charAt(pos.getIndex());
pos.setIndex(pos.getIndex() + 1);
return ch;
} else {
return INVALID_CHAR;
}
}
boolean skipFoldingWhiteSpace() {
// fast paths: a single ASCII space or no FWS
if (skipChar(' ')) {
if (!peekFoldingWhiteSpace()) {
return true;
} else {
pos.setIndex(pos.getIndex() - 1);
}
} else if (!peekFoldingWhiteSpace()) {
return false;
}
// normal path
int startIndex = pos.getIndex();
if (skipWhiteSpace()) {
while (skipNewline()) {
if (!skipWhiteSpace()) {
pos.setIndex(startIndex);
return false;
}
}
return true;
} else if (skipNewline() && skipWhiteSpace()) {
return true;
} else {
pos.setIndex(startIndex);
return false;
}
}
final boolean skipWhiteSpace() {
int startIndex = pos.getIndex();
while (skipAlternative(' ', '\t')) { /* empty */ }
return pos.getIndex() > startIndex;
}
final boolean skipNewline() {
return skipPair('\r', '\n');
}
final boolean skipAlternativeTriple(
char firstStandard, char firstAlternative,
char secondStandard, char secondAlternative,
char thirdStandard, char thirdAlternative
) {
if (skipAlternativePair(firstStandard, firstAlternative,
secondStandard, secondAlternative)) {
if (skipAlternative(thirdStandard, thirdAlternative)) {
return true;
} else {
pos.setIndex(pos.getIndex() - 2);
}
}
return false;
}
final boolean skipAlternativePair(
char firstStandard, char firstAlternative,
char secondStandard, char secondAlternative
) {
if (skipAlternative(firstStandard, firstAlternative)) {
if (skipAlternative(secondStandard, secondAlternative)) {
return true;
} else {
pos.setIndex(pos.getIndex() - 1);
}
}
return false;
}
final boolean skipAlternative(char standard, char alternative) {
return skipChar(standard) || skipChar(alternative);
}
final boolean skipPair(char first, char second) {
if (skipChar(first)) {
if (skipChar(second)) {
return true;
} else {
pos.setIndex(pos.getIndex() - 1);
}
}
return false;
}
final boolean skipChar(char ch) {
if (pos.getIndex() < text.length()
&& text.charAt(pos.getIndex()) == ch) {
pos.setIndex(pos.getIndex() + 1);
return true;
} else {
return false;
}
}
final boolean peekAsciiDigit() {
return (pos.getIndex() < text.length()
&& '0' <= text.charAt(pos.getIndex())
&& text.charAt(pos.getIndex()) <= '9');
}
boolean peekFoldingWhiteSpace() {
return (pos.getIndex() < text.length()
&& (text.charAt(pos.getIndex()) == ' '
|| text.charAt(pos.getIndex()) == '\t'
|| text.charAt(pos.getIndex()) == '\r'));
}
final boolean peekChar(char ch) {
return (pos.getIndex() < text.length()
&& text.charAt(pos.getIndex()) == ch);
}
}
private class Rfc2822StrictParser extends AbstractDateParser {
Rfc2822StrictParser(String text, ParsePosition pos) {
super(text, pos);
}
@Override
Date tryParse() throws ParseException {
int dayName = parseOptionalBegin();
int day = parseDay();
int month = parseMonth();
int year = parseYear();
parseFoldingWhiteSpace();
int hour = parseHour();
parseChar(':');
int minute = parseMinute();
int second = (skipChar(':')) ? parseSecond() : 0;
parseFwsBetweenTimeOfDayAndZone();
int zone = parseZone();
try {
return MailDateFormat.this.toDate(dayName, day, month, year,
hour, minute, second, zone);
} catch (IllegalArgumentException e) {
throw new ParseException("Invalid input: some of the calendar "
+ "fields have invalid values, or day-name is "
+ "inconsistent with date", pos.getIndex());
}
}
Returns: the java.util.Calendar constant for the parsed day name, or
UNKNOWN_DAY_NAME iff the begin is missing
/**
* @return the java.util.Calendar constant for the parsed day name, or
* UNKNOWN_DAY_NAME iff the begin is missing
*/
int parseOptionalBegin() throws ParseException {
int dayName;
if (!peekAsciiDigit()) {
skipFoldingWhiteSpace();
dayName = parseDayName();
parseChar(',');
} else {
dayName = UNKNOWN_DAY_NAME;
}
return dayName;
}
int parseDay() throws ParseException {
skipFoldingWhiteSpace();
return parseAsciiDigits(1, 2);
}
Returns: the java.util.Calendar constant for the parsed month name
/**
* @return the java.util.Calendar constant for the parsed month name
*/
int parseMonth() throws ParseException {
parseFwsInMonth();
int month = parseMonthName(isMonthNameCaseSensitive());
parseFwsInMonth();
return month;
}
void parseFwsInMonth() throws ParseException {
parseFoldingWhiteSpace();
}
boolean isMonthNameCaseSensitive() {
return true;
}
int parseYear() throws ParseException {
int year = parseAsciiDigits(4, MAX_YEAR_DIGITS);
if (year >= 1900) {
return year;
} else {
pos.setIndex(pos.getIndex() - 4);
while (text.charAt(pos.getIndex() - 1) == '0') {
pos.setIndex(pos.getIndex() - 1);
}
throw new ParseException("Invalid year", pos.getIndex());
}
}
int parseHour() throws ParseException {
return parseAsciiDigits(2);
}
int parseMinute() throws ParseException {
return parseAsciiDigits(2);
}
int parseSecond() throws ParseException {
return parseAsciiDigits(2);
}
void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
parseFoldingWhiteSpace();
}
int parseZone() throws ParseException {
return parseZoneOffset();
}
}
private class Rfc2822LenientParser extends Rfc2822StrictParser {
private Boolean hasDefaultFws;
Rfc2822LenientParser(String text, ParsePosition pos) {
super(text, pos);
}
@Override
int parseOptionalBegin() {
while (pos.getIndex() < text.length() && !peekAsciiDigit()) {
pos.setIndex(pos.getIndex() + 1);
}
return UNKNOWN_DAY_NAME;
}
@Override
int parseDay() throws ParseException {
skipFoldingWhiteSpace();
return parseAsciiDigits(1, 3);
}
@Override
void parseFwsInMonth() throws ParseException {
// '-' is allowed to accomodate for the date format as specified in
// <a href="http://www.ietf.org/rfc/rfc3501.txt">RFC 3501</a>
if (hasDefaultFws == null) {
hasDefaultFws = !skipChar('-');
skipFoldingWhiteSpace();
} else if (hasDefaultFws) {
skipFoldingWhiteSpace();
} else {
parseChar('-');
}
}
@Override
boolean isMonthNameCaseSensitive() {
return false;
}
@Override
int parseYear() throws ParseException {
int year = parseAsciiDigits(1, MAX_YEAR_DIGITS);
if (year >= 1000) {
return year;
} else if (year >= 50) {
return year + 1900;
} else {
return year + 2000;
}
}
@Override
int parseHour() throws ParseException {
return parseAsciiDigits(1, 2);
}
@Override
int parseMinute() throws ParseException {
return parseAsciiDigits(1, 2);
}
@Override
int parseSecond() throws ParseException {
return parseAsciiDigits(1, 2);
}
@Override
void parseFwsBetweenTimeOfDayAndZone() throws ParseException {
skipFoldingWhiteSpace();
}
@Override
int parseZone() throws ParseException {
try {
if (pos.getIndex() >= text.length()) {
throw new ParseException("Missing zone", pos.getIndex());
}
if (peekChar('+') || peekChar('-')) {
return parseZoneOffset();
} else if (skipAlternativePair('U', 'u', 'T', 't')) {
return 0;
} else if (skipAlternativeTriple('G', 'g', 'M', 'm',
'T', 't')) {
return 0;
} else {
int hoursOffset;
if (skipAlternative('E', 'e')) {
hoursOffset = 4;
} else if (skipAlternative('C', 'c')) {
hoursOffset = 5;
} else if (skipAlternative('M', 'm')) {
hoursOffset = 6;
} else if (skipAlternative('P', 'p')) {
hoursOffset = 7;
} else {
throw new ParseException("Invalid zone",
pos.getIndex());
}
if (skipAlternativePair('S', 's', 'T', 't')) {
hoursOffset += 1;
} else if (skipAlternativePair('D', 'd', 'T', 't')) {
} else {
pos.setIndex(pos.getIndex() - 1);
throw new ParseException("Invalid zone",
pos.getIndex());
}
return hoursOffset * 60;
}
} catch (ParseException e) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "No timezone? : '" + text + "'", e);
}
return 0;
}
}
@Override
boolean isValidZoneOffset(int offset) {
return true;
}
@Override
boolean skipFoldingWhiteSpace() {
boolean result = peekFoldingWhiteSpace();
skipLoop:
while (pos.getIndex() < text.length()) {
switch (text.charAt(pos.getIndex())) {
case ' ':
case '\t':
case '\r':
case '\n':
pos.setIndex(pos.getIndex() + 1);
break;
default:
break skipLoop;
}
}
return result;
}
@Override
boolean peekFoldingWhiteSpace() {
return super.peekFoldingWhiteSpace()
|| (pos.getIndex() < text.length()
&& text.charAt(pos.getIndex()) == '\n');
}
}
}