/*
* DateParser.java February 2001
*
* Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net>
*
* Licensed 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 org.simpleframework.http.parse;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.DAY_OF_WEEK;
import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MILLISECOND;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.YEAR;
import java.util.Calendar;
import java.util.TimeZone;
import org.simpleframework.util.parse.Parser;
/**
* This is used to create a <code>Parser</code> for the HTTP date format. This
* will parse the 3 formats that are acceptable for the HTTP/1.1 date. The three
* formats that are acceptable for the HTTP-date are
*
* <pre>
* Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
* Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
* Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
* </pre>
* <p>
* This can also parse the date in ms as retrived from the <code>System</code>'s
* <code>System.currentTimeMillis</code> method. This has a parse method for a
* <code>long</code> which will do the same as the <code>parse(String)</code>.
* Once the date has been parsed there are two methods that allow the date to be
* represented, the <code>toLong</code> method converts the date to a
* <code>long</code> and the <code>toString</code> method will convert the date
* into a <code>String</code>.
* <p>
* This produces the same string as the <code>SimpleDateFormat.format</code>
* using the pattern <code>"EEE, dd MMM yyyy hh:mm:ss 'GMT'"</code>. This will
* however do the job faster as it does not take arbitrary inputs.
*
* @author Niall Gallagher
*/
public class DateParser extends Parser {
/**
* Ensure that the time zone for dates if set to GMT.
*/
private static final TimeZone ZONE = TimeZone.getTimeZone("GMT");
/**
* Contains the possible days of the week for RFC 1123.
*/
private static final String WKDAYS[] = { "Mon", "Tue", "Wed", "Thu", "Fri",
"Sat", "Sun" };
/**
* Contains the possible days of the week for RFC 850.
*/
private static final String WEEKDAYS[] = { "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
/**
* Contains the possible months in the year for HTTP-date.
*/
private static final String MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
/**
* Used as an index into the months array to get the month.
*/
private int month;
/**
* Represents the decimal value of the date such as 1977.
*/
private int year;
/**
* Represents the decimal value of the date such as 18.
*/
private int day;
/**
* Used as an index into the weekdays array to get the weekday.
*/
private int weekday;
/**
* Represents the decimal value of the hour such as 24.
*/
private int hour;
/**
* Represents the decimal value of the minute.
*/
private int mins;
/**
* Represents the decimal value for the second.
*/
private int secs;
/**
* The parser contains this method so that the a date does not have to be
* parsed from <code>System.currentTimeMillis</code>. This returns a date
* using a <code>DataParser.parse(long)</code> method invocation.
*
* @return this returns a RFC 1123 date for the current time
*/
public static String getDate() {
long time = System.currentTimeMillis();
return new DateParser(time).toString();
}
/**
* The default constructor will create a parser that can parse
* <code>String</code>s that contain dates in the form of RFC 1123, RFC 850
* or asctime. If the dates that are to be parsed are not in the form of one
* of these date encodings the results of this parser will be random.
*/
public DateParser() {
this.init();
}
/**
* This constructor will conveniently parse the <code>long</code> argument
* in the constructor. This can also be done by first calling the no-arg
* constructor and then using the parse method.
* <p>
* This will then set this object to one that uses the RFC 1123 format for a
* date.
*
* @param date
* the date to be parsed
*/
public DateParser(long date) {
this();
this.parse(date);
}
/**
* This constructor will conveniently parse the <code>String</code> argument
* in the constructor. This can also be done by first calling the no-arg
* constructor and then using the parse method.
* <p>
* This will then set this object to one that uses the RFC 1123 format for a
* date.
*
* @param date
* the date to be parsed
*/
public DateParser(String date) {
this();
this.parse(date);
}
/**
* This is used to extract the date from a <code>long</code>. If this method
* is given the value of the date as a <code>long</code> it will construct
* the RFC 1123 date as required by RFC 2616 sec 3.3.
* <p>
* This saves time on parsing a <code>String</code> that is encoded in the
* HTTP-date format. The date given must be positive, if the date given is
* not a positive '<code>long</code>' then the results of this method is
* random/unknown.
*
* @param date
* the date to be parsed
*/
public void parse(long date) {
Calendar calendar = Calendar.getInstance(ZONE);
calendar.setTimeInMillis(date);
this.weekday = calendar.get(DAY_OF_WEEK);
this.year = calendar.get(YEAR);
this.month = calendar.get(MONTH);
this.day = calendar.get(DAY_OF_MONTH);
this.hour = calendar.get(HOUR_OF_DAY);
this.mins = calendar.get(MINUTE);
this.secs = calendar.get(SECOND);
this.month = this.month > 11 ? 11 : this.month;
this.weekday = (this.weekday + 5) % 7;
}
/**
* Convenience method used to convert the specified HTTP date in to a long
* representing the time. This is used when a single method is required to
* convert a HTTP date format to a usable long value for use in creating
* <code>Date</code> objects.
*
* @param date
* the date specified in on of the HTTP date formats
*
* @return the date value as a long value in milliseconds
*/
public long convert(String date) {
this.parse(date);
return this.toLong();
}
/**
* Convenience method used to convert the specified long date in to a HTTP
* date format. This is used when a single method is required to convert a
* long data value in milliseconds to a HTTP date value.
*
* @param date
* the date specified as a long of milliseconds
*
* @return the date represented in the HTTP date format RFC 1123
*/
public String convert(long date) {
this.parse(date);
return this.toString();
}
/**
* This is used to reset the date and the buffer variables for this
* <code>DateParser</code>. Every in is set to the value of 0.
*/
@Override
protected void init() {
this.month = this.year = this.day = this.weekday = this.hour = this.mins = this.secs = this.off = 0;
}
/**
* This is used to parse the contents of the <code>buf</code>. This checks
* the fourth char of the buffer to see what it contains. Invariably a date
* format belonging to RFC 1123 will have a ',' character in position 4, a
* date format belonging to asctime will have a ' ' character in position 4
* and if neither of these characters are found at position 4 then it is
* assumed that the date is in the RFC 850 fromat, however it may not be.
*/
@Override
protected void parse() {
if (this.buf.length < 4) return;
if (this.buf[3] == ',') {
this.rfc1123();
} else if (this.buf[3] == ' ') {
this.asctime();
} else {
this.rfc850();
}
}
/**
* This will parse a date that is in the form of an RFC 1123 date. This date
* format is the date format that is to be used with all applications that
* are HTTP/1.1 compliant. The RFC 1123 date format is
*
* <pre>
* rfc1123 = 'wkday "," SP date1 SP time SP GMT'.
* date1 = '2DIGIT SP month SP 4DIGIT' and finally
* time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'.
* </pre>
*/
private void rfc1123() {
this.wkday();
this.off += 2;
this.date1();
this.off++;
this.time();
}
/**
* This will parse a date that is in the form of an RFC 850 date. This date
* format is the date format that is to be used with some applications that
* are HTTP/1.0 compliant. The RFC 1123 date format is
*
* <pre>
* rfc850 = 'weekday "," SP date2 SP time SP GMT'.
* date2 = '2DIGIT "-" month "-" 2DIGIT' and finally
* time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'.
* </pre>
*/
private void rfc850() {
this.weekday();
this.off += 2;
this.date2();
this.off++;
this.time();
}
/**
* This will parse a date that is in the form of an asctime date. This date
* format is the date format that is to be used with some applications that
* are HTTP/1.0 compliant. The RFC 1123 date format is
*
* <pre>
* asctime = 'weekday SP date3 SP time SP 4DIGIT'.
* date3 = 'month SP (2DIGIT | (SP 1DIGIT))' and
* time = '2DIGIT ":" 2DIGIT ":" 2DIGIT'.
* </pre>
*/
private void asctime() {
this.wkday();
this.off++;
this.date3();
this.off++;
this.time();
this.off++;
this.year4();
}
/**
* This is the date1 format of a date that is used by the RFC 1123 date
* format. This date is
*
* <pre>
* date1 = '2DIGIT SP month SP 4DIGIT'.
* example '02 Jun 1982'.
* </pre>
*/
private void date1() {
this.day();
this.off++;
this.month();
this.off++;
this.year4();
}
/**
* This is the date2 format of a date that is used by the RFC 850 date
* format. This date is
*
* <pre>
* date2 = '2DIGIT "-" month "-" 2DIGIT'
* example '02-Jun-82'.
* </pre>
*/
private void date2() {
this.day();
this.off++;
this.month();
this.off++;
this.year2();
}
/**
* This is the date3 format of a date that is used by the asctime date
* format. This date is
*
* <pre>
* date3 = 'month SP (2DIGIT | (SP 1DIGIT))'
* example 'Jun 2'.
*
* <pre>
*/
private void date3() {
this.month();
this.off++;
this.day();
}
/**
* This is used to parse a consecutive set of digit characters to create the
* day of the week. This will tolerate a space on front of the digits
* thiswill allow all date formats including asctime to use this to get the
* day. This may parse more than 2 digits, however if there are more than 2
* digits the date format is incorrect anyway.
*/
private void day() {
if (this.space(this.buf[this.off])) {
this.off++;
}
while (this.off < this.count) {
if (!this.digit(this.buf[this.off])) {
break;
}
this.day *= 10;
this.day += this.buf[this.off];
this.day -= '0';
this.off++;
}
}
/**
* This is used to get the year from a set of digit characters. This is used
* to parse years that are of the form of 2 digits (e.g 82) however this
* will assume that any dates that are in 2 digit format are dates for the
* 2000 th milleneum so 01 will be 2001.
* <p>
* This may parse more than 2 digits but if there are more than 2 digits in
* a row then the date format is incorrect anyway.
*/
private void year2() {
int mill = 2000; /* milleneum */
int cent = 0; /* century */
while (this.off < this.count) {
if (!this.digit(this.buf[this.off])) {
break;
}
cent *= 10;
cent += this.buf[this.off];
cent -= '0';
this.off++;
}
this.year = mill + cent; /* result 4 digits */
}
/**
* This is used to get the year from a set of digit characters. This is used
* to parse years that are of the form of 4 digits (e.g 1982).
* <p>
* This may parse more than 4 digits but if there are more than 2 digits in
* a row then the date format is incorrect anyway.
*/
private void year4() {
while (this.off < this.count) {
if (!this.digit(this.buf[this.off])) {
break;
}
this.year *= 10;
this.year += this.buf[this.off];
this.year -= '0';
this.off++;
}
}
/**
* This is used to parse the time for a HTTP-date. The time for a HTTP-date
* is in the form <code>00:00:00</code> that is
*
* <pre>
* time = '2DIGIT ":" 2DIGIT ":" 2DIGIT' so this will
* read only a time of that form, although this will
* parse time = '2DIGIT CHAR 2DIGIT CHAR 2DIGIT'.
* </pre>
*/
private void time() {
this.hours();
this.off++;
this.mins();
this.off++;
this.secs();
}
/**
* This is used to initialize the hour. This will read a consecutive
* sequence of digit characters and convert them into a decimal number to
* represent the hour that this date represents.
* <p>
* This may parse more than 2 digits but if there are more than 2 digits the
* date is already incorrect.
*/
private void hours() {
while (this.off < this.count) {
if (!this.digit(this.buf[this.off])) {
break;
}
this.hour *= 10;
this.hour += this.buf[this.off];
this.hour -= '0';
this.off++;
}
}
/**
* This is used to initialize the mins. This will read a consecutive
* sequence of digit characters and convert them into a decimal number to
* represent the mins that this date represents.
* <p>
* This may parse more than 2 digits but if there are more than 2 digits the
* date is already incorrect.
*/
private void mins() {
while (this.off < this.count) {
if (!this.digit(this.buf[this.off])) {
break;
}
this.mins *= 10;
this.mins += this.buf[this.off];
this.mins -= '0';
this.off++;
}
}
/**
* This is used to initialize the secs. This will read a consecutive
* sequence of digit characters and convert them into a decimal number to
* represent the secs that this date represents.
* <p>
* This may parse more than 2 digits but if there are more than 2 digits the
* date is already incorrect
*/
private void secs() {
while (this.off < this.count) {
if (!this.digit(this.buf[this.off])) {
break;
}
this.secs *= 10;
this.secs += this.buf[this.off];
this.secs -= '0';
this.off++;
}
}
/**
* This is used to read the week day of HTTP-date. The shorthand day (e.g
* Mon for Monday) is used by the RFC 1123 and asctime date formats. This
* will simply try to read each day from the buffer, when the day is read
* successfully then the index of that day is saved.
*/
private void wkday() {
for (int i = 0; i < WKDAYS.length; i++) {
if (this.skip(WKDAYS[i])) {
this.weekday = i;
return;
}
}
}
/**
* This is used to read the week day of HTTP-date. This format is used by
* the RFC 850 date format. This will simply try to read each day from the
* buffer, when the day is read successfully then the index of that day is
* saved.
*/
private void weekday() {
for (int i = 0; i < WKDAYS.length; i++) {
if (this.skip(WEEKDAYS[i])) {
this.weekday = i;
return;
}
}
}
/**
* This is used to read the month of HTTP-date. This will simply try to read
* each month from the buffer, when the month is read successfully then the
* index of that month is saved.
*/
private void month() {
for (int i = 0; i < MONTHS.length; i++) {
if (this.skip(MONTHS[i])) {
this.month = i;
return;
}
}
}
/**
* This is used to append the date in RFC 1123 format to the given string
* builder. This will append the date and a trailing space character to the
* buffer. Dates like the following are appended.
*
* <pre>
* Tue, 02 Jun 1982
* </pre>
*
* . For performance reasons a string builder is used. This avoids an
* unneeded synchronization caused by the string buffers.
*
* @param builder
* this is the builder to append the date to
*/
private void date(StringBuilder builder) {
builder.append(WKDAYS[this.weekday]);
builder.append(", ");
if (this.day <= 9) {
builder.append('0');
}
builder.append(this.day);
builder.append(' ');
builder.append(MONTHS[this.month]);
builder.append(' ');
builder.append(this.year);
builder.append(' ');
}
/**
* This is used to append the time in RFC 1123 format to the given string
* builder. This will append the time and a trailing space character to the
* buffer. Times like the following are appended.
*
* <pre>
* 23:59:59
* </pre>
*
* . For performance reasons a string builder is used. This avoids an
* unneeded synchronization caused by the string buffers.
*
* @param builder
* this is the builder to write the time to
*/
private void time(StringBuilder builder) {
if (this.hour <= 9) {
builder.append('0');
}
builder.append(this.hour);
builder.append(':');
if (this.mins <= 9) {
builder.append('0');
}
builder.append(this.mins);
builder.append(':');
if (this.secs <= 9) {
builder.append('0');
}
builder.append(this.secs);
builder.append(' ');
}
/**
* This is used to append the time zone to the provided appender. For HTTP
* the dates should always be in GMT format. So this will simply append the
* "GMT" string to the end of the builder.
*
* @param builder
* this builder to append the time zone to
*/
private void zone(StringBuilder builder) {
builder.append("GMT");
}
/**
* This returns the date in as a <code>long</code>, given the exact time
* this will use the <code>java.util.Date</code> to parse this date into a
* <code>long</code>. The <code>GregorianCalendar</code> uses the method
* <code>getTime</code> which produces the <code>Date</code> object from
* this the <code>getTime</code> returns the <code>long</code>
*
* @return the date parsed as a <code>long</code>
*/
public long toLong() {
Calendar calendar = Calendar.getInstance(ZONE); /* GMT */
calendar.set(this.year, this.month, this.day, this.hour, this.mins,
this.secs);
calendar.set(MILLISECOND, 0);
return calendar.getTime().getTime();
}
/**
* This prints the date in the format of a RFC 1123 date. Example
*
* <pre>
* Tue, 02 Jun 1982 23:59:59 GMT
* </pre>
*
* . This uses a <code>StringBuffer</code> to accumulate the various
* <code>String</code>s/<code>int</code>s to form the resulting date value.
* The resulting date value is the one required by RFC 2616.
* <p>
* The HTTP date must be in the form of RFC 1123. The hours, minutes and
* seconds are appended with the 0 character if they are less than 9 i.e. if
* they do not have two digits.
*
* @return the date in RFC 1123 format
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder(30);
this.date(builder);
this.time(builder);
this.zone(builder);
return builder.toString();
}
}