From 07b3f949d3df35f796dc3124e1e3f72249fe1502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Tue, 11 Jun 2024 15:56:12 +0200 Subject: [PATCH] Fix two Date issues regarding preconditions - Round Dates to second precision when comparing them - Lookup header delegate for Date and subtypes Fixes #41110 --- .../preconditions/DatePreconditionTests.java | 63 +++++++++++++++++++ .../common/jaxrs/RuntimeDelegateImpl.java | 5 +- .../reactive/server/jaxrs/RequestImpl.java | 28 ++++++--- 3 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/preconditions/DatePreconditionTests.java diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/preconditions/DatePreconditionTests.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/preconditions/DatePreconditionTests.java new file mode 100644 index 0000000000000..8b8968bd2e484 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/preconditions/DatePreconditionTests.java @@ -0,0 +1,63 @@ +package io.quarkus.resteasy.reactive.server.test.preconditions; + +import static io.restassured.RestAssured.get; + +import java.time.Instant; +import java.util.Date; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Request; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.ResponseBuilder; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DatePreconditionTests { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Resource.class)); + + // Make sure we test a subtype of Date, since that is what Hibernate ORM gives us most of the time (hah) + // Also make sure we have non-zero milliseconds, since that will be the case for most date values representing + // "now", and we want to make sure pre-conditions work (second-resolution) + static final Date date = new Date(Date.from(Instant.parse("2007-12-03T10:15:30.24Z")).getTime()) { + }; + + public static class Something { + } + + @Test + public void test() { + get("/preconditions") + .then() + .statusCode(200) + .header("Last-Modified", "Mon, 03 Dec 2007 10:15:30 GMT") + .body(Matchers.equalTo("foo")); + RestAssured + .with() + .header("If-Modified-Since", "Mon, 03 Dec 2007 10:15:30 GMT") + .get("/preconditions") + .then() + .statusCode(304); + } + + @Path("/preconditions") + public static class Resource { + @GET + public Response get(Request request) { + ResponseBuilder resp = request.evaluatePreconditions(date); + if (resp != null) { + return resp.build(); + } + return Response.ok("foo").lastModified(date).build(); + } + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/RuntimeDelegateImpl.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/RuntimeDelegateImpl.java index 252ab655a3bb4..98039bb181a7a 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/RuntimeDelegateImpl.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/RuntimeDelegateImpl.java @@ -95,7 +95,10 @@ public HeaderDelegate createHeaderDelegate(Class type) throws IllegalA } if (type.equals(MediaType.class)) { return (HeaderDelegate) MediaTypeHeaderDelegate.INSTANCE; - } else if (type.equals(Date.class)) { + } else if (Date.class.isAssignableFrom(type)) { + // for Date, we do subtypes too, because ORM will instantiate java.util.Date as subtypes + // and it's extremely likely we get those here, and we still have to generate a valid + // date representation for them, rather than Object.toString which will be wrong return (HeaderDelegate) DateDelegate.INSTANCE; } else if (type.equals(CacheControl.class)) { return (HeaderDelegate) CacheControlDelegate.INSTANCE; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RequestImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RequestImpl.java index 9a33e0ea8c7e7..8b4e923712d01 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RequestImpl.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RequestImpl.java @@ -36,6 +36,7 @@ private boolean isRfc7232preconditions() { return true;//todo: do we need config for this? } + @Override public Variant selectVariant(List variants) throws IllegalArgumentException { if (variants == null || variants.size() == 0) throw new IllegalArgumentException("Variant list must not be empty"); @@ -53,7 +54,7 @@ public Variant selectVariant(List variants) throws IllegalArgumentExcep return negotiation.getBestMatch(variants); } - public List convertEtag(List tags) { + private List convertEtag(List tags) { ArrayList result = new ArrayList(); for (String tag : tags) { String[] split = tag.split(","); @@ -64,7 +65,7 @@ public List convertEtag(List tags) { return result; } - public Response.ResponseBuilder ifMatch(List ifMatch, EntityTag eTag) { + private Response.ResponseBuilder ifMatch(List ifMatch, EntityTag eTag) { boolean match = false; for (EntityTag tag : ifMatch) { if (tag.equals(eTag) || tag.getValue().equals("*")) { @@ -78,7 +79,7 @@ public Response.ResponseBuilder ifMatch(List ifMatch, EntityTag eTag) } - public Response.ResponseBuilder ifNoneMatch(List ifMatch, EntityTag eTag) { + private Response.ResponseBuilder ifNoneMatch(List ifMatch, EntityTag eTag) { boolean match = false; for (EntityTag tag : ifMatch) { if (tag.equals(eTag) || tag.getValue().equals("*")) { @@ -96,6 +97,7 @@ public Response.ResponseBuilder ifNoneMatch(List ifMatch, EntityTag e return null; } + @Override public Response.ResponseBuilder evaluatePreconditions(EntityTag eTag) { if (eTag == null) throw new IllegalArgumentException("ETag was null"); @@ -118,26 +120,36 @@ public Response.ResponseBuilder evaluatePreconditions(EntityTag eTag) { return builder; } - public Response.ResponseBuilder ifModifiedSince(String strDate, Date lastModified) { + private Response.ResponseBuilder ifModifiedSince(String strDate, Date lastModified) { Date date = DateUtil.parseDate(strDate); - if (date.getTime() >= lastModified.getTime()) { + if (date.getTime() >= millisecondsWithSecondsPrecision(lastModified)) { return Response.notModified(); } return null; } - public Response.ResponseBuilder ifUnmodifiedSince(String strDate, Date lastModified) { + private Response.ResponseBuilder ifUnmodifiedSince(String strDate, Date lastModified) { Date date = DateUtil.parseDate(strDate); - if (date.getTime() >= lastModified.getTime()) { + if (date.getTime() >= millisecondsWithSecondsPrecision(lastModified)) { return null; } return Response.status(Response.Status.PRECONDITION_FAILED).lastModified(lastModified); } + /** + * We must compare header dates (seconds-precision) with dates that have the same precision, + * otherwise they may include milliseconds and they will never match the Last-Modified + * values that we generate from them (since we drop their milliseconds when we write the headers) + */ + private long millisecondsWithSecondsPrecision(Date lastModified) { + return (lastModified.getTime() / 1000) * 1000; + } + + @Override public Response.ResponseBuilder evaluatePreconditions(Date lastModified) { if (lastModified == null) throw new IllegalArgumentException("Param cannot be null"); @@ -159,6 +171,7 @@ public Response.ResponseBuilder evaluatePreconditions(Date lastModified) { return builder; } + @Override public Response.ResponseBuilder evaluatePreconditions(Date lastModified, EntityTag eTag) { if (lastModified == null) throw new IllegalArgumentException("Last modified was null"); @@ -182,6 +195,7 @@ else if (lastModifiedBuilder == null && etagBuilder != null) return rtn; } + @Override public Response.ResponseBuilder evaluatePreconditions() { List ifMatch = requestContext.getHttpHeaders().getRequestHeaders().get(HttpHeaders.IF_MATCH); if (ifMatch == null || ifMatch.size() == 0) {