Skip to content

Commit 1d8ebdf

Browse files
committed
Add reactive progressive rendering features to MustacheView
1 parent 4d8df3c commit 1d8ebdf

File tree

12 files changed

+400
-24
lines changed

12 files changed

+400
-24
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
.DS_Store
1111
.classpath
1212
.factorypath
13+
.attach_pid*
1314
.gradle
1415
.idea
1516
.metadata

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheReactiveWebConfiguration.java

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler,
3838
resolver.setPrefix(mustache.getPrefix());
3939
resolver.setSuffix(mustache.getSuffix());
4040
resolver.setViewNames(mustache.getViewNames());
41+
resolver.setCache(mustache.isCache());
4142
resolver.setRequestContextAttribute(mustache.getRequestContextAttribute());
4243
resolver.setCharset(mustache.getCharsetName());
4344
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationReactiveIntegrationTests.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package org.springframework.boot.autoconfigure.mustache;
1818

19+
import java.time.Duration;
1920
import java.util.Date;
2021

2122
import com.samskivert.mustache.Mustache;
23+
import org.hamcrest.Matchers;
2224
import org.junit.jupiter.api.Test;
25+
import reactor.core.publisher.Flux;
2326

2427
import org.springframework.beans.factory.annotation.Autowired;
2528
import org.springframework.boot.SpringApplication;
@@ -35,6 +38,7 @@
3538
import org.springframework.context.annotation.Bean;
3639
import org.springframework.context.annotation.Configuration;
3740
import org.springframework.context.annotation.Import;
41+
import org.springframework.http.MediaType;
3842
import org.springframework.stereotype.Controller;
3943
import org.springframework.test.web.reactive.server.WebTestClient;
4044
import org.springframework.ui.Model;
@@ -46,7 +50,7 @@
4650
* Integration Tests for {@link MustacheAutoConfiguration}, {@link MustacheViewResolver}
4751
* and {@link MustacheView}.
4852
*
49-
* @author Brian Clozel
53+
* @author Brian Clozel, Dave Syer
5054
*/
5155
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
5256
properties = "spring.main.web-application-type=reactive")
@@ -69,6 +73,14 @@ public void testPartialPage() {
6973
assertThat(result).contains("Hello App").contains("Hello World");
7074
}
7175

