Skip to content

Commit

Permalink
Replace ThreadLocal with ConcurrentQueue in HttpDateFormat
Browse files Browse the repository at this point in the history
Introduce a common facade for SimpleDateFormat and DateTimeFormatter
Able to switch to DateTimeFormatter for a small performance boost

Signed-off-by: jansupol <jan.supol@oracle.com>
  • Loading branch information
jansupol committed Mar 21, 2024
1 parent 90d0e7d commit 6ad2d5b
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand Down Expand Up @@ -168,7 +168,7 @@ public static NewCookie parseNewCookie(String header) {
cookie.sameSite = NewCookie.SameSite.valueOf(value.toUpperCase());
} else if (param.startsWith("expires")) {
try {
cookie.expiry = HttpDateFormat.readDate(value + ", " + bites[++i]);
cookie.expiry = HttpDateFormat.readDate(value + ", " + bites[++i].trim());
} catch (ParseException e) {
LOGGER.log(Level.FINE, LocalizationMessages.ERROR_NEWCOOKIE_EXPIRES(value), e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand Down Expand Up @@ -43,7 +43,7 @@ public boolean supports(final Class<?> type) {
@Override
public String toString(final Date header) {
throwIllegalArgumentExceptionIfNull(header, LocalizationMessages.DATE_IS_NULL());
return HttpDateFormat.getPreferredDateFormat().format(header);
return HttpDateFormat.getPreferredDateFormatter().format(header);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
* Helper class for HTTP specified date formats.
Expand All @@ -33,6 +39,46 @@
*/
public final class HttpDateFormat {

private static final boolean USE_SIMPLE_DATE_FORMAT_OVER_DATE_TIME_FORMATTER = true;

/**
* <p>
* A minimum formatter for converting java {@link Date} and {@link LocalDateTime} to {@code String} and vice-versa.
* </p>
* <p>
* Works as a facade for implementation backed by {@link SimpleDateFormat} and {@link DateTimeFormatter}.
* </p>
*/
public static interface HttpDateFormatter {
/**
*
* @param date
* @return
*/
Date toDate(String date);

/**
*
* @param date
* @return
*/
LocalDateTime toDateTime(String date);
/**
* Formats a {@link Date} into a date-time string.
*
* @param date the time value to be formatted into a date-time string.
* @return the formatted date-time string.
*/
String format(Date date);
/**
* Formats a {@link LocalDateTime} into a date-time string.
*
* @param dateTime the time value to be formatted into a date-time string.
* @return the formatted date-time string.
*/
String format(LocalDateTime dateTime);
}

private HttpDateFormat() {
}
/**
Expand All @@ -50,33 +96,65 @@ private HttpDateFormat() {

private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT");

private static final ThreadLocal<List<SimpleDateFormat>> dateFormats = ThreadLocal.withInitial(() -> createDateFormats());
private static final List<HttpDateFormatter> dateFormats = createDateFormats();
private static final Queue<List<HttpDateFormatter>> simpleDateFormats = new ConcurrentLinkedQueue<>();

private static List<SimpleDateFormat> createDateFormats() {
final SimpleDateFormat[] formats = new SimpleDateFormat[]{
new SimpleDateFormat(RFC1123_DATE_FORMAT_PATTERN, Locale.US),
new SimpleDateFormat(RFC1036_DATE_FORMAT_PATTERN, Locale.US),
new SimpleDateFormat(ANSI_C_ASCTIME_DATE_FORMAT_PATTERN, Locale.US)
private static List<HttpDateFormatter> createDateFormats() {
final HttpDateFormatter[] formats = new HttpDateFormatter[]{
new HttpDateFormatterFromDateTimeFormatter(
DateTimeFormatter.ofPattern(RFC1123_DATE_FORMAT_PATTERN, Locale.US).withZone(GMT_TIME_ZONE.toZoneId())),
new HttpDateFormatterFromDateTimeFormatter(
DateTimeFormatter.ofPattern(RFC1123_DATE_FORMAT_PATTERN.replace("zzz", "ZZZ"), Locale.US)
.withZone(GMT_TIME_ZONE.toZoneId())),
new HttpDateFormatterFromDateTimeFormatter(
DateTimeFormatter.ofPattern(RFC1036_DATE_FORMAT_PATTERN, Locale.US).withZone(GMT_TIME_ZONE.toZoneId())),
new HttpDateFormatterFromDateTimeFormatter(
DateTimeFormatter.ofPattern(RFC1036_DATE_FORMAT_PATTERN.replace("zzz", "ZZZ"), Locale.US)
.withZone(GMT_TIME_ZONE.toZoneId())),
new HttpDateFormatterFromDateTimeFormatter(
DateTimeFormatter.ofPattern(ANSI_C_ASCTIME_DATE_FORMAT_PATTERN, Locale.US)
.withZone(GMT_TIME_ZONE.toZoneId()))
};
formats[0].setTimeZone(GMT_TIME_ZONE);
formats[1].setTimeZone(GMT_TIME_ZONE);
formats[2].setTimeZone(GMT_TIME_ZONE);

return Collections.unmodifiableList(Arrays.asList(formats));
}

private static List<HttpDateFormatter> createSimpleDateFormats() {
final HttpDateFormatterFromSimpleDateTimeFormat[] formats = new HttpDateFormatterFromSimpleDateTimeFormat[]{
new HttpDateFormatterFromSimpleDateTimeFormat(new SimpleDateFormat(RFC1123_DATE_FORMAT_PATTERN, Locale.US)),
new HttpDateFormatterFromSimpleDateTimeFormat(new SimpleDateFormat(RFC1036_DATE_FORMAT_PATTERN, Locale.US)),
new HttpDateFormatterFromSimpleDateTimeFormat(new SimpleDateFormat(ANSI_C_ASCTIME_DATE_FORMAT_PATTERN, Locale.US))
};
formats[0].simpleDateFormat.setTimeZone(GMT_TIME_ZONE);
formats[1].simpleDateFormat.setTimeZone(GMT_TIME_ZONE);
formats[2].simpleDateFormat.setTimeZone(GMT_TIME_ZONE);

return Collections.unmodifiableList(Arrays.asList(formats));
}

/**
* Return an unmodifiable list of HTTP specified date formats to use for
* parsing or formatting {@link Date}.
* Get the preferred HTTP specified date format (RFC 1123).
* <p>
* The list of date formats are scoped to the current thread and may be
* used without requiring to synchronize access to the instances when
* The date format is scoped to the current thread and may be
* used without requiring to synchronize access to the instance when
* parsing or formatting.
*
* @return the list of data formats.
* @return the preferred of data format.
*/
private static List<SimpleDateFormat> getDateFormats() {
return dateFormats.get();
public static HttpDateFormatter getPreferredDateFormatter() {
if (USE_SIMPLE_DATE_FORMAT_OVER_DATE_TIME_FORMATTER) {
List<HttpDateFormatter> list = simpleDateFormats.poll();
if (list == null) {
list = createSimpleDateFormats();
}
// returns clone because calling SDF.parse(...) can change time zone
final SimpleDateFormat sdf = (SimpleDateFormat)
((HttpDateFormatterFromSimpleDateTimeFormat) list.get(0)).simpleDateFormat.clone();
simpleDateFormats.add(list);
return new HttpDateFormatterFromSimpleDateTimeFormat(sdf);
} else {
return dateFormats.get(0);
}
}

/**
Expand All @@ -87,10 +165,20 @@ private static List<SimpleDateFormat> getDateFormats() {
* parsing or formatting.
*
* @return the preferred of data format.
* @deprecated Use getPreferredDateFormatter instead
*/
// Unused in Jersey
@Deprecated(forRemoval = true)
public static SimpleDateFormat getPreferredDateFormat() {
List<HttpDateFormatter> list = simpleDateFormats.poll();
if (list == null) {
list = createSimpleDateFormats();
}
// returns clone because calling SDF.parse(...) can change time zone
return (SimpleDateFormat) dateFormats.get().get(0).clone();
final SimpleDateFormat sdf = (SimpleDateFormat)
((HttpDateFormatterFromSimpleDateTimeFormat) list.get(0)).simpleDateFormat.clone();
simpleDateFormats.add(list);
return sdf;
}

/**
Expand All @@ -102,18 +190,106 @@ public static SimpleDateFormat getPreferredDateFormat() {
* @throws java.text.ParseException in case the date string cannot be parsed.
*/
public static Date readDate(final String date) throws ParseException {
ParseException pe = null;
for (final SimpleDateFormat f : HttpDateFormat.getDateFormats()) {
return USE_SIMPLE_DATE_FORMAT_OVER_DATE_TIME_FORMATTER
? readDateSDF(date)
: readDateDTF(date);
}

private static Date readDateDTF(final String date) throws ParseException {
final List<HttpDateFormatter> list = dateFormats;
return readDate(date, list);
}

private static Date readDateSDF(final String date) throws ParseException {
List<HttpDateFormatter> list = simpleDateFormats.poll();
if (list == null) {
list = createSimpleDateFormats();
}
final Date ret = readDate(date, list);
simpleDateFormats.add(list);
return ret;
}

private static Date readDate(final String date, List<HttpDateFormatter> formatters) throws ParseException {
Exception pe = null;
for (final HttpDateFormatter f : formatters) {
try {
Date result = f.parse(date);
// parse can change time zone -> set it back to GMT
f.setTimeZone(GMT_TIME_ZONE);
return result;
} catch (final ParseException e) {
return f.toDate(date);
} catch (final Exception e) {
pe = (pe == null) ? e : pe;
}
}

throw pe;
throw ParseException.class.isInstance(pe) ? (ParseException) pe
: new ParseException(pe.getMessage(),
DateTimeParseException.class.isInstance(pe) ? ((DateTimeParseException) pe).getErrorIndex() : 0);
}

/**
* Warning! DateTimeFormatter is incompatible with SimpleDateFormat for two digits year, since SimpleDateFormat uses
* 80 years before now and 20 years after, whereas DateTimeFormatter uses years starting with 2000.
*/
private static class HttpDateFormatterFromDateTimeFormatter implements HttpDateFormatter {
private final DateTimeFormatter dateTimeFormatter;

private HttpDateFormatterFromDateTimeFormatter(DateTimeFormatter dateTimeFormatter) {
this.dateTimeFormatter = dateTimeFormatter;
}

@Override
public Date toDate(String date) {
return new Date(Instant.from(dateTimeFormatter.parse(date)).toEpochMilli());
}

@Override
public LocalDateTime toDateTime(String date) {
return Instant.from(dateTimeFormatter.parse(date)).atZone(GMT_TIME_ZONE.toZoneId()).toLocalDateTime();
}

@Override
public String format(Date date) {
return dateTimeFormatter.format(date.toInstant());
}

@Override
public String format(LocalDateTime dateTime) {
return dateTimeFormatter.format(dateTime);
}
}

private static class HttpDateFormatterFromSimpleDateTimeFormat implements HttpDateFormatter {
private final SimpleDateFormat simpleDateFormat;

private HttpDateFormatterFromSimpleDateTimeFormat(SimpleDateFormat simpleDateFormat) {
this.simpleDateFormat = simpleDateFormat;
}

@Override
public Date toDate(String date) {
final Date result;
try {
result = simpleDateFormat.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
// parse can change time zone -> set it back to GMT
simpleDateFormat.setTimeZone(GMT_TIME_ZONE);
return result;
}

@Override
public LocalDateTime toDateTime(String date) {
return Instant.from(toDate(date).toInstant()).atZone(GMT_TIME_ZONE.toZoneId()).toLocalDateTime();
}

@Override
public String format(Date date) {
return simpleDateFormat.format(date);
}

@Override
public String format(LocalDateTime dateTime) {
return simpleDateFormat.format(Date.from(dateTime.atZone(GMT_TIME_ZONE.toZoneId()).toInstant()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand Down Expand Up @@ -81,7 +81,7 @@ public String toString(final NewCookie cookie) {
}
if (cookie.getExpiry() != null) {
b.append(";Expires=");
b.append(HttpDateFormat.getPreferredDateFormat().format(cookie.getExpiry()));
b.append(HttpDateFormat.getPreferredDateFormatter().format(cookie.getExpiry()));
}

return b.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand Down Expand Up @@ -402,8 +402,8 @@ public void testFormParamDate() throws ExecutionException, InterruptedException
initiateWebApplication(FormResourceDate.class);

final String date_RFC1123 = "Sun, 06 Nov 1994 08:49:37 GMT";
final String date_RFC1036 = "Sunday, 06-Nov-94 08:49:37 GMT";
final String date_ANSI_C = "Sun Nov 6 08:49:37 1994";
final String date_RFC1036 = "Sunday, 07-Nov-04 08:49:37 GMT";
final String date_ANSI_C = "Sun Nov 6 08:49:37 1994";

final Form form = new Form();
form.param("a", date_RFC1123);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ protected void addStringParameter(final StringBuilder sb, final String name, fin

protected void addDateParameter(final StringBuilder sb, final String name, final Date p) {
if (p != null) {
sb.append("; ").append(name).append("=\"").append(HttpDateFormat.getPreferredDateFormat().format(p)).append("\"");
sb.append("; ").append(name).append("=\"")
.append(HttpDateFormat.getPreferredDateFormatter().format(p))
.append("\"");
}
}

Expand Down Expand Up @@ -302,7 +304,7 @@ private Date createDate(final String name) throws ParseException {
if (value == null) {
return null;
}
return HttpDateFormat.getPreferredDateFormat().parse(value);
return HttpDateFormat.getPreferredDateFormatter().toDate(value);
}

private long createLong(final String name) throws ParseException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void testCreate() {
contentDisposition = new ContentDisposition(header);
assertNotNull(contentDisposition);
assertEquals(contentDispositionType, contentDisposition.getType());
final String dateString = HttpDateFormat.getPreferredDateFormat().format(date);
final String dateString = HttpDateFormat.getPreferredDateFormatter().format(date);
header = contentDispositionType + ";filename=\"test.file\";creation-date=\""
+ dateString + "\";modification-date=\"" + dateString + "\";read-date=\""
+ dateString + "\";size=1222";
Expand Down Expand Up @@ -101,7 +101,7 @@ public void testToString() {
final Date date = new Date();
final ContentDisposition contentDisposition = ContentDisposition.type(contentDispositionType).fileName("test.file")
.creationDate(date).modificationDate(date).readDate(date).size(1222).build();
final String dateString = HttpDateFormat.getPreferredDateFormat().format(date);
final String dateString = HttpDateFormat.getPreferredDateFormatter().format(date);
final String header = contentDispositionType + "; filename=\"test.file\"; creation-date=\""
+ dateString + "\"; modification-date=\"" + dateString + "\"; read-date=\"" + dateString + "\"; size=1222";
assertEquals(header, contentDisposition.toString());
Expand Down Expand Up @@ -252,7 +252,7 @@ private void assertFileNameExt(
final boolean decode
) throws ParseException {
final Date date = new Date();
final String dateString = HttpDateFormat.getPreferredDateFormat().format(date);
final String dateString = HttpDateFormat.getPreferredDateFormatter().format(date);
final String prefixHeader = contentDispositionType + ";filename=\"" + actualFileName + "\";"
+ "creation-date=\"" + dateString + "\";modification-date=\"" + dateString + "\";read-date=\""
+ dateString + "\";size=1222" + ";name=\"testData\";" + "filename*=\"";
Expand Down
Loading

0 comments on commit 6ad2d5b

Please sign in to comment.