Replace ThreadLocal with ConcurrentQueue in HttpDateFormat
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>
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java
index bcd7e5f..14a008d 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java
@@ -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
@@ -168,7 +168,7 @@
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);
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/DateProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/DateProvider.java
index 09e7553..8417fb5 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/DateProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/DateProvider.java
@@ -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
@@ -43,7 +43,7 @@
@Override
public String toString(final Date header) {
throwIllegalArgumentExceptionIfNull(header, LocalizationMessages.DATE_IS_NULL());
- return HttpDateFormat.getPreferredDateFormat().format(header);
+ return HttpDateFormat.getPreferredDateFormatter().format(header);
}
@Override
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpDateFormat.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpDateFormat.java
index 0f76eed..9cf2abf 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpDateFormat.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpDateFormat.java
@@ -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.
@@ -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() {
}
/**
@@ -50,33 +96,40 @@
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));
}
- /**
- * Return an unmodifiable list of HTTP specified date formats to use for
- * parsing or formatting {@link Date}.
- * <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
- * parsing or formatting.
- *
- * @return the list of data formats.
- */
- private static List<SimpleDateFormat> getDateFormats() {
- return dateFormats.get();
+ 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));
}
/**
@@ -88,9 +141,44 @@
*
* @return the preferred of data format.
*/
+ 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);
+ }
+ }
+
+ /**
+ * Get the preferred HTTP specified date format (RFC 1123).
+ * <p>
+ * 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 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;
}
/**
@@ -102,18 +190,106 @@
* @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()));
+ }
}
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/NewCookieProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/NewCookieProvider.java
index 8615aeb..08fbf5a 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/NewCookieProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/NewCookieProvider.java
@@ -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
@@ -81,7 +81,7 @@
}
if (cookie.getExpiry() != null) {
b.append(";Expires=");
- b.append(HttpDateFormat.getPreferredDateFormat().format(cookie.getExpiry()));
+ b.append(HttpDateFormat.getPreferredDateFormatter().format(cookie.getExpiry()));
}
return b.toString();
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/FormParamTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/FormParamTest.java
index 34f29d6..07b368e 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/FormParamTest.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/FormParamTest.java
@@ -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
@@ -402,8 +402,8 @@
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);
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java
index 6d77453..f6c7165 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java
@@ -202,7 +202,9 @@
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("\"");
}
}
@@ -302,7 +304,7 @@
if (value == null) {
return null;
}
- return HttpDateFormat.getPreferredDateFormat().parse(value);
+ return HttpDateFormat.getPreferredDateFormatter().toDate(value);
}
private long createLong(final String name) throws ParseException {
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java
index 8cecb6b..138466e 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java
@@ -66,7 +66,7 @@
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";
@@ -101,7 +101,7 @@
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());
@@ -252,7 +252,7 @@
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*=\"";
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/FormDataContentDispositionTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/FormDataContentDispositionTest.java
index c931809..0595890 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/FormDataContentDispositionTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/FormDataContentDispositionTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -45,7 +45,7 @@
.modificationDate(date).readDate(date).size(1222).build();
assertFormDataContentDisposition(contentDisposition, date);
try {
- 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"
+ ";name=\"testData\"";
@@ -92,7 +92,7 @@
final FormDataContentDisposition contentDisposition = FormDataContentDisposition.name("testData")
.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"
+ "; name=\"testData\"";
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/HttpHeaderTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/HttpHeaderTest.java
index 63305db..4e1bfd3 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/HttpHeaderTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/HttpHeaderTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -100,8 +100,8 @@
@Test
public void testDateParsing() throws ParseException {
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";
HttpHeaderReader.readDate(date_RFC1123);
HttpHeaderReader.readDate(date_RFC1036);
@@ -113,7 +113,7 @@
final String date_RFC1123 = "Sun, 06 Nov 1994 08:49:37 GMT";
final Date date = HttpHeaderReader.readDate(date_RFC1123);
- final String date_formatted = HttpDateFormat.getPreferredDateFormat().format(date);
+ final String date_formatted = HttpDateFormat.getPreferredDateFormatter().format(date);
assertEquals(date_RFC1123, date_formatted);
}