76+
@Test
77+
public void testSse() {
78+
this.client.get().uri("/sse").exchange() //
79+
.expectBody(String.class).value(Matchers.containsString("event: message"))
80+
.value(Matchers.containsString("\ndata: <span>Hello</span>"))
81+
.value(Matchers.containsString("World")).value(Matchers.endsWith("\n\n"));
82+
}
83+
7284
@Configuration(proxyBeanMethods = false)
7385
@Import({ ReactiveWebServerFactoryAutoConfiguration.class,
7486
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
@@ -92,6 +104,16 @@ public String layout(Model model) {
92104
return "partial";
93105
}
94106

107+
@RequestMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
108+
public String sse(Model model) {
109+
model.addAttribute("time", new Date());
110+
model.addAttribute("flux.message",
111+
Flux.just("<span>Hello</span>", "<span>World</span>")
112+
.delayElements(Duration.ofMillis(10)));
113+
model.addAttribute("title", "Hello App");
114+
return "sse";
115+
}
116+
95117
@Bean
96118
public MustacheViewResolver viewResolver() {
97119
Mustache.Compiler compiler = Mustache.compiler().withLoader(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{#flux.message}}
2+
event: message
3+
{{#ssedata}}
4+
<h2>Title</h2>
5+
{{{.}}}
6+
{{/ssedata}}
7+
{{/flux.message}}

spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

+48
Original file line numberDiff line numberDiff line change
@@ -3032,6 +3032,54 @@ Spring Boot includes auto-configuration support for the following templating eng
30323032
When you use one of these templating engines with the default configuration, your
30333033
templates are picked up automatically from `src/main/resources/templates`.
30343034

3035+
==== Mustache Views
3036+
3037+
There are some special features of the `MustacheView` that make it suitable for handling the rendering of reactive elements. Most browsers will start to show content before the HTML tags are closed, so you can drip feed a list or a table into the view as the content becomes available.
3038+
3039+
===== Progressive Rendering
3040+
3041+
A model element of type `Publisher` will be left in the model (instead of expanding it before the view is rendered), if its name starts with "flux" or "mono" or "publisher". The `View` is then rendered and flushed to the HTTP response as soon as each element is published. Browsers are really good at rendering partially complete HTML, so the flux elements will most likely be visible to the user as soon as they are available. This is useful for rendering the "main" content of a page if it is a list or a table, for instance.
3042+
3043+
===== Sserver Sent Event (SSE) Support
3044+
3045+
To render a `View` with content type `text/event-stream` you need a model element of type `Publisher`, and also a template that includes that element (probably starts and ends with it). There is a convenience Lambda (`ssedata`) added to the model for you that prepends every line with `data:` - you can use it if you wish to simplify the rendering of the data elements. Two new lines are added after each item in `{{#ssedata}}`. E.g. with an element called `flux.events` of type `Flux<Event>`:
3046+
3047+
```
3048+
{{#flux.events}}
3049+
event: message
3050+
id: {{id}}
3051+
{{#ssedata}}
3052+
<div>
3053+
<span>Name: {{name}}<span>
3054+
<span>Value: {{value}}<span>
3055+
</div>
3056+
{{/ssedata}}
3057+
{{/flux.events}}
3058+
```
3059+
3060+
the output will be
3061+
3062+
```
3063+
event: message
3064+
id: 0
3065+
data: <div>
3066+
data: <span>Name: foo<span>
3067+
data: <span>Value: bar<span>
3068+
data: </div>
3069+
3070+
3071+
event: message
3072+
id: 1
3073+
data: <div>
3074+
data: <span>Name: spam<span>
3075+
data: <span>Value: bucket<span>
3076+
data: </div>
3077+
3078+
3079+
... etc.
3080+
```
3081+
3082+
assuming the `Event` object has fields `id`, `name`, `value`.
30353083

30363084

30373085
[[boot-features-webflux-error-handling]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2016-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.reactive.result.view;
18+
19+
import java.io.IOException;
20+
import java.io.Writer;
21+
import java.nio.charset.Charset;
22+
import java.util.function.Supplier;
23+
24+
import org.reactivestreams.Publisher;
25+
import reactor.core.publisher.Flux;
26+
import reactor.core.publisher.Mono;
27+
28+
import org.springframework.core.io.buffer.DataBuffer;
29+
30+
/**
31+
* A {@link Writer} that can write a {@link Flux} (or {@link Publisher}) to a data buffer.
32+
* Used to render progressive output in a {@link MustacheView}.
33+
*
34+
* @author Dave Syer
35+
*/
36+
class FluxWriter extends Writer {
37+
38+
private final Supplier<DataBuffer> factory;
39+
40+
private final Charset charset;
41+
42+
private Flux<String> buffers;
43+
44+
public FluxWriter(Supplier<DataBuffer> factory) {
45+
this(factory, Charset.defaultCharset());
46+
}
47+
48+
public FluxWriter(Supplier<DataBuffer> factory, Charset charset) {
49+
this.factory = factory;
50+
this.charset = charset;
51+
this.buffers = Flux.empty();
52+
}
53+
54+
public Publisher<? extends Publisher<? extends DataBuffer>> getBuffers() {
55+
return this.buffers
56+
.map(string -> Mono.just(buffer().write(string, this.charset)));
57+
}
58+
59+
@Override
60+
public void write(char[] cbuf, int off, int len) throws IOException {
61+
this.buffers = this.buffers.concatWith(Mono.just(new String(cbuf, off, len)));
62+
}
63+
64+
@Override
65+
public void flush() throws IOException {
66+
}
67+
68+
@Override
69+
public void close() throws IOException {
70+
}
71+
72+
public void release() {
73+
// TODO: maybe implement this and call it on error
74+
}
75+
76+
private DataBuffer buffer() {
77+
return this.factory.get();
78+
}
79+
80+
public void write(Object thing) {
81+
if (thing instanceof Publisher) {
82+
@SuppressWarnings("unchecked")
83+
Publisher<String> publisher = (Publisher<String>) thing;
84+
this.buffers = this.buffers.concatWith(Flux.from(publisher));
85+
}
86+
else {
87+
if (thing instanceof String) {
88+
this.buffers = this.buffers.concatWith(Mono.just((String) thing));
89+
}
90+
}
91+
}
92+
93+
}

0 commit comments

Comments
 (0)