javax.mail.internet.MailDateFormat Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2016 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://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/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 packager/legal/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
* @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.
*/
public MailDateFormat() {
super(PATTERN, Locale.US);
}
/**
* 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.
*
* @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.
*
* @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.
*
* @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.
*
*
* @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
*/
@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
*/
@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
*/
@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
*/
@Override
public void applyPattern(String pattern) {
throw new UnsupportedOperationException("Method "
+ "applyPattern() shouldn't be called");
}
/**
* 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.
*
* @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
*/
@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
*/
@Override
public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
throw new UnsupportedOperationException("Method "
+ "setDateFormatSymbols() shouldn't be called");
}
/**
* 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
*
*/
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;
/**
* @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());
}
/**
* @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());
}
/**
* @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());
}
}
/**
* @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);
}
/**
* @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
// RFC 3501
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');
}
}
}