diff --git a/spring-web/src/main/java/org/springframework/http/ETag.java b/spring-web/src/main/java/org/springframework/http/ETag.java
new file mode 100644
index 000000000000..c4d670d50cbc
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/ETag.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Represents an ETag for HTTP conditional requests.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.3.38
+ * @see RFC 7232
+ */
+public class ETag {
+
+ private static final Log logger = LogFactory.getLog(ETag.class);
+
+ private static final ETag WILDCARD = new ETag("*", false);
+
+
+ private final String tag;
+
+ private final boolean weak;
+
+
+ public ETag(String tag, boolean weak) {
+ this.tag = tag;
+ this.weak = weak;
+ }
+
+
+ public String tag() {
+ return this.tag;
+ }
+
+ public boolean weak() {
+ return this.weak;
+ }
+
+ /**
+ * Whether this a wildcard tag matching to any entity tag value.
+ */
+ public boolean isWildcard() {
+ return (this == WILDCARD);
+ }
+
+ /**
+ * Return the fully formatted tag including "W/" prefix and quotes.
+ */
+ public String formattedTag() {
+ if (this == WILDCARD) {
+ return "*";
+ }
+ return (this.weak ? "W/" : "") + "\"" + this.tag + "\"";
+ }
+
+ @Override
+ public String toString() {
+ return formattedTag();
+ }
+
+
+ /**
+ * Parse entity tags from an "If-Match" or "If-None-Match" header.
+ * @param source the source string to parse
+ * @return the parsed ETags
+ */
+ public static List parse(String source) {
+
+ List result = new ArrayList<>();
+ State state = State.BEFORE_QUOTES;
+ int startIndex = -1;
+ boolean weak = false;
+
+ for (int i = 0; i < source.length(); i++) {
+ char c = source.charAt(i);
+
+ if (state == State.IN_QUOTES) {
+ if (c == '"') {
+ String tag = source.substring(startIndex, i);
+ if (StringUtils.hasText(tag)) {
+ result.add(new ETag(tag, weak));
+ }
+ state = State.AFTER_QUOTES;
+ startIndex = -1;
+ weak = false;
+ }
+ continue;
+ }
+
+ if (Character.isWhitespace(c)) {
+ continue;
+ }
+
+ if (c == ',') {
+ state = State.BEFORE_QUOTES;
+ continue;
+ }
+
+ if (state == State.BEFORE_QUOTES) {
+ if (c == '*') {
+ result.add(WILDCARD);
+ state = State.AFTER_QUOTES;
+ continue;
+ }
+ if (c == '"') {
+ state = State.IN_QUOTES;
+ startIndex = i + 1;
+ continue;
+ }
+ if (c == 'W' && source.length() > i + 2) {
+ if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') {
+ state = State.IN_QUOTES;
+ i = i + 2;
+ startIndex = i + 1;
+ weak = true;
+ continue;
+ }
+ }
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Unexpected char at index " + i);
+ }
+ }
+
+ if (state != State.IN_QUOTES && logger.isDebugEnabled()) {
+ logger.debug("Expected closing '\"'");
+ }
+
+ return result;
+ }
+
+
+ private enum State {
+
+ BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES
+
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
index cb1dee45bbaa..5fe14a360be4 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
@@ -40,8 +40,6 @@
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
@@ -393,12 +391,6 @@ public class HttpHeaders implements MultiValueMap, Serializable
*/
public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>());
- /**
- * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
- * @see Section 2.3 of RFC 7232
- */
- private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
-
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH);
private static final ZoneId GMT = ZoneId.of("GMT");
@@ -1568,35 +1560,27 @@ public void clearContentHeaders() {
/**
* Retrieve a combined result from the field values of the ETag header.
- * @param headerName the header name
+ * @param name the header name
* @return the combined result
* @throws IllegalArgumentException if parsing fails
* @since 4.3
*/
- protected List getETagValuesAsList(String headerName) {
- List values = get(headerName);
- if (values != null) {
- List result = new ArrayList<>();
- for (String value : values) {
- if (value != null) {
- Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value);
- while (matcher.find()) {
- if ("*".equals(matcher.group())) {
- result.add(matcher.group());
- }
- else {
- result.add(matcher.group(1));
- }
- }
- if (result.isEmpty()) {
- throw new IllegalArgumentException(
- "Could not parse header '" + headerName + "' with value '" + value + "'");
- }
+ protected List getETagValuesAsList(String name) {
+ List values = get(name);
+ if (values == null) {
+ return Collections.emptyList();
+ }
+ List result = new ArrayList<>();
+ for (String value : values) {
+ if (value != null) {
+ List tags = ETag.parse(value);
+ Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'");
+ for (ETag tag : tags) {
+ result.add(tag.formattedTag());
}
}
- return result;
}
- return Collections.emptyList();
+ return result;
}
/**
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 27ea338ee2dd..8bb3b4817f6b 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,13 +26,12 @@
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import org.springframework.http.ETag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -54,12 +53,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
private static final List SAFE_METHODS = Arrays.asList("GET", "HEAD");
- /**
- * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
- * @see Section 2.3 of RFC 7232
- */
- private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
-
/**
* Date formats as specified in the HTTP RFC.
* @see Section 7.1.1.1 of RFC 7231
@@ -289,11 +282,10 @@ private boolean validateIfNoneMatch(@Nullable String etag) {
etag = etag.substring(2);
}
while (ifNoneMatch.hasMoreElements()) {
- String clientETags = ifNoneMatch.nextElement();
- Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
- while (etagMatcher.find()) {
- if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
+ for (ETag requestedETag : ETag.parse(ifNoneMatch.nextElement())) {
+ String tag = requestedETag.tag();
+ if (StringUtils.hasLength(tag) && etag.equals(padEtagIfNecessary(tag))) {
this.notModified = true;
break;
}