Java tutorial
/* * 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 * <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. */ 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. * <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 */ @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. * <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; /** * @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 // <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'); } } }