From bd7007227c12490a6a4da96afa09c1f2637aa6d9 Mon Sep 17 00:00:00 2001 From: Artur Date: Fri, 25 Apr 2025 14:43:27 +0300 Subject: [PATCH 1/3] Provide a working example instead of unclear placeholders See gh-34828 Signed-off-by: Artur --- .../web/reactive/socket/WebSocketHandler.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java index 0703dae330d..ab6c8aa2eca 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java @@ -45,13 +45,26 @@ import reactor.core.publisher.Mono; * public Mono<Void> handle(WebSocketSession session) { * * Flux<WebSocketMessage> output = session.receive() - * .doOnNext(message -> { - * // ... + * .doOnNext(message -> { + * // This is for side effects such as + * // - Logging incoming messages + * // - Updating some metrics or counters + * // - Performing access checks or validations (non-blocking) + * System.out.println("Got message: " + message.getPayloadAsText()); * }) * .concatMap(message -> { - * // ... + * // This is where you handle the actual processing of the incoming message. It + * // might involve: + * // - Parsing the message content (e.g., JSON parsing) + * // - Invoking a reactive service (e.g., database, HTTP call, etc.) + * // - Returning a transformed value, typically a Mono<String> or Mono<SomeType> + * // if you're mapping to another data format + * return Mono.just(message.getPayloadAsText()); * }) - * .map(value -> session.textMessage("Echo " + value)); + * .map(value -> { + * // This is where you produce one or more responses for the message + * return session.textMessage("Echo " + value)); + * }); * * return session.send(output); * } From ac773d97e944a350483ed9d2f18f7176d2412697 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 29 Apr 2025 16:43:27 +0300 Subject: [PATCH 2/3] Polishing contribution Closes gh-34828 --- .../web/reactive/socket/WebSocketHandler.java | 109 ++++++++---------- 1 file changed, 50 insertions(+), 59 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java index ab6c8aa2eca..ead8608fe81 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -23,78 +23,69 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; /** - * Handler for a WebSocket session. - * - *

A server {@code WebSocketHandler} is mapped to requests with + * Handler for a WebSocket messages. You can use it as follows: + *

    + *
  • On the server side, {@code WebSocketHandler} is mapped to requests with * {@link org.springframework.web.reactive.handler.SimpleUrlHandlerMapping * SimpleUrlHandlerMapping} and * {@link org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter - * WebSocketHandlerAdapter}. A client {@code WebSocketHandler} is passed to the + * WebSocketHandlerAdapter}. + *
  • On the client side, {@code WebSocketHandler} is passed into the * {@link org.springframework.web.reactive.socket.client.WebSocketClient * WebSocketClient} execute method. + *
* - *

Use {@link WebSocketSession#receive() session.receive()} to compose on - * the inbound message stream, and {@link WebSocketSession#send(Publisher) - * session.send(publisher)} for the outbound message stream. Below is an - * example, combined flow to process inbound and to send outbound messages: + *

{@link WebSocketSession#receive() session.receive()} handles inbound + * messages, while {@link WebSocketSession#send(Publisher) session.send} + * sends outbound messages. Below is an example of handling inbound messages + * and responding to every message: * *

- * class ExampleHandler implements WebSocketHandler {
- *
- * 	@Override
- * 	public Mono<Void> handle(WebSocketSession session) {
- *
- * 		Flux<WebSocketMessage> output = session.receive()
- * 			.doOnNext(message -> {
- * 				// This is for side effects such as
- * 				// - Logging incoming messages
- * 				// - Updating some metrics or counters
- * 				// - Performing access checks or validations (non-blocking)
- * 				System.out.println("Got message: " + message.getPayloadAsText());
- * 			})
- * 			.concatMap(message -> {
- * 				// This is where you handle the actual processing of the incoming message. It
- * 				// might involve:
- * 				// - Parsing the message content (e.g., JSON parsing)
- * 				// - Invoking a reactive service (e.g., database, HTTP call, etc.)
- * 				// - Returning a transformed value, typically a Mono<String> or Mono<SomeType>
- * 				// if you're mapping to another data format
- * 				return Mono.just(message.getPayloadAsText());
- * 			})
- * 			.map(value -> {
- * 				// This is where you produce one or more responses for the message
- * 				return session.textMessage("Echo " + value));
- * 			});
- *
- * 		return session.send(output);
- * 	}
- * }
+ *	class ExampleHandler implements WebSocketHandler {
+ *
+ *		@Override
+ *		public Mono<Void> handle(WebSocketSession session) {
+ *			Flux<WebSocketMessage> output = session.receive()
+ * 				.doOnNext(message -> {
+ * 					// Imperative calls without a return value:
+ * 					// perform access checks, log, validate, update metrics.
+ * 					// ...
+ * 				})
+ * 				.concatMap(message -> {
+ * 					// Async, non-blocking calls:
+ * 					// parse messages, call a database, make remote calls.
+ * 					// Return the same message, or a transformed value
+ * 					// ...
+ * 				});
+ * 			return session.send(output);
+ *		}
+ *	}
  * 
* *

If processing inbound and sending outbound messages are independent * streams, they can be joined together with the "zip" operator: * *

- * class ExampleHandler implements WebSocketHandler {
- *
- * 	@Override
- * 	public Mono<Void> handle(WebSocketSession session) {
- *
- * 		Mono<Void> input = session.receive()
- *			.doOnNext(message -> {
- * 				// ...
- * 			})
- * 			.concatMap(message -> {
- * 				// ...
- * 			})
- * 			.then();
- *
- *		Flux<String> source = ... ;
- * 		Mono<Void> output = session.send(source.map(session::textMessage));
- *
- * 		return Mono.zip(input, output).then();
- * 	}
- * }
+ *	class ExampleHandler implements WebSocketHandler {
+ *
+ *		@Override
+ *		public Mono<Void> handle(WebSocketSession session) {
+ *
+ *			Mono<Void> input = session.receive()
+ *				.doOnNext(message -> {
+ * 					// ...
+ * 				})
+ * 				.concatMap(message -> {
+ * 					// ...
+ * 				})
+ * 				.then();
+ *
+ *			Flux<String> source = ... ;
+ * 			Mono<Void> output = session.send(source.map(session::textMessage));
+ *
+ * 			return Mono.zip(input, output).then();
+ *		}
+ *	}
  * 
* *

A {@code WebSocketHandler} must compose the inbound and outbound streams From c0679191739c7de90c6aeecbfc2e65e941b1bf5f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 2 May 2025 08:19:18 +0100 Subject: [PATCH 3/3] Ensure Fragment can merge attributes Use a new map when merging as the original may be immutable. Closes gh-34848 --- .../web/reactive/result/view/Fragment.java | 6 +-- .../reactive/result/view/FragmentTests.java | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentTests.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Fragment.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Fragment.java index f835b3f6805..446383eed9c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Fragment.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/Fragment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -91,9 +91,7 @@ public final class Fragment { if (CollectionUtils.isEmpty(model.asMap())) { return; } - if (this.model == null) { - this.model = new LinkedHashMap<>(); - } + this.model = new LinkedHashMap<>(this.model != null ? this.model : Collections.emptyMap()); model.asMap().forEach((key, value) -> this.model.putIfAbsent(key, value)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentTests.java new file mode 100644 index 00000000000..10edd3b5add --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2025 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.web.reactive.result.view; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.ui.ConcurrentModel; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link Fragment}. + * @author Rossen Stoyanchev + */ +public class FragmentTests { + + @Test + void mergeAttributes() { + Fragment fragment = Fragment.create("myView", Map.of("fruit", "apple")); + fragment.mergeAttributes(new ConcurrentModel("vegetable", "pepper")); + + assertThat(fragment.model()).containsExactly(Map.entry("fruit", "apple"), Map.entry("vegetable", "pepper")); + } + + @Test + void mergeAttributesCollision() { + Fragment fragment = Fragment.create("myView", Map.of("fruit", "apple")); + fragment.mergeAttributes(new ConcurrentModel("fruit", "orange")); + + assertThat(fragment.model()).containsExactly(Map.entry("fruit", "apple")); + } + +}