Skip to content

Commit 9d57c0b

Browse files
committed
Add compare method to ETag
Closes gh-33385
1 parent 19700d0 commit 9d57c0b

File tree

3 files changed

+82
-83
lines changed

3 files changed

+82
-83
lines changed

spring-web/src/main/java/org/springframework/http/ETag.java

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,44 @@ public boolean isWildcard() {
4747
return (this == WILDCARD);
4848
}
4949

50+
/**
51+
* Perform a strong or weak comparison to another {@link ETag}.
52+
* @param other the ETag to compare to
53+
* @param strong whether to perform strong or weak comparison
54+
* @return whether there is a match or not
55+
* @since 6.2
56+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3.2">RFC 9110, Section 8.8.3.2</a>
57+
*/
58+
public boolean compare(ETag other, boolean strong) {
59+
if (!StringUtils.hasLength(tag()) || !StringUtils.hasLength(other.tag())) {
60+
return false;
61+
}
62+
63+
if (strong && (weak() || other.weak())) {
64+
return false;
65+
}
66+
67+
return tag().equals(other.tag());
68+
}
69+
70+
@Override
71+
public boolean equals(Object other) {
72+
return (this == other ||
73+
(other instanceof ETag oet && this.tag.equals(oet.tag) && this.weak == oet.weak));
74+
}
75+
76+
@Override
77+
public int hashCode() {
78+
int result = this.tag.hashCode();
79+
result = 31 * result + Boolean.hashCode(this.weak);
80+
return result;
81+
}
82+
83+
@Override
84+
public String toString() {
85+
return formattedTag();
86+
}
87+
5088
/**
5189
* Return the fully formatted tag including "W/" prefix and quotes.
5290
*/
@@ -57,11 +95,23 @@ public String formattedTag() {
5795
return (this.weak ? "W/" : "") + "\"" + this.tag + "\"";
5896
}
5997

60-
@Override
61-
public String toString() {
62-
return formattedTag();
63-
}
6498

99+
/**
100+
* Create an {@link ETag} instance from a String representation.
101+
* @param rawValue the formatted ETag value
102+
* @return the created instance
103+
* @since 6.2
104+
*/
105+
public static ETag create(String rawValue) {
106+
boolean weak = rawValue.startsWith("W/");
107+
if (weak) {
108+
rawValue = rawValue.substring(2);
109+
}
110+
if (rawValue.length() > 2 && rawValue.startsWith("\"") && rawValue.endsWith("\"")) {
111+
rawValue = rawValue.substring(1, rawValue.length() - 1);
112+
}
113+
return new ETag(rawValue, weak);
114+
}
65115

