Skip to content

Commit 6206446

Browse files
committed
ETag
1 parent 4f607b5 commit 6206446

File tree

17 files changed

+246
-41
lines changed

17 files changed

+246
-41
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http;
18+
19+
import org.springframework.util.Assert;
20+
21+
/**
22+
* ETag header value holder.
23+
*
24+
* @author Riley Park
25+
* @since TODO
26+
* @param value value that uniquely represents the resource
27+
* @param weak if weak validation should be used
28+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag">ETag Header</a>
29+
*/
30+
public record ETag(
31+
String value,
32+
boolean weak
33+
) {
34+
35+
/**
36+
* ETag prefix.
37+
*
38+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives">ETag Header Directives</a>
39+
*/
40+
public static final String PREFIX = "\"";
41+
42+
/**
43+
* ETag prefix, with a weak validator.
44+
*
45+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives">ETag Header Directives</a>
46+
* @see <a href=https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#weak_validation">Weak Validation</a>
47+
*/
48+
public static final String PREFIX_WEAK = "W/\"";
49+
50+
/**
51+
* ETag suffix.
52+
*
53+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives">ETag Header Directives</a>
54+
*/
55+
public static final String SUFFIX = "\"";
56+
57+
/**
58+
* Parses an {@code ETag} header value as defined in RFC 7232.
59+
* @param etag the {@literal ETag} header value
60+
* @return the parsed content disposition
61+
* @see #toString()
62+
*/
63+
public static ETag parse(String etag) {
64+
boolean weak = etag.startsWith(PREFIX_WEAK);
65+
Assert.isTrue(etag.startsWith(PREFIX) || weak,
66+
"Invalid ETag: does not start with " + PREFIX + " or " + PREFIX_WEAK);
67+
Assert.isTrue(etag.endsWith(SUFFIX), "Invalid ETag: does not end with " + SUFFIX);
68+
int start = (weak ? PREFIX_WEAK.length() : PREFIX.length());
69+
String value = etag.substring(start, etag.length() - SUFFIX.length());
70+
return new ETag(value, weak);
71+
}
72+
73+
public String toHeaderValue() {
74+
return (weak ? PREFIX_WEAK : PREFIX) + value + SUFFIX;
75+
}
76+
77+
}

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,11 +1072,16 @@ public long getDate() {
10721072
* Set the (new) entity tag of the body, as specified by the {@code ETag} header.
10731073
*/
10741074
public void setETag(@Nullable String etag) {
1075+
setETag((etag != null) ? ETag.parse(etag) : null);
1076+
}
1077+
1078+
/**
1079+
* Set the (new) entity tag of the body, as specified by the {@code ETag} header.
1080+
* @since TODO
1081+
*/
1082+
public void setETag(@Nullable ETag etag) {
10751083
if (etag != null) {
1076-
Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/"),
1077-
"Invalid ETag: does not start with W/ or \"");
1078-
Assert.isTrue(etag.endsWith("\""), "Invalid ETag: does not end with \"");
1079-
set(ETAG, etag);
1084+
set(ETAG, etag.toHeaderValue());
10801085
}
10811086
else {
10821087
remove(ETAG);

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,15 @@ public interface HeadersBuilder<B extends HeadersBuilder<B>> {
406406
*/
407407
B eTag(@Nullable String etag);
408408

409+
/**
410+
* Set the entity tag of the body, as specified by the {@code ETag} header.
411+
* @since TODO
412+
* @param etag the new entity tag
413+
* @return this builder
414+
* @see HttpHeaders#setETag(ETag)
415+
*/
416+
B eTag(@Nullable ETag etag);
417+
409418
/**
410419
* Set the time the resource was last changed, as specified by the
411420
* {@code Last-Modified} header.
@@ -569,14 +578,12 @@ public BodyBuilder contentType(MediaType contentType) {
569578

570579
@Override
571580
public BodyBuilder eTag(@Nullable String etag) {
572-
if (etag != null) {
573-
if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) {
574-
etag = "\"" + etag;
575-
}
576-
if (!etag.endsWith("\"")) {
577-
etag = etag + "\"";
578-
}
579-
}
581+
this.headers.setETag(etag);
582+
return this;
583+
}
584+
585+
@Override
586+
public BodyBuilder eTag(@Nullable ETag etag) {
580587
this.headers.setETag(etag);
581588
return this;
582589
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.context.i18n.LocaleContext;
3434
import org.springframework.core.ResolvableType;
3535
import org.springframework.core.codec.Hints;
36+
import org.springframework.http.ETag;
3637
import org.springframework.http.HttpHeaders;
3738
import org.springframework.http.HttpMethod;
3839
import org.springframework.http.HttpStatus;
@@ -368,10 +369,10 @@ private String padEtagIfNecessary(@Nullable String etag) {
368369
if (!StringUtils.hasLength(etag)) {
369370
return etag;
370371
}
371-
if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
372+
if ((etag.startsWith(ETag.PREFIX) || etag.startsWith(ETag.PREFIX_WEAK)) && etag.endsWith(ETag.SUFFIX)) {
372373
return etag;
373374
}
374-
return "\"" + etag + "\"";
375+
return ETag.PREFIX + etag + ETag.SUFFIX;
375376
}
376377

377378
private boolean eTagStrongMatch(@Nullable String first, @Nullable String second) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
23+
24+
/**
25+
* Tests for {@link ETag}.
26+
* @author Riley Park
27+
*/
28+
class ETagTests {
29+
30+
@Test
31+
void parse() {
32+
String raw = "\"v2.6\"";
33+
ETag etag = ETag.parse(raw);
34+
assertThat(etag.value()).isEqualTo("v2.6");
35+
assertThat(etag.weak()).isFalse();
36+
assertThat(etag.toHeaderValue()).isEqualTo(raw);
37+
}
38+
39+
@Test
40+
void parseWeak() {
41+
String raw = "W/\"v2.6\"";
42+
ETag etag = ETag.parse(raw);
43+
assertThat(etag.value()).isEqualTo("v2.6");
44+
assertThat(etag.weak()).isTrue();
45+
assertThat(etag.toHeaderValue()).isEqualTo(raw);
46+
}
47+
48+
@Test
49+
void illegalETagWithoutQuoteAfterWSlash() {
50+
String raw = "W/v2.6\"";
51+
assertThatIllegalArgumentException().isThrownBy(() -> ETag.parse(raw));
52+
}
53+
54+
}

spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ void eTag() {
189189
assertThat(headers.getFirst("ETag")).as("Invalid ETag header").isEqualTo("\"v2.6\"");
190190
}
191191

192+
@Test
193+
void eTagW() {
194+
String eTag = "W\"v2.6\"";
195+
headers.setETag(eTag);
196+
assertThat(headers.getETag()).as("Invalid ETag header").isEqualTo(eTag);
197+
assertThat(headers.getFirst("ETag")).as("Invalid ETag header").isEqualTo("W\"v2.6\"");
198+
}
199+
192200
@Test
193201
void host() {
194202
InetSocketAddress host = InetSocketAddress.createUnresolved("localhost", 8080);
@@ -219,6 +227,12 @@ void illegalETag() {
219227
assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(eTag));
220228
}
221229

230+
@Test
231+
void illegalETagWithoutQuoteAfterWSlash() {
232+
String eTag = "W/v2.6\"";
233+
assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(eTag));
234+
}
235+
222236
@Test
223237
void ifMatch() {
224238
String ifMatch = "\"v2.6\"";

spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ void Etagheader() {
223223
responseEntity = ResponseEntity.ok().eTag("W/\"foo\"").build();
224224
assertThat(responseEntity.getHeaders().getETag()).isEqualTo("W/\"foo\"");
225225

226-
responseEntity = ResponseEntity.ok().eTag(null).build();
226+
responseEntity = ResponseEntity.ok().eTag((String) null).build();
227227
assertThat(responseEntity.getHeaders().getETag()).isNull();
228228
}
229229

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import org.springframework.core.codec.Hints;
3434
import org.springframework.http.CacheControl;
35+
import org.springframework.http.ETag;
3536
import org.springframework.http.HttpHeaders;
3637
import org.springframework.http.HttpMethod;
3738
import org.springframework.http.HttpStatus;
@@ -148,12 +149,12 @@ public EntityResponse.Builder<T> contentType(MediaType contentType) {
148149

149150
@Override
150151
public EntityResponse.Builder<T> eTag(String etag) {
151-
if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) {
152-
etag = "\"" + etag;
153-
}
154-
if (!etag.endsWith("\"")) {
155-
etag = etag + "\"";
156-
}
152+
this.headers.setETag(etag);
153+
return this;
154+
}
155+
156+
@Override
157+
public EntityResponse.Builder<T> eTag(ETag etag) {
157158
this.headers.setETag(etag);
158159
return this;
159160
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.core.ParameterizedTypeReference;
3838
import org.springframework.core.codec.Hints;
3939
import org.springframework.http.CacheControl;
40+
import org.springframework.http.ETag;
4041
import org.springframework.http.HttpHeaders;
4142
import org.springframework.http.HttpMethod;
4243
import org.springframework.http.HttpStatusCode;
@@ -148,12 +149,13 @@ public ServerResponse.BodyBuilder contentType(MediaType contentType) {
148149
@Override
149150
public ServerResponse.BodyBuilder eTag(String etag) {
150151
Assert.notNull(etag, "etag must not be null");
151-
if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) {
152-
etag = "\"" + etag;
153-
}
154-
if (!etag.endsWith("\"")) {
155-
etag = etag + "\"";
156-
}
152+
this.headers.setETag(etag);
153+
return this;
154+
}
155+
156+
@Override
157+
public ServerResponse.BodyBuilder eTag(ETag etag) {
158+
Assert.notNull(etag, "etag must not be null");
157159
this.headers.setETag(etag);
158160
return this;
159161
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.core.ParameterizedTypeReference;
3030
import org.springframework.http.CacheControl;
31+
import org.springframework.http.ETag;
3132
import org.springframework.http.HttpHeaders;
3233
import org.springframework.http.HttpMethod;
3334
import org.springframework.http.HttpStatusCode;
@@ -220,6 +221,15 @@ interface Builder<T> {
220221
*/
221222
Builder<T> eTag(String etag);
222223

224+
/**
225+
* Set the entity tag of the body, as specified by the {@code ETag} header.
226+
* @since TODO
227+
* @param etag the new entity tag
228+
* @return this builder
229+
* @see HttpHeaders#setETag(ETag)
230+
*/
231+
Builder<T> eTag(ETag etag);
232+
223233
/**
224234
* Set the time the resource was last changed, as specified by the
225235
* {@code Last-Modified} header.

0 commit comments

Comments
 (0)