| /* |
| * 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; |
| } |
| } |
| } |
| } |