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 c7a49a82748..29d8cfee90c 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. @@ -86,9 +86,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/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/WebSocketHandler.java index 0703dae330d..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,65 +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: + *

* - *

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 -> {
- * 				// ...
- * 			})
- * 			.concatMap(message -> {
- * 				// ...
- * 			})
- * 			.map(value -> 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 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")); + } + +}