66116
/**
67117
* Parse entity tags from an "If-Match" or "If-None-Match" header.

spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -247,25 +247,18 @@ private boolean validateIfNoneMatch(@Nullable String etag) {
247247
return true;
248248
}
249249

250-
private boolean matchRequestedETags(Enumeration<String> requestedETags, @Nullable String eTag, boolean weakCompare) {
251-
if (StringUtils.hasLength(eTag)) {
252-
eTag = ETag.quoteETagIfNecessary(eTag);
253-
}
254-
while (requestedETags.hasMoreElements()) {
255-
// Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
256-
for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) {
257-
// only consider "lost updates" checks for unsafe HTTP methods
258-
if (requestedETag.isWildcard() && StringUtils.hasLength(eTag)
259-
&& !SAFE_METHODS.contains(getRequest().getMethod())) {
260-
return false;
261-
}
262-
if (weakCompare) {
263-
if (etagWeakMatch(eTag, requestedETag.formattedTag())) {
250+
private boolean matchRequestedETags(Enumeration<String> requestedETags, @Nullable String tag, boolean weakCompare) {
251+
if (StringUtils.hasLength(tag)) {
252+
ETag eTag = ETag.create(tag);
253+
boolean isNotSafeMethod = !SAFE_METHODS.contains(getRequest().getMethod());
254+
while (requestedETags.hasMoreElements()) {
255+
// Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
256+
for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) {
257+
// only consider "lost updates" checks for unsafe HTTP methods
258+
if (requestedETag.isWildcard() && isNotSafeMethod) {
264259
return false;
265260
}
266-
}
267-
else {
268-
if (etagStrongMatch(eTag, requestedETag.formattedTag())) {
261+
if (requestedETag.compare(eTag, !weakCompare)) {
269262
return false;
270263
}
271264
}
@@ -274,26 +267,6 @@ private boolean matchRequestedETags(Enumeration<String> requestedETags, @Nullabl
274267
return true;
275268
}
276269

277-
private boolean etagStrongMatch(@Nullable String first, @Nullable String second) {
278-
if (!StringUtils.hasLength(first) || first.startsWith("W/")) {
279-
return false;
280-
}
281-
return first.equals(second);
282-
}
283-
284-
private boolean etagWeakMatch(@Nullable String first, @Nullable String second) {
285-
if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) {
286-
return false;
287-
}
288-
if (first.startsWith("W/")) {
289-
first = first.substring(2);
290-
}
291-
if (second.startsWith("W/")) {
292-
second = second.substring(2);
293-
}
294-
return first.equals(second);
295-
}
296-
297270
private void updateResponseStateChanging(@Nullable String etag, long lastModifiedTimestamp) {
298271
if (this.notModified && getResponse() != null) {
299272
getResponse().setStatus(HttpStatus.PRECONDITION_FAILED.value());

spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -330,62 +330,37 @@ private boolean validateIfMatch(@Nullable String eTag) {
330330
if (SAFE_METHODS.contains(getRequest().getMethod())) {
331331
return false;
332332
}
333-
if (CollectionUtils.isEmpty(getRequestHeaders().get(HttpHeaders.IF_MATCH))) {
333+
List<String> values = getRequestHeaders().getOrEmpty(HttpHeaders.IF_MATCH);
334+
if (CollectionUtils.isEmpty(values)) {
334335
return false;
335336
}
336-
this.notModified = matchRequestedETags(getRequestHeaders().getIfMatch(), eTag, false);
337+
this.notModified = matchRequestedETags(values, eTag, false);
337338
}
338339
catch (IllegalArgumentException ex) {
339340
return false;
340341
}
341342
return true;
342343
}
343344

344-
private boolean matchRequestedETags(List<String> requestedETags, @Nullable String eTag, boolean weakCompare) {
345-
if (StringUtils.hasLength(eTag)) {
346-
eTag = ETag.quoteETagIfNecessary(eTag);
347-
}
348-
for (String clientEtag : requestedETags) {
349-
// only consider "lost updates" checks for unsafe HTTP methods
350-
if ("*".equals(clientEtag) && StringUtils.hasLength(eTag)
351-
&& !SAFE_METHODS.contains(getRequest().getMethod())) {
352-
return false;
353-
}
354-
// Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
355-
if (weakCompare) {
356-
if (eTagWeakMatch(eTag, clientEtag)) {
357-
return false;
358-
}
359-
}
360-
else {
361-
if (eTagStrongMatch(eTag, clientEtag)) {
362-
return false;
345+
private boolean matchRequestedETags(List<String> requestedETagValues, @Nullable String tag, boolean weakCompare) {
346+
if (StringUtils.hasLength(tag)) {
347+
ETag eTag = ETag.create(tag);
348+
boolean isNotSafeMethod = !SAFE_METHODS.contains(getRequest().getMethod());
349+
for (String eTagValue : requestedETagValues) {
350+
for (ETag requestedETag : ETag.parse(eTagValue)) {
351+
// only consider "lost updates" checks for unsafe HTTP methods
352+
if (requestedETag.isWildcard() && isNotSafeMethod) {
353+
return false;
354+
}
355+
if (requestedETag.compare(eTag, !weakCompare)) {
356+
return false;
357+
}
363358
}
364359
}
365360
}
366361
return true;
367362
}
368363

369-
private boolean eTagStrongMatch(@Nullable String first, @Nullable String second) {
370-
if (!StringUtils.hasLength(first) || first.startsWith("W/")) {
371-
return false;
372-
}
373-
return first.equals(second);
374-
}
375-
376-
private boolean eTagWeakMatch(@Nullable String first, @Nullable String second) {
377-
if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) {
378-
return false;
379-
}
380-
if (first.startsWith("W/")) {
381-
first = first.substring(2);
382-
}
383-
if (second.startsWith("W/")) {
384-
second = second.substring(2);
385-
}
386-
return first.equals(second);
387-
}
388-
389364
private void updateResponseStateChanging(@Nullable String eTag, Instant lastModified) {
390365
if (this.notModified) {
391366
getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
@@ -400,7 +375,8 @@ private boolean validateIfNoneMatch(@Nullable String eTag) {
400375
if (CollectionUtils.isEmpty(getRequestHeaders().get(HttpHeaders.IF_NONE_MATCH))) {
401376
return false;
402377
}
403-
this.notModified = !matchRequestedETags(getRequestHeaders().getIfNoneMatch(), eTag, true);
378+
List<String> values = getRequestHeaders().getOrEmpty(HttpHeaders.IF_NONE_MATCH);
379+
this.notModified = !matchRequestedETags(values, eTag, true);
404380
}
405381
catch (IllegalArgumentException ex) {
406382
return false;

0 commit comments

Comments
 (0)