/*
* Copyright 2016 The Netty Project
*
* The Netty Project 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 io.netty.handler.codec;
import static io.netty.util.internal.
ObjectUtil.checkNotNull;
import io.netty.util.
AsciiString;
import io.netty.util.concurrent.
FastThreadLocal;
import java.util.
BitSet;
import java.util.
Calendar;
import java.util.
Date;
import java.util.
GregorianCalendar;
import java.util.
TimeZone;
/**
* A formatter for HTTP header dates, such as "Expires" and "Date" headers, or "expires" field in "Set-Cookie".
*
* On the parsing side, it honors RFC6265 (so it supports RFC1123).
* Note that:
* <ul>
* <li>Day of week is ignored and not validated</li>
* <li>Timezone is ignored, as RFC6265 assumes UTC</li>
* </ul>
* If you're looking for a date format that validates day of week, or supports other timezones, consider using
* java.util.DateTimeFormatter.RFC_1123_DATE_TIME.
*
* On the formatting side, it uses RFC1123 format.
*
* @see <a href="https://tools.ietf.org/html/rfc6265#section-5.1.1">RFC6265</a> for the parsing side
* @see <a href="https://tools.ietf.org/html/rfc1123#page-55">RFC1123</a> for the encoding side.
*/
public final class
DateFormatter {
private static final
BitSet DELIMITERS = new
BitSet();
static {
DELIMITERS.
set(0x09);
for (char
c = 0x20;
c <= 0x2F;
c++) {
DELIMITERS.
set(
c);
}
for (char
c = 0x3B;
c <= 0x40;
c++) {
DELIMITERS.
set(
c);
}
for (char
c = 0x5B;
c <= 0x60;
c++) {
DELIMITERS.
set(
c);
}
for (char
c = 0x7B;
c <= 0x7E;
c++) {
DELIMITERS.
set(
c);
}
}
private static final
String[]
DAY_OF_WEEK_TO_SHORT_NAME =
new
String[]{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
private static final
String[]
CALENDAR_MONTH_TO_SHORT_NAME =
new
String[]{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
private static final
FastThreadLocal<
DateFormatter>
INSTANCES =
new
FastThreadLocal<
DateFormatter>() {
@
Override
protected
DateFormatter initialValue() {
return new
DateFormatter();
}
};
/**
* Parse some text into a {@link Date}, according to RFC6265
* @param txt text to parse
* @return a {@link Date}, or null if text couldn't be parsed
*/
public static
Date parseHttpDate(
CharSequence txt) {
return
parseHttpDate(
txt, 0,
txt.
length());
}
/**
* Parse some text into a {@link Date}, according to RFC6265
* @param txt text to parse
* @param start the start index inside {@code txt}
* @param end the end index inside {@code txt}
* @return a {@link Date}, or null if text couldn't be parsed
*/
public static
Date parseHttpDate(
CharSequence txt, int
start, int
end) {
int
length =
end -
start;
if (
length == 0) {
return null;
} else if (
length < 0) {
throw new
IllegalArgumentException("Can't have end < start");
} else if (
length > 64) {
throw new
IllegalArgumentException("Can't parse more than 64 chars," +
"looks like a user error or a malformed header");
}
return
formatter().
parse0(
checkNotNull(
txt, "txt"),
start,
end);
}
/**
* Format a {@link Date} into RFC1123 format
* @param date the date to format
* @return a RFC1123 string
*/
public static
String format(
Date date) {
return
formatter().
format0(
checkNotNull(
date, "date"));
}
/**
* Append a {@link Date} to a {@link StringBuilder} into RFC1123 format
* @param date the date to format
* @param sb the StringBuilder
* @return the same StringBuilder
*/
public static
StringBuilder append(
Date date,
StringBuilder sb) {
return
formatter().
append0(
checkNotNull(
date, "date"),
checkNotNull(
sb, "sb"));
}
private static
DateFormatter formatter() {
DateFormatter formatter =
INSTANCES.
get();
formatter.
reset();
return
formatter;
}
// delimiter = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E
private static boolean
isDelim(char
c) {
return
DELIMITERS.
get(
c);
}
private static boolean
isDigit(char
c) {
return
c >= 48 &&
c <= 57;
}
private static int
getNumericalValue(char
c) {
return
c - 48;
}
private final
GregorianCalendar cal = new
GregorianCalendar(
TimeZone.
getTimeZone("UTC"));
private final
StringBuilder sb = new
StringBuilder(29); // Sun, 27 Nov 2016 19:37:15 GMT
private boolean
timeFound;
private int
hours;
private int
minutes;
private int
seconds;
private boolean
dayOfMonthFound;
private int
dayOfMonth;
private boolean
monthFound;
private int
month;
private boolean
yearFound;
private int
year;
private
DateFormatter() {
reset();
}
public void
reset() {
timeFound = false;
hours = -1;
minutes = -1;
seconds = -1;
dayOfMonthFound = false;
dayOfMonth = -1;
monthFound = false;
month = -1;
yearFound = false;
year = -1;
cal.
clear();
sb.
setLength(0);
}
private boolean
tryParseTime(
CharSequence txt, int
tokenStart, int
tokenEnd) {
int
len =
tokenEnd -
tokenStart;
// h:m:s to hh:mm:ss
if (
len < 5 ||
len > 8) {
return false;
}
int
localHours = -1;
int
localMinutes = -1;
int
localSeconds = -1;
int
currentPartNumber = 0;
int
currentPartValue = 0;
int
numDigits = 0;
for (int
i =
tokenStart;
i <
tokenEnd;
i++) {
char
c =
txt.
charAt(
i);
if (
isDigit(
c)) {
currentPartValue =
currentPartValue * 10 +
getNumericalValue(
c);
if (++
numDigits > 2) {
return false; // too many digits in this part
}
} else if (
c == ':') {
if (
numDigits == 0) {
// no digits between separators
return false;
}
switch (
currentPartNumber) {
case 0:
// flushing hours
localHours =
currentPartValue;
break;
case 1:
// flushing minutes
localMinutes =
currentPartValue;
break;
default:
// invalid, too many :
return false;
}
currentPartValue = 0;
currentPartNumber++;
numDigits = 0;
} else {
// invalid char
return false;
}
}
if (
numDigits > 0) {
// pending seconds
localSeconds =
currentPartValue;
}
if (
localHours >= 0 &&
localMinutes >= 0 &&
localSeconds >= 0) {
hours =
localHours;
minutes =
localMinutes;
seconds =
localSeconds;
return true;
}
return false;
}
private boolean
tryParseDayOfMonth(
CharSequence txt, int
tokenStart, int
tokenEnd) {
int
len =
tokenEnd -
tokenStart;
if (
len == 1) {
char
c0 =
txt.
charAt(
tokenStart);
if (
isDigit(
c0)) {
dayOfMonth =
getNumericalValue(
c0);
return true;
}
} else if (
len == 2) {
char
c0 =
txt.
charAt(
tokenStart);
char
c1 =
txt.
charAt(
tokenStart + 1);
if (
isDigit(
c0) &&
isDigit(
c1)) {
dayOfMonth =
getNumericalValue(
c0) * 10 +
getNumericalValue(
c1);
return true;
}
}
return false;
}
private static boolean
matchMonth(
String month,
CharSequence txt, int
tokenStart) {
return
AsciiString.
regionMatchesAscii(
month, true, 0,
txt,
tokenStart, 3);
}
private boolean
tryParseMonth(
CharSequence txt, int
tokenStart, int
tokenEnd) {
int
len =
tokenEnd -
tokenStart;
if (
len != 3) {
return false;
}
if (
matchMonth("Jan",
txt,
tokenStart)) {
month =
Calendar.
JANUARY;
} else if (
matchMonth("Feb",
txt,
tokenStart)) {
month =
Calendar.
FEBRUARY;
} else if (
matchMonth("Mar",
txt,
tokenStart)) {
month =
Calendar.
MARCH;
} else if (
matchMonth("Apr",
txt,
tokenStart)) {
month =
Calendar.
APRIL;
} else if (
matchMonth("May",
txt,
tokenStart)) {
month =
Calendar.
MAY;
} else if (
matchMonth("Jun",
txt,
tokenStart)) {
month =
Calendar.
JUNE;
} else if (
matchMonth("Jul",
txt,
tokenStart)) {
month =
Calendar.
JULY;
} else if (
matchMonth("Aug",
txt,
tokenStart)) {
month =
Calendar.
AUGUST;
} else if (
matchMonth("Sep",
txt,
tokenStart)) {
month =
Calendar.
SEPTEMBER;
} else if (
matchMonth("Oct",
txt,
tokenStart)) {
month =
Calendar.
OCTOBER;
} else if (
matchMonth("Nov",
txt,
tokenStart)) {
month =
Calendar.
NOVEMBER;
} else if (
matchMonth("Dec",
txt,
tokenStart)) {
month =
Calendar.
DECEMBER;
} else {
return false;
}
return true;
}
private boolean
tryParseYear(
CharSequence txt, int
tokenStart, int
tokenEnd) {
int
len =
tokenEnd -
tokenStart;
if (
len == 2) {
char
c0 =
txt.
charAt(
tokenStart);
char
c1 =
txt.
charAt(
tokenStart + 1);
if (
isDigit(
c0) &&
isDigit(
c1)) {
year =
getNumericalValue(
c0) * 10 +
getNumericalValue(
c1);
return true;
}
} else if (
len == 4) {
char
c0 =
txt.
charAt(
tokenStart);
char
c1 =
txt.
charAt(
tokenStart + 1);
char
c2 =
txt.
charAt(
tokenStart + 2);
char
c3 =
txt.
charAt(
tokenStart + 3);
if (
isDigit(
c0) &&
isDigit(
c1) &&
isDigit(
c2) &&
isDigit(
c3)) {
year =
getNumericalValue(
c0) * 1000 +
getNumericalValue(
c1) * 100 +
getNumericalValue(
c2) * 10 +
getNumericalValue(
c3);
return true;
}
}
return false;
}
private boolean
parseToken(
CharSequence txt, int
tokenStart, int
tokenEnd) {
// return true if all parts are found
if (!
timeFound) {
timeFound =
tryParseTime(
txt,
tokenStart,
tokenEnd);
if (
timeFound) {
return
dayOfMonthFound &&
monthFound &&
yearFound;
}
}
if (!
dayOfMonthFound) {
dayOfMonthFound =
tryParseDayOfMonth(
txt,
tokenStart,
tokenEnd);
if (
dayOfMonthFound) {
return
timeFound &&
monthFound &&
yearFound;
}
}
if (!
monthFound) {
monthFound =
tryParseMonth(
txt,
tokenStart,
tokenEnd);
if (
monthFound) {
return
timeFound &&
dayOfMonthFound &&
yearFound;
}
}
if (!
yearFound) {
yearFound =
tryParseYear(
txt,
tokenStart,
tokenEnd);
}
return
timeFound &&
dayOfMonthFound &&
monthFound &&
yearFound;
}
private
Date parse0(
CharSequence txt, int
start, int
end) {
boolean
allPartsFound =
parse1(
txt,
start,
end);
return
allPartsFound &&
normalizeAndValidate() ?
computeDate() : null;
}
private boolean
parse1(
CharSequence txt, int
start, int
end) {
// return true if all parts are found
int
tokenStart = -1;
for (int
i =
start;
i <
end;
i++) {
char
c =
txt.
charAt(
i);
if (
isDelim(
c)) {
if (
tokenStart != -1) {
// terminate token
if (
parseToken(
txt,
tokenStart,
i)) {
return true;
}
tokenStart = -1;
}
} else if (
tokenStart == -1) {
// start new token
tokenStart =
i;
}
}
// terminate trailing token
return
tokenStart != -1 &&
parseToken(
txt,
tokenStart,
txt.
length());
}
private boolean
normalizeAndValidate() {
if (
dayOfMonth < 1
||
dayOfMonth > 31
||
hours > 23
||
minutes > 59
||
seconds > 59) {
return false;
}
if (
year >= 70 &&
year <= 99) {
year += 1900;
} else if (
year >= 0 &&
year < 70) {
year += 2000;
} else if (
year < 1601) {
// invalid value
return false;
}
return true;
}
private
Date computeDate() {
cal.
set(
Calendar.
DAY_OF_MONTH,
dayOfMonth);
cal.
set(
Calendar.
MONTH,
month);
cal.
set(
Calendar.
YEAR,
year);
cal.
set(
Calendar.
HOUR_OF_DAY,
hours);
cal.
set(
Calendar.
MINUTE,
minutes);
cal.
set(
Calendar.
SECOND,
seconds);
return
cal.
getTime();
}
private
String format0(
Date date) {
append0(
date,
sb);
return
sb.
toString();
}
private
StringBuilder append0(
Date date,
StringBuilder sb) {
cal.
setTime(
date);
sb.
append(
DAY_OF_WEEK_TO_SHORT_NAME[
cal.
get(
Calendar.
DAY_OF_WEEK) - 1]).
append(", ");
sb.
append(
cal.
get(
Calendar.
DAY_OF_MONTH)).
append(' ');
sb.
append(
CALENDAR_MONTH_TO_SHORT_NAME[
cal.
get(
Calendar.
MONTH)]).
append(' ');
sb.
append(
cal.
get(
Calendar.
YEAR)).
append(' ');
appendZeroLeftPadded(
cal.
get(
Calendar.
HOUR_OF_DAY),
sb).
append(':');
appendZeroLeftPadded(
cal.
get(
Calendar.
MINUTE),
sb).
append(':');
return
appendZeroLeftPadded(
cal.
get(
Calendar.
SECOND),
sb).
append(" GMT");
}
private static
StringBuilder appendZeroLeftPadded(int
value,
StringBuilder sb) {
if (
value < 10) {
sb.
append('0');
}
return
sb.
append(
value);
}
}