Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ThreadLocal with ConcurrentQueue in HttpDateFormat #5569

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading