javax.mail.internet.MailDateFormat Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 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.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.text.DateFormat;
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 take pattern strings. 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.
*
* @author Max Spivak
* @since JavaMail 1.2
*/
public class MailDateFormat extends SimpleDateFormat {
private static final long serialVersionUID = -8148227605210628779L;
public MailDateFormat() {
super("EEE, d MMM yyyy HH:mm:ss 'XXXXX' (z)", Locale.US);
}
/**
* 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
*/
public StringBuffer format(Date date, StringBuffer dateStrBuf,
FieldPosition fieldPosition) {
/* How this method works: First format the date with the
* format specified in the constructor inserting string 'XXXXX'
* where the timezone offset goes. Find where in the string the
* string 'XXXXX' appears and remember that in var "pos".
* Calculate the offset, taking the DST into account and insert
* it into the stringbuffer at position pos.
*/
int start = dateStrBuf.length();
super.format(date, dateStrBuf, fieldPosition);
int pos = 0;
// find the beginning of the 'XXXXX' string in the formatted date
// 25 is the first position that we expect to find XXXXX at
for (pos = start + 25; dateStrBuf.charAt(pos) != 'X'; pos++)
;
// set the timezone to +HHMM or -HHMM
calendar.clear();
calendar.setTime(date);
int offset = calendar.get(Calendar.ZONE_OFFSET) +
calendar.get(Calendar.DST_OFFSET);
// take care of the sign
if (offset < 0) {
dateStrBuf.setCharAt(pos++, '-');
offset = (-offset);
} else
dateStrBuf.setCharAt(pos++, '+');
int rawOffsetInMins = offset / 60 / 1000; // offset from GMT in mins
int offsetInHrs = rawOffsetInMins / 60;
int offsetInMins = rawOffsetInMins % 60;
dateStrBuf.setCharAt(pos++, Character.forDigit((offsetInHrs/10), 10));
dateStrBuf.setCharAt(pos++, Character.forDigit((offsetInHrs%10), 10));
dateStrBuf.setCharAt(pos++, Character.forDigit((offsetInMins/10), 10));
dateStrBuf.setCharAt(pos++, Character.forDigit((offsetInMins%10), 10));
// done with timezone
return dateStrBuf;
}
////////////////////////////////////////////////////////////
/**
* Parses the given date in the format specified by
* RFC 2822 in the current TimeZone.
*
* @param text the formatted date to be parsed
* @param pos the current parse position
* @return Date the parsed date in a Date object
* @since JavaMail 1.2
*/
public Date parse(String text, ParsePosition pos) {
return parseDate(text.toCharArray(), pos, isLenient());
}
/*
Valid Examples:
Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)
Date:
Date: Mon, 22 Mar 1993 09:41:09 -0800 (PST)
Date: 26 Aug 76 14:29 EDT
*/
/**
* method of what to look for:
*
*
* skip WS
* skip day "," (this is "Mon", "Tue")
* skip WS
*
* parse number (until WS) ==> 1*2DIGIT (day of month)
*
* skip WS
*
* parse alpha chars (until WS) ==> find month
*
* skip WS
*
* parse number (until WS) ==> 2*4DIGIT (year)
*
* skip WS
*
* // now looking for time
* parse number (until ':') ==> hours
* parse number (until ':') ==> minutes
* parse number (until WS) ==> seconds
*
* // now look for Time Zone
* skip WS
* if ('+' or '-') then numerical time zone offset
* if (alpha) then alpha time zone offset
*/
static boolean debug = false;
private static MailLogger logger = new MailLogger(
MailDateFormat.class,
"DEBUG",
debug,
System.out);
/**
* create a Date by parsing the char array
*/
static private Date parseDate(char[] orig, ParsePosition pos,
boolean lenient) {
try {
int day = -1;
int month = -1;
int year = -1;
int hours = 0;
int minutes = 0;
int seconds = 0;
int offset = 0;
MailDateParser p = new MailDateParser(orig, pos.getIndex());
// get the day
p.skipUntilNumber();
day = p.parseNumber();
if (!p.skipIfChar('-')) { // for IMAP internal Date
p.skipWhiteSpace();
}
// get the month
month = p.parseMonth();
if (!p.skipIfChar('-')) { // for IMAP internal Date
p.skipWhiteSpace();
}
// get the year
year = p.parseNumber(); // should not return a negative number
if (year < 50) {
year += 2000;
} else if (year < 100) {
year += 1900;
} // otherwise the year is correct (and should be 4 digits)
// get the time
// first get hours
p.skipWhiteSpace();
hours = p.parseNumber();
// get minutes
p.skipChar(':');
minutes = p.parseNumber();
// get seconds (may be no seconds)
if (p.skipIfChar(':')) {
seconds = p.parseNumber();
}
// try to get a Time Zone
try {
p.skipWhiteSpace();
offset = p.parseTimeZone();
} catch (ParseException pe) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"No timezone? : '" + new String(orig) + "'", pe);
}
}
pos.setIndex(p.getIndex());
Date d = ourUTC(year, month, day, hours, minutes, seconds, offset,
lenient);
return d;
} catch (Exception e) {
// Catch *all* exceptions, including RuntimeExceptions like
// ArrayIndexOutofBoundsException ... we need to be
// extra tolerant of all those bogus dates that might screw
// up our parser. Sigh.
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"Bad date: '" + new String(orig) + "'", e);
}
pos.setIndex(1); // to prevent DateFormat.parse() from throwing ex
return null;
}
}
private static final Calendar cal =
new GregorianCalendar(TimeZone.getTimeZone("GMT"));
private synchronized static Date ourUTC(int year, int mon, int mday,
int hour, int min, int sec,
int tzoffset, boolean lenient) {
// clear the time and then set all the values
cal.clear();
cal.setLenient(lenient);
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, mon);
cal.set(Calendar.DATE, mday);
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, min);
cal.add(Calendar.MINUTE, tzoffset); // adjust for the timezone
cal.set(Calendar.SECOND, sec);
return cal.getTime();
}
////////////////////////////////////////////////////////////
/** Don't allow setting the calendar */
public void setCalendar(Calendar newCalendar) {
throw new RuntimeException("Method setCalendar() shouldn't be called");
}
/** Don't allow setting the NumberFormat */
public void setNumberFormat(NumberFormat newNumberFormat) {
throw new RuntimeException("Method setNumberFormat() shouldn't be called");
}
/* test code for MailDateFormat */
/*
public static void main(String[] args) {
DateFormat df = new MailDateFormat();
Date d = new Date();
// test output in all the timezones
System.out.println("------- test all timezones ---------------");
System.out.println("Current date: " + d);
String[] allIDs = TimeZone.getAvailableIDs();
for (int i = 0; i < allIDs.length; i++) {
TimeZone tz = TimeZone.getTimeZone(allIDs[i]);
df.setTimeZone(tz);
System.out.println("Date in " + tz.getID() + ": " +
df.format(new Date()));
}
try {
System.out.println(df.parse("Sun, 21 Mar 1993 23:56:48 -0800 (PST)"));
System.out.println(df.parse("Mon, 22 Mar 1994 13:34:51 +0000"));
System.out.println(df.parse("26 Aug 76 14:29 EDT"));
System.out.println(df.parse("15 Apr 11 23:49 EST"));
System.out.println(df.parse("15 Apr 11 23:49 ABC"));
} catch (ParseException pex) {
pex.printStackTrace();
}
// reset DateFormat TZ
df.setTimeZone(TimeZone.getDefault());
// test all days in a month
System.out.println();
System.out.println("------- test all days in a month ---------------");
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 1972);
cal.set(Calendar.MONTH, Calendar.OCTOBER);
cal.set(Calendar.DATE, 1);
cal.set(Calendar.HOUR, 10);
cal.set(Calendar.MINUTE, 50);
cal.set(Calendar.AM_PM, Calendar.PM);
System.out.println("Initial Date: " + cal.getTime());
System.out.println("Current Date: " + df.format(cal.getTime()));
for (int i = 0; i < 30; i++) {
cal.roll(Calendar.DATE, true);
System.out.println("Current Date: " + df.format(cal.getTime()));
}
// test all months
System.out.println();
System.out.println("------- test all months in a year -----------");
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DATE, 7);
System.out.println("Initial Date: " + cal.getTime());
System.out.println("Current Date: " + df.format(cal.getTime()));
for (int i = 1; i < 12; i++) {
cal.roll(Calendar.MONTH, true);
System.out.println("Current Date: " + df.format(cal.getTime()));
}
// test leap years
System.out.println();
System.out.println("------- test leap years -----------");
cal.set(Calendar.YEAR, 1999);
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DATE, 31);
cal.roll(Calendar.MONTH, true);
System.out.println("Initial Date: " + cal.getTime());
System.out.println("Current Date: " + df.format(cal.getTime()));
for (int i = 1; i < 12; i++) {
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DATE, 31);
cal.roll(Calendar.YEAR, true);
cal.roll(Calendar.MONTH, true);
System.out.println("Current Date: " + df.format(cal.getTime()));
}
}
*/
}
/**
* Helper class to deal with parsing the characters
*/
class MailDateParser {
int index = 0;
char[] orig = null;
public MailDateParser(char[] orig, int index) {
this.orig = orig;
this.index = index;
}
/**
* skips chars until it finds a number (0-9)
*
* if it does not find a number, it will throw
* an ArrayIndexOutOfBoundsException
*/
public void skipUntilNumber() throws ParseException {
try {
while (true) {
switch ( orig[index] ) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return;
default:
index++;
break;
}
}
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParseException("No Number Found", index);
}
}
/**
* skips any number of tabs, spaces, CR, and LF - folding whitespace
*/
public void skipWhiteSpace() {
int len = orig.length;
while (index < len) {
switch (orig[index]) {
case ' ': // space
case '\t': // tab
case '\r': // CR
case '\n': // LF
index++;
break;
default:
return;
}
}
}
/**
* used to look at the next character without "parsing" that
* character.
*/
public int peekChar() throws ParseException {
if (index < orig.length)
return orig[index];
else
throw new ParseException("No more characters", index);
}
/**
* skips the given character. if the current char does not
* match a ParseException will be thrown
*/
public void skipChar(char c) throws ParseException {
if (index < orig.length) {
if (orig[index] == c) {
index++;
} else {
throw new ParseException("Wrong char", index);
}
} else {
throw new ParseException("No more characters", index);
}
}
/**
* will only skip the current char if it matches the given
* char
*/
public boolean skipIfChar(char c) throws ParseException {
if (index < orig.length) {
if (orig[index] == c) {
index++;
return true;
} else {
return false;
}
} else {
throw new ParseException("No more characters", index);
}
}
/**
* current char must point to a number. the number will be
* parsed and the resulting number will be returned. if a
* number is not found, a ParseException will be thrown
*/
public int parseNumber() throws ParseException {
int length = orig.length;
boolean gotNum = false;
int result = 0;
while (index < length) {
switch( orig[index] ) {
case '0':
result *= 10;
gotNum = true;
break;
case '1':
result = result * 10 + 1;
gotNum = true;
break;
case '2':
result = result * 10 + 2;
gotNum = true;
break;
case '3':
result = result * 10 + 3;
gotNum = true;
break;
case '4':
result = result * 10 + 4;
gotNum = true;
break;
case '5':
result = result * 10 + 5;
gotNum = true;
break;
case '6':
result = result * 10 + 6;
gotNum = true;
break;
case '7':
result = result * 10 + 7;
gotNum = true;
break;
case '8':
result = result * 10 + 8;
gotNum = true;
break;
case '9':
result = result * 10 + 9;
gotNum = true;
break;
default:
if (gotNum)
return result;
else
throw new ParseException("No Number found", index);
}
index++;
}
// check the result
if (gotNum)
return result;
// else, throw a parse error
throw new ParseException("No Number found", index);
}
/**
* will look for one of "Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dev"
* and return the numerical version of the month. (0-11). a ParseException
* error is thrown if a month cannot be found.
*/
public int parseMonth() throws ParseException {
char curr;
try {
switch(orig[index++]) {
case 'J':
case 'j': // "Jan" (0) / "Jun" (5) / "Jul" (6)
// check next char
switch(orig[index++]) {
case 'A':
case 'a':
curr = orig[index++];
if (curr == 'N' || curr == 'n') {
return 0;
}
break;
case 'U':
case 'u':
curr = orig[index++];
if (curr == 'N' || curr == 'n') {
return 5;
} else if (curr == 'L' || curr == 'l') {
return 6;
}
break;
}
break;
case 'F':
case 'f': // "Feb"
curr = orig[index++];
if (curr == 'E' || curr == 'e') {
curr = orig[index++];
if (curr == 'B' || curr == 'b') {
return 1;
}
}
break;
case 'M':
case 'm': // "Mar" (2) / "May" (4)
curr = orig[index++];
if (curr == 'A' || curr == 'a') {
curr = orig[index++];
if (curr == 'R' || curr == 'r') {
return 2;
} else if (curr == 'Y' || curr == 'y') {
return 4;
}
}
break;
case 'A':
case 'a': // "Apr" (3) / "Aug" (7)
curr = orig[index++];
if (curr == 'P' || curr == 'p') {
curr = orig[index++];
if (curr == 'R' || curr == 'r') {
return 3;
}
} else if (curr == 'U' || curr == 'u') {
curr = orig[index++];
if (curr == 'G' || curr == 'g') {
return 7;
}
}
break;
case 'S':
case 's': // "Sep" (8)
curr = orig[index++];
if (curr == 'E' || curr == 'e') {
curr = orig[index++];
if (curr == 'P' || curr == 'p') {
return 8;
}
}
break;
case 'O':
case 'o': // "Oct"
curr = orig[index++];
if (curr == 'C' || curr == 'c') {
curr = orig[index++];
if (curr == 'T' || curr == 't') {
return 9;
}
}
break;
case 'N':
case 'n': // "Nov"
curr = orig[index++];
if (curr == 'O' || curr == 'o') {
curr = orig[index++];
if (curr == 'V' || curr == 'v') {
return 10;
}
}
break;
case 'D':
case 'd': // "Dec"
curr = orig[index++];
if (curr == 'E' || curr == 'e') {
curr = orig[index++];
if (curr == 'C' || curr == 'c') {
return 11;
}
}
break;
}
} catch (ArrayIndexOutOfBoundsException e) {
}
throw new ParseException("Bad Month", index);
}
/**
* will parse the timezone - either Numerical version (e.g. +0800, -0500)
* or the alpha version (e.g. PDT, PST). the result will be returned in
* minutes needed to be added to the date to bring it to GMT.
*/
public int parseTimeZone() throws ParseException {
if (index >= orig.length)
throw new ParseException("No more characters", index);
char test = orig[index];
if ( test == '+' || test == '-' ) {
return parseNumericTimeZone();
} else {
return parseAlphaTimeZone();
}
}
/**
* will parse the Numerical time zone version (e.g. +0800, -0500)
* the result will be returned in minutes needed to be added
* to the date to bring it to GMT.
*/
public int parseNumericTimeZone() throws ParseException {
// we switch the sign if it is a '+'
// since the time in the string we are
// parsing is off from GMT by that amount.
// and we want to get the time back into
// GMT, so we substract it.
boolean switchSign = false;
char first = orig[index++];
if (first == '+') {
switchSign = true;
} else if (first != '-') {
throw new ParseException("Bad Numeric TimeZone", index);
}
int oindex = index;
int tz = parseNumber();
if (tz >= 2400)
throw new ParseException("Numeric TimeZone out of range", oindex);
int offset = (tz / 100) * 60 + (tz % 100);
if (switchSign) {
return -offset;
} else {
return offset;
}
}
/**
* will parse the alpha time zone version (e.g. PDT, PST).
* the result will be returned in minutes needed to be added
* to the date to bring it to GMT.
*/
public int parseAlphaTimeZone() throws ParseException {
int result = 0;
boolean foundCommon = false;
char curr;
try {
switch(orig[index++]) {
case 'U':
case 'u': // "UT" / Universal Time
curr = orig[index++];
if (curr == 'T' || curr == 't') {
result = 0;
break;
}
throw new ParseException("Bad Alpha TimeZone", index);
case 'G':
case 'g': // "GMT" ; Universal Time
curr = orig[index++];
if (curr == 'M' || curr == 'm') {
curr = orig[index++];
if (curr == 'T' || curr == 't') {
result = 0;
break;
}
}
throw new ParseException("Bad Alpha TimeZone", index);
case 'E':
case 'e': // "EST" / "EDT" ; Eastern: - 5/ - 4
result = 300;
foundCommon = true;
break;
case 'C':
case 'c': // "CST" / "CDT" ; Central: - 6/ - 5
result = 360;
foundCommon = true;
break;
case 'M':
case 'm': // "MST" / "MDT" ; Mountain: - 7/ - 6
result = 420;
foundCommon = true;
break;
case 'P':
case 'p': // "PST" / "PDT" ; Pacific: - 8/ - 7
result = 480;
foundCommon = true;
break;
default:
throw new ParseException("Bad Alpha TimeZone", index);
}
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParseException("Bad Alpha TimeZone", index);
}
if (foundCommon) {
curr = orig[index++];
if (curr == 'S' || curr == 's') {
curr = orig[index++];
if (curr != 'T' && curr != 't') {
throw new ParseException("Bad Alpha TimeZone", index);
}
} else if (curr == 'D' || curr == 'd') {
curr = orig[index++];
if (curr == 'T' || curr != 't') {
// for daylight time
result -= 60;
} else {
throw new ParseException("Bad Alpha TimeZone", index);
}
}
}
return result;
}
int getIndex() {
return index;
}
}