diff --git a/spring-web/src/main/java/org/springframework/http/ETag.java b/spring-web/src/main/java/org/springframework/http/ETag.java index 0acfaefc04..35c767eccd 100644 --- a/spring-web/src/main/java/org/springframework/http/ETag.java +++ b/spring-web/src/main/java/org/springframework/http/ETag.java @@ -47,6 +47,44 @@ public record ETag(String tag, boolean weak) { return (this == WILDCARD); } + /** + * Perform a strong or weak comparison to another {@link ETag}. + * @param other the ETag to compare to + * @param strong whether to perform strong or weak comparison + * @return whether there is a match or not + * @since 6.2 + * @see RFC 9110, Section 8.8.3.2 + */ + public boolean compare(ETag other, boolean strong) { + if (!StringUtils.hasLength(tag()) || !StringUtils.hasLength(other.tag())) { + return false; + } + + if (strong && (weak() || other.weak())) { + return false; + } + + return tag().equals(other.tag()); + } + + @Override + public boolean equals(Object other) { + return (this == other || + (other instanceof ETag oet && this.tag.equals(oet.tag) && this.weak == oet.weak)); + } + + @Override + public int hashCode() { + int result = this.tag.hashCode(); + result = 31 * result + Boolean.hashCode(this.weak); + return result; + } + + @Override + public String toString() { + return formattedTag(); + } + /** * Return the fully formatted tag including "W/" prefix and quotes. */ @@ -57,11 +95,23 @@ public record ETag(String tag, boolean weak) { return (this.weak ? "W/" : "") + "\"" + this.tag + "\""; } - @Override - public String toString() { - return formattedTag(); - } + /** + * Create an {@link ETag} instance from a String representation. + * @param rawValue the formatted ETag value + * @return the created instance + * @since 6.2 + */ + public static ETag create(String rawValue) { + boolean weak = rawValue.startsWith("W/"); + if (weak) { + rawValue = rawValue.substring(2); + } + if (rawValue.length() > 2 && rawValue.startsWith("\"") && rawValue.endsWith("\"")) { + rawValue = rawValue.substring(1, rawValue.length() - 1); + } + return new ETag(rawValue, weak); + } /** * Parse entity tags from an "If-Match" or "If-None-Match" header. diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index 141a258c14..554aca37ce 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -247,25 +247,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return true; } - private boolean matchRequestedETags(Enumeration requestedETags, @Nullable String eTag, boolean weakCompare) { - if (StringUtils.hasLength(eTag)) { - eTag = ETag.quoteETagIfNecessary(eTag); - } - while (requestedETags.hasMoreElements()) { - // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 - for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) { - // only consider "lost updates" checks for unsafe HTTP methods - if (requestedETag.isWildcard() && StringUtils.hasLength(eTag) - && !SAFE_METHODS.contains(getRequest().getMethod())) { - return false; - } - if (weakCompare) { - if (etagWeakMatch(eTag, requestedETag.formattedTag())) { + private boolean matchRequestedETags(Enumeration requestedETags, @Nullable String tag, boolean weakCompare) { + if (StringUtils.hasLength(tag)) { + ETag eTag = ETag.create(tag); + boolean isNotSafeMethod = !SAFE_METHODS.contains(getRequest().getMethod()); + while (requestedETags.hasMoreElements()) { + // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 + for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) { + // only consider "lost updates" checks for unsafe HTTP methods + if (requestedETag.isWildcard() && isNotSafeMethod) { return false; } - } - else { - if (etagStrongMatch(eTag, requestedETag.formattedTag())) { + if (requestedETag.compare(eTag, !weakCompare)) { return false; } } @@ -274,26 +267,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return true; } - private boolean etagStrongMatch(@Nullable String first, @Nullable String second) { - if (!StringUtils.hasLength(first) || first.startsWith("W/")) { - return false; - } - return first.equals(second); - } - - private boolean etagWeakMatch(@Nullable String first, @Nullable String second) { - if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) { - return false; - } - if (first.startsWith("W/")) { - first = first.substring(2); - } - if (second.startsWith("W/")) { - second = second.substring(2); - } - return first.equals(second); - } - private void updateResponseStateChanging(@Nullable String etag, long lastModifiedTimestamp) { if (this.notModified && getResponse() != null) { getResponse().setStatus(HttpStatus.PRECONDITION_FAILED.value()); diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 49f64e6223..0500180897 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -330,10 +330,11 @@ public class DefaultServerWebExchange implements ServerWebExchange { if (SAFE_METHODS.contains(getRequest().getMethod())) { return false; } - if (CollectionUtils.isEmpty(getRequestHeaders().get(HttpHeaders.IF_MATCH))) { + List values = getRequestHeaders().getOrEmpty(HttpHeaders.IF_MATCH); + if (CollectionUtils.isEmpty(values)) { return false; } - this.notModified = matchRequestedETags(getRequestHeaders().getIfMatch(), eTag, false); + this.notModified = matchRequestedETags(values, eTag, false); } catch (IllegalArgumentException ex) { return false; @@ -341,51 +342,25 @@ public class DefaultServerWebExchange implements ServerWebExchange { return true; } - private boolean matchRequestedETags(List requestedETags, @Nullable String eTag, boolean weakCompare) { - if (StringUtils.hasLength(eTag)) { - eTag = ETag.quoteETagIfNecessary(eTag); - } - for (String clientEtag : requestedETags) { - // only consider "lost updates" checks for unsafe HTTP methods - if ("*".equals(clientEtag) && StringUtils.hasLength(eTag) - && !SAFE_METHODS.contains(getRequest().getMethod())) { - return false; - } - // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 - if (weakCompare) { - if (eTagWeakMatch(eTag, clientEtag)) { - return false; - } - } - else { - if (eTagStrongMatch(eTag, clientEtag)) { - return false; + private boolean matchRequestedETags(List requestedETagValues, @Nullable String tag, boolean weakCompare) { + if (StringUtils.hasLength(tag)) { + ETag eTag = ETag.create(tag); + boolean isNotSafeMethod = !SAFE_METHODS.contains(getRequest().getMethod()); + for (String eTagValue : requestedETagValues) { + for (ETag requestedETag : ETag.parse(eTagValue)) { + // only consider "lost updates" checks for unsafe HTTP methods + if (requestedETag.isWildcard() && isNotSafeMethod) { + return false; + } + if (requestedETag.compare(eTag, !weakCompare)) { + return false; + } } } } return true; } - private boolean eTagStrongMatch(@Nullable String first, @Nullable String second) { - if (!StringUtils.hasLength(first) || first.startsWith("W/")) { - return false; - } - return first.equals(second); - } - - private boolean eTagWeakMatch(@Nullable String first, @Nullable String second) { - if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) { - return false; - } - if (first.startsWith("W/")) { - first = first.substring(2); - } - if (second.startsWith("W/")) { - second = second.substring(2); - } - return first.equals(second); - } - private void updateResponseStateChanging(@Nullable String eTag, Instant lastModified) { if (this.notModified) { getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED); @@ -400,7 +375,8 @@ public class DefaultServerWebExchange implements ServerWebExchange { if (CollectionUtils.isEmpty(getRequestHeaders().get(HttpHeaders.IF_NONE_MATCH))) { return false; } - this.notModified = !matchRequestedETags(getRequestHeaders().getIfNoneMatch(), eTag, true); + List values = getRequestHeaders().getOrEmpty(HttpHeaders.IF_NONE_MATCH); + this.notModified = !matchRequestedETags(values, eTag, true); } catch (IllegalArgumentException ex) { return false;