blob: 7c68656679a8d331816274392bb30aebc5267a1d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package javax.mail.internet;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
/**
* Formats ths date as specified by
* draft-ietf-drums-msg-fmt-08 dated January 26, 2000
* which supercedes RFC822.
* <p/>
* <p/>
* The format used is <code>EEE, d MMM yyyy HH:mm:ss Z</code> and
* locale is always US-ASCII.
*
* @version $Rev$ $Date$
*/
public class MailDateFormat extends SimpleDateFormat {
private static final long serialVersionUID = -8148227605210628779L;
public MailDateFormat() {
super("EEE, d MMM yyyy HH:mm:ss Z (z)", Locale.US);
}
@Override
public StringBuffer format(final Date date, final StringBuffer buffer, final FieldPosition position) {
return super.format(date, buffer, position);
}
/**
* Parse a Mail date into a Date object. This uses fairly
* lenient rules for the format because the Mail standards
* for dates accept multiple formats.
*
* @param string The input string.
* @param position The position argument.
*
* @return The Date object with the information inside.
*/
@Override
public Date parse(final String string, final ParsePosition position) {
final MailDateParser parser = new MailDateParser(string, position);
try {
return parser.parse(isLenient());
} catch (final ParseException e) {
e.printStackTrace();
// just return a null for any parsing errors
return null;
}
}
/**
* The calendar cannot be set
* @param calendar
* @throws UnsupportedOperationException
*/
@Override
public void setCalendar(final Calendar calendar) {
throw new UnsupportedOperationException();
}
/**
* The format cannot be set
* @param format
* @throws UnsupportedOperationException
*/
@Override
public void setNumberFormat(final NumberFormat format) {
throw new UnsupportedOperationException();
}
// utility class for handling date parsing issues
class MailDateParser {
// our list of defined whitespace characters
static final String whitespace = " \t\r\n";
// current parsing position
int current;
// our end parsing position
int endOffset;
// the date source string
String source;
// The parsing position. We update this as we move along and
// also for any parsing errors
ParsePosition pos;
public MailDateParser(final String source, final ParsePosition pos)
{
this.source = source;
this.pos = pos;
// we start using the providing parsing index.
this.current = pos.getIndex();
this.endOffset = source.length();
}
/**
* Parse the timestamp, returning a date object.
*
* @param lenient The lenient setting from the Formatter object.
*
* @return A Date object based off of parsing the date string.
* @exception ParseException
*/
public Date parse(final boolean lenient) throws ParseException {
// we just skip over any next date format, which means scanning ahead until we
// find the first numeric character
locateNumeric();
// the day can be either 1 or two digits
final int day = parseNumber(1, 2);
// step over the delimiter
skipDateDelimiter();
// parse off the month (which is in character format)
final int month = parseMonth();
// step over the delimiter
skipDateDelimiter();
// now pull of the year, which can be either 2-digit or 4-digit
final int year = parseYear();
// white space is required here
skipRequiredWhiteSpace();
// accept a 1 or 2 digit hour
final int hour = parseNumber(1, 2);
skipRequiredChar(':');
// the minutes must be two digit
final int minutes = parseNumber(2, 2);
// the seconds are optional, but the ":" tells us if they are to
// be expected.
int seconds = 0;
if (skipOptionalChar(':')) {
seconds = parseNumber(2, 2);
}
// skip over the white space
skipWhiteSpace();
// and finally the timezone information
final int offset = parseTimeZone();
// set the index of how far we've parsed this
pos.setIndex(current);
// create a calendar for creating the date
final Calendar greg = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
// we inherit the leniency rules
greg.setLenient(lenient);
greg.set(year, month, day, hour, minutes, seconds);
// now adjust by the offset. This seems a little strange, but we
// need to negate the offset because this is a UTC calendar, so we need to
// apply the reverse adjustment. for example, for the EST timezone, the offset
// value will be -300 (5 hours). If the time was 15:00:00, the UTC adjusted time
// needs to be 20:00:00, so we subract -300 minutes.
greg.add(Calendar.MINUTE, -offset);
// now return this timestamp.
return greg.getTime();
}
/**
* Skip over a position where there's a required value
* expected.
*
* @param ch The required character.
*
* @exception ParseException
*/
private void skipRequiredChar(final char ch) throws ParseException {
if (current >= endOffset) {
parseError("Delimiter '" + ch + "' expected");
}
if (source.charAt(current) != ch) {
parseError("Delimiter '" + ch + "' expected");
}
current++;
}
/**
* Skip over a position where iff the position matches the
* character
*
* @param ch The required character.
*
* @return true if the character was there, false otherwise.
* @exception ParseException
*/
private boolean skipOptionalChar(final char ch) {
if (current >= endOffset) {
return false;
}
if (source.charAt(current) != ch) {
return false;
}
current++;
return true;
}
/**
* Skip over any white space characters until we find
* the next real bit of information. Will scan completely to the
* end, if necessary.
*/
private void skipWhiteSpace() {
while (current < endOffset) {
// if this is not in the white space list, then success.
if (whitespace.indexOf(source.charAt(current)) < 0) {
return;
}
current++;
}
// everything used up, just return
}
/**
* Skip over any non-white space characters until we find
* either a whitespace char or the end of the data.
*/
private void skipNonWhiteSpace() {
while (current < endOffset) {
// if this is not in the white space list, then success.
if (whitespace.indexOf(source.charAt(current)) >= 0) {
return;
}
current++;
}
// everything used up, just return
}
/**
* Skip over any white space characters until we find
* the next real bit of information. Will scan completely to the
* end, if necessary.
*/
private void skipRequiredWhiteSpace() throws ParseException {
final int start = current;
while (current < endOffset) {
// if this is not in the white space list, then success.
if (whitespace.indexOf(source.charAt(current)) < 0) {
// we must have at least one white space character
if (start == current) {
parseError("White space character expected");
}
return;
}
current++;
}
// everything used up, just return, but make sure we had at least one
// white space
if (start == current) {
parseError("White space character expected");
}
}
private void parseError(final String message) throws ParseException {
// we've got an error, set the index to the end.
pos.setErrorIndex(current);
throw new ParseException(message, current);
}
/**
* Locate an expected numeric field.
*
* @exception ParseException
*/
private void locateNumeric() throws ParseException {
while (current < endOffset) {
// found a digit? we're done
if (Character.isDigit(source.charAt(current))) {
return;
}
current++;
}
// we've got an error, set the index to the end.
parseError("Number field expected");
}
/**
* Parse out an expected numeric field.
*
* @param minDigits The minimum number of digits we expect in this filed.
* @param maxDigits The maximum number of digits expected. Parsing will
* stop at the first non-digit character. An exception will
* be thrown if the field contained more than maxDigits
* in it.
*
* @return The parsed numeric value.
* @exception ParseException
*/
private int parseNumber(final int minDigits, final int maxDigits) throws ParseException {
final int start = current;
int accumulator = 0;
while (current < endOffset) {
final char ch = source.charAt(current);
// if this is not a digit character, then quit
if (!Character.isDigit(ch)) {
break;
}
// add the digit value into the accumulator
accumulator = accumulator * 10 + Character.digit(ch, 10);
current++;
}
final int fieldLength = current - start;
if (fieldLength < minDigits || fieldLength > maxDigits) {
parseError("Invalid number field");
}
return accumulator;
}
/**
* Skip a delimiter between the date portions of the
* string. The IMAP internal date format uses "-", so
* we either accept a single "-" or any number of white
* space characters (at least one required).
*
* @exception ParseException
*/
private void skipDateDelimiter() throws ParseException {
if (current >= endOffset) {
parseError("Invalid date field delimiter");
}
if (source.charAt(current) == '-') {
current++;
}
else {
// must be at least a single whitespace character
skipRequiredWhiteSpace();
}
}
/**
* Parse a character month name into the date month
* offset.
*
* @return
* @exception ParseException
*/
private int parseMonth() throws ParseException {
if ((endOffset - current) < 3) {
parseError("Invalid month");
}
int monthOffset = 0;
final String month = source.substring(current, current + 3).toLowerCase();
if (month.equals("jan")) {
monthOffset = 0;
}
else if (month.equals("feb")) {
monthOffset = 1;
}
else if (month.equals("mar")) {
monthOffset = 2;
}
else if (month.equals("apr")) {
monthOffset = 3;
}
else if (month.equals("may")) {
monthOffset = 4;
}
else if (month.equals("jun")) {
monthOffset = 5;
}
else if (month.equals("jul")) {
monthOffset = 6;
}
else if (month.equals("aug")) {
monthOffset = 7;
}
else if (month.equals("sep")) {
monthOffset = 8;
}
else if (month.equals("oct")) {
monthOffset = 9;
}
else if (month.equals("nov")) {
monthOffset = 10;
}
else if (month.equals("dec")) {
monthOffset = 11;
}
else {
parseError("Invalid month");
}
// ok, this is valid. Update the position and return it
current += 3;
return monthOffset;
}
/**
* Parse off a year field that might be expressed as
* either 2 or 4 digits.
*
* @return The numeric value of the year.
* @exception ParseException
*/
private int parseYear() throws ParseException {
// the year is between 2 to 4 digits
int year = parseNumber(2, 4);
// the two digit years get some sort of adjustment attempted.
if (year < 50) {
year += 2000;
}
else if (year < 100) {
year += 1990;
}
return year;
}
/**
* Parse all of the different timezone options.
*
* @return The timezone offset.
* @exception ParseException
*/
private int parseTimeZone() throws ParseException {
if (current >= endOffset) {
parseError("Missing time zone");
}
// get the first non-blank. If this is a sign character, this
// is a zone offset.
final char sign = source.charAt(current);
if (sign == '-' || sign == '+') {
// need to step over the sign character
current++;
// a numeric timezone is always a 4 digit number, but
// expressed as minutes/seconds. I'm too lazy to write a
// different parser that will bound on just a couple of characters, so
// we'll grab this as a single value and adjust
final int zoneInfo = parseNumber(4, 4);
int offset = (zoneInfo / 100) * 60 + (zoneInfo % 100);
// negate this, if we have a negativeo offset
if (sign == '-') {
offset = -offset;
}
return offset;
}
else {
// need to parse this out using the obsolete zone names. This will be
// either a 3-character code (defined set), or a single character military
// zone designation.
final int start = current;
skipNonWhiteSpace();
final String name = source.substring(start, current).toUpperCase();
if (name.length() == 1) {
return militaryZoneOffset(name);
}
else if (name.length() <= 3) {
return namedZoneOffset(name);
}
else {
parseError("Invalid time zone");
}
return 0;
}
}
/**
* Parse the obsolete mail timezone specifiers. The
* allowed set of timezones are terribly US centric.
* That's the spec. The preferred timezone form is
* the +/-mmss form.
*
* @param name The input name.
*
* @return The standard timezone offset for the specifier.
* @exception ParseException
*/
private int namedZoneOffset(final String name) throws ParseException {
// NOTE: This is "UT", NOT "UTC"
if (name.equals("UT")) {
return 0;
}
else if (name.equals("GMT")) {
return 0;
}
else if (name.equals("EST")) {
return -300;
}
else if (name.equals("EDT")) {
return -240;
}
else if (name.equals("CST")) {
return -360;
}
else if (name.equals("CDT")) {
return -300;
}
else if (name.equals("MST")) {
return -420;
}
else if (name.equals("MDT")) {
return -360;
}
else if (name.equals("PST")) {
return -480;
}
else if (name.equals("PDT")) {
return -420;
}
else {
parseError("Invalid time zone");
return 0;
}
}
/**
* Parse a single-character military timezone.
*
* @param name The one-character name.
*
* @return The offset corresponding to the military designation.
*/
private int militaryZoneOffset(final String name) throws ParseException {
switch (Character.toUpperCase(name.charAt(0))) {
case 'A':
return 60;
case 'B':
return 120;
case 'C':
return 180;
case 'D':
return 240;
case 'E':
return 300;
case 'F':
return 360;
case 'G':
return 420;
case 'H':
return 480;
case 'I':
return 540;
case 'K':
return 600;
case 'L':
return 660;
case 'M':
return 720;
case 'N':
return -60;
case 'O':
return -120;
case 'P':
return -180;
case 'Q':
return -240;
case 'R':
return -300;
case 'S':
return -360;
case 'T':
return -420;
case 'U':
return -480;
case 'V':
return -540;
case 'W':
return -600;
case 'X':
return -660;
case 'Y':
return -720;
case 'Z':
return 0;
default:
parseError("Invalid time zone");
return 0;
}
}
}
}