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")); + } + +}