Skip to content

Commit 33fef8d

Browse files
committed
Merge branch '6.2.x'
2 parents 8a7d20f + f62251a commit 33fef8d

File tree

4 files changed

+105
-35
lines changed

4 files changed

+105
-35
lines changed

spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ private String appendQueryParams(
465465

466466
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate);
467467
for (Map.Entry<String, List<String>> entry : requestParams.entrySet()) {
468-
String nameVar = entry.getKey().replace(":", "%3A"); // suppress treatment as regex
468+
String nameVar = "queryParam-" + entry.getKey().replace(":", "%3A"); // suppress treatment as regex
469469
uriVars.put(nameVar, entry.getKey());
470470
for (int j = 0; j < entry.getValue().size(); j++) {
471471
String valueVar = nameVar + "[" + j + "]";

spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java

+13-11
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,19 @@ void queryParamsWithUriTemplate() {
7979

8080
assertThat(uriTemplate)
8181
.isEqualTo("/path?" +
82-
"{param1}={param1[0]}&" +
83-
"{param2}={param2[0]}&" +
84-
"{param2}={param2[1]}");
82+
"{queryParam-param1}={queryParam-param1[0]}&" +
83+
"{queryParam-param2}={queryParam-param2[0]}&" +
84+
"{queryParam-param2}={queryParam-param2[1]}");
8585

8686
assertThat(requestValues.getUriVariables())
87-
.containsOnlyKeys("param1", "param2", "param1[0]", "param2[0]", "param2[1]")
88-
.containsEntry("param1", "param1")
89-
.containsEntry("param2", "param2")
90-
.containsEntry("param1[0]", "1st value")
91-
.containsEntry("param2[0]", "2nd value A")
92-
.containsEntry("param2[1]", "2nd value B");
87+
.containsOnlyKeys(
88+
"queryParam-param1", "queryParam-param2", "queryParam-param1[0]",
89+
"queryParam-param2[0]", "queryParam-param2[1]")
90+
.containsEntry("queryParam-param1", "param1")
91+
.containsEntry("queryParam-param2", "param2")
92+
.containsEntry("queryParam-param1[0]", "1st value")
93+
.containsEntry("queryParam-param2[0]", "2nd value A")
94+
.containsEntry("queryParam-param2[1]", "2nd value B");
9395

9496
URI uri = UriComponentsBuilder.fromUriString(uriTemplate)
9597
.encode()
@@ -107,7 +109,7 @@ void queryParamWithSemicolon() {
107109
.build();
108110

109111
String uriTemplate = requestValues.getUriTemplate();
110-
assertThat(uriTemplate).isEqualTo("/path?{userId%3Aeq}={userId%3Aeq[0]}");
112+
assertThat(uriTemplate).isEqualTo("/path?{queryParam-userId%3Aeq}={queryParam-userId%3Aeq[0]}");
111113

112114
URI uri = UriComponentsBuilder.fromUriString(uriTemplate)
113115
.encode()
@@ -162,7 +164,7 @@ void requestPartAndRequestParam() {
162164
String uriTemplate = requestValues.getUriTemplate();
163165
assertThat(uriTemplate).isNotNull();
164166

165-
assertThat(uriTemplate).isEqualTo("/path?{query param}={query param[0]}");
167+
assertThat(uriTemplate).isEqualTo("/path?{queryParam-query param}={queryParam-query param[0]}");
166168

167169
URI uri = UriComponentsBuilder.fromUriString(uriTemplate)
168170
.encode()

spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,15 @@
1616

1717
package org.springframework.web.service.invoker;
1818

19+
import java.net.URI;
20+
1921
import org.jspecify.annotations.Nullable;
2022
import org.junit.jupiter.api.Test;
2123

2224
import org.springframework.web.bind.annotation.PathVariable;
25+
import org.springframework.web.bind.annotation.RequestParam;
2326
import org.springframework.web.service.annotation.GetExchange;
27+
import org.springframework.web.util.DefaultUriBuilderFactory;
2428

2529
import static org.assertj.core.api.Assertions.assertThat;
2630

@@ -46,6 +50,17 @@ void pathVariable() {
4650
assertPathVariable("id", "test");
4751
}
4852

53+
@Test // gh-34499
54+
void pathVariableAndRequestParamWithSameName() {
55+
this.service.executeWithPathVarAndRequestParam("{transfer-id}", "aValue");
56+
57+
assertPathVariable("transfer-id", "{transfer-id}");
58+
59+
HttpRequestValues values = this.client.getRequestValues();
60+
URI uri = (new DefaultUriBuilderFactory()).expand(values.getUriTemplate(), values.getUriVariables());
61+
assertThat(uri.toString()).isEqualTo("/transfers/%7Btransfer-id%7D?transfer-id=aValue");
62+
}
63+
4964
@SuppressWarnings("SameParameterValue")
5065
private void assertPathVariable(String name, @Nullable String expectedValue) {
5166
assertThat(this.client.getRequestValues().getUriVariables().get(name)).isEqualTo(expectedValue);
@@ -57,6 +72,11 @@ private interface Service {
5772
@GetExchange
5873
void execute(@PathVariable String id);
5974

75+
@GetExchange("/transfers/{transfer-id}")
76+
void executeWithPathVarAndRequestParam(
77+
@PathVariable("transfer-id") String transferId,
78+
@RequestParam("transfer-id") String transferIdParam);
79+
6080
}
6181

6282
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java

+70-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@
2121
import java.util.LinkedHashSet;
2222
import java.util.List;
2323
import java.util.Set;
24-
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.concurrent.atomic.AtomicReference;
2525
import java.util.function.Consumer;
2626

2727
import org.jspecify.annotations.Nullable;
@@ -72,20 +72,19 @@ public class ResponseBodyEmitter {
7272

7373
private @Nullable Handler handler;
7474

75+
private final AtomicReference<State> state = new AtomicReference<>(State.START);
76+
7577
/** Store send data before handler is initialized. */
7678
private final Set<DataWithMediaType> earlySendAttempts = new LinkedHashSet<>(8);
7779

78-
/** Store successful completion before the handler is initialized. */
79-
private final AtomicBoolean complete = new AtomicBoolean();
80-
8180
/** Store an error before the handler is initialized. */
8281
private @Nullable Throwable failure;
8382

84-
private final DefaultCallback timeoutCallback = new DefaultCallback();
83+
private final TimeoutCallback timeoutCallback = new TimeoutCallback();
8584

8685
private final ErrorCallback errorCallback = new ErrorCallback();
8786

88-
private final DefaultCallback completionCallback = new DefaultCallback();
87+
private final CompletionCallback completionCallback = new CompletionCallback();
8988

9089

9190
/**
@@ -125,7 +124,7 @@ synchronized void initialize(Handler handler) throws IOException {
125124
this.earlySendAttempts.clear();
126125
}
127126

128-
if (this.complete.get()) {
127+
if (this.state.get() == State.COMPLETE) {
129128
if (this.failure != null) {
130129
this.handler.completeWithError(this.failure);
131130
}
@@ -141,7 +140,7 @@ synchronized void initialize(Handler handler) throws IOException {
141140
}
142141

143142
void initializeWithError(Throwable ex) {
144-
if (this.complete.compareAndSet(false, true)) {
143+
if (this.state.compareAndSet(State.START, State.COMPLETE)) {
145144
this.failure = ex;
146145
this.earlySendAttempts.clear();
147146
this.errorCallback.accept(ex);
@@ -183,8 +182,7 @@ public void send(Object object) throws IOException {
183182
* @throws java.lang.IllegalStateException wraps any other errors
184183
*/
185184
public synchronized void send(Object object, @Nullable MediaType mediaType) throws IOException {
186-
Assert.state(!this.complete.get(), () -> "ResponseBodyEmitter has already completed" +
187-
(this.failure != null ? " with error: " + this.failure : ""));
185+
assertNotComplete();
188186
if (this.handler != null) {
189187
try {
190188
this.handler.send(object, mediaType);
@@ -211,11 +209,15 @@ public synchronized void send(Object object, @Nullable MediaType mediaType) thro
211209
* @since 6.0.12
212210
*/
213211
public synchronized void send(Set<DataWithMediaType> items) throws IOException {
214-
Assert.state(!this.complete.get(), () -> "ResponseBodyEmitter has already completed" +
215-
(this.failure != null ? " with error: " + this.failure : ""));
212+
assertNotComplete();
216213
sendInternal(items);
217214
}
218215

216+
private void assertNotComplete() {
217+
Assert.state(this.state.get() == State.START, () -> "ResponseBodyEmitter has already completed" +
218+
(this.failure != null ? " with error: " + this.failure : ""));
219+
}
220+
219221
private void sendInternal(Set<DataWithMediaType> items) throws IOException {
220222
if (items.isEmpty()) {
221223
return;
@@ -245,7 +247,7 @@ private void sendInternal(Set<DataWithMediaType> items) throws IOException {
245247
* related events such as an error while {@link #send(Object) sending}.
246248
*/
247249
public void complete() {
248-
if (this.complete.compareAndSet(false, true) && this.handler != null) {
250+
if (trySetComplete() && this.handler != null) {
249251
this.handler.complete();
250252
}
251253
}
@@ -262,14 +264,19 @@ public void complete() {
262264
* {@link #send(Object) sending}.
263265
*/
264266
public void completeWithError(Throwable ex) {
265-
if (this.complete.compareAndSet(false, true)) {
267+
if (trySetComplete()) {
266268
this.failure = ex;
267269
if (this.handler != null) {
268270
this.handler.completeWithError(ex);
269271
}
270272
}
271273
}
272274

275+
private boolean trySetComplete() {
276+
return (this.state.compareAndSet(State.START, State.COMPLETE) ||
277+
(this.state.compareAndSet(State.TIMEOUT, State.COMPLETE)));
278+
}
279+
273280
/**
274281
* Register code to invoke when the async request times out. This method is
275282
* called from a container thread when an async request times out.
@@ -364,7 +371,7 @@ public Object getData() {
364371
}
365372

366373

367-
private class DefaultCallback implements Runnable {
374+
private class TimeoutCallback implements Runnable {
368375

369376
private final List<Runnable> delegates = new ArrayList<>(1);
370377

@@ -374,9 +381,10 @@ public synchronized void addDelegate(Runnable delegate) {
374381

375382
@Override
376383
public void run() {
377-
ResponseBodyEmitter.this.complete.compareAndSet(false, true);
378-
for (Runnable delegate : this.delegates) {
379-
delegate.run();
384+
if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.TIMEOUT)) {
385+
for (Runnable delegate : this.delegates) {
386+
delegate.run();
387+
}
380388
}
381389
}
382390
}
@@ -392,11 +400,51 @@ public synchronized void addDelegate(Consumer<Throwable> callback) {
392400

393401
@Override
394402
public void accept(Throwable t) {
395-
ResponseBodyEmitter.this.complete.compareAndSet(false, true);
396-
for(Consumer<Throwable> delegate : this.delegates) {
397-
delegate.accept(t);
403+
if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.COMPLETE)) {
404+
for (Consumer<Throwable> delegate : this.delegates) {
405+
delegate.accept(t);
406+
}
407+
}
408+
}
409+
}
410+
411+
412+
private class CompletionCallback implements Runnable {
413+
414+
private final List<Runnable> delegates = new ArrayList<>(1);
415+
416+
public synchronized void addDelegate(Runnable delegate) {
417+
this.delegates.add(delegate);
418+
}
419+
420+
@Override
421+
public void run() {
422+
if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.COMPLETE)) {
423+
for (Runnable delegate : this.delegates) {
424+
delegate.run();
425+
}
398426
}
399427
}
400428
}
401429

430+
431+
/**
432+
* Represents a state for {@link ResponseBodyEmitter}.
433+
* <p><pre>
434+
* START ----+
435+
* | |
436+
* v |
437+
* TIMEOUT |
438+
* | |
439+
* v |
440+
* COMPLETE <--+
441+
* </pre>
442+
* @since 6.2.4
443+
*/
444+
private enum State {
445+
START,
446+
TIMEOUT, // handling a timeout
447+
COMPLETE
448+
}
449+
402450
}

0 commit comments

Comments
 (0)