Skip to content

Commit ef6a376

Browse files
committed
Support multi-view rendering
See gh-33162
1 parent 545228d commit ef6a376

File tree

15 files changed

+870
-7
lines changed

15 files changed

+870
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2002-2024 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+
* https://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.web.reactive.result.view;
18+
19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
import java.util.function.Consumer;
22+
23+
import org.reactivestreams.Publisher;
24+
import reactor.core.publisher.Flux;
25+
26+
import org.springframework.core.ReactiveAdapter;
27+
import org.springframework.core.ReactiveAdapterRegistry;
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.HttpStatusCode;
30+
import org.springframework.lang.Nullable;
31+
import org.springframework.util.Assert;
32+
33+
/**
34+
* Default implementation of {@link FragmentRendering.Builder}.
35+
*
36+
* @author Rossen Stoyanchev
37+
* @since 6.2
38+
*/
39+
class DefaultFragmentRenderingBuilder implements FragmentRendering.Builder {
40+
41+
private final Flux<Fragment> fragments;
42+
43+
@Nullable
44+
private HttpStatusCode status;
45+
46+
@Nullable
47+
private HttpHeaders headers;
48+
49+
50+
DefaultFragmentRenderingBuilder(Collection<Fragment> fragments) {
51+
this(Flux.fromIterable(fragments));
52+
}
53+
54+
DefaultFragmentRenderingBuilder(Object fragments) {
55+
this(adaptProducer(fragments));
56+
}
57+
58+
DefaultFragmentRenderingBuilder(Publisher<Fragment> fragments) {
59+
this.fragments = Flux.from(fragments);
60+
}
61+
62+
private static Publisher<Fragment> adaptProducer(Object fragments) {
63+
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(fragments.getClass());
64+
Assert.isTrue(adapter != null, "Unknown producer " + fragments.getClass());
65+
return adapter.toPublisher(fragments);
66+
}
67+
68+
69+
@Override
70+
public FragmentRendering.Builder status(HttpStatusCode status) {
71+
this.status = status;
72+
return this;
73+
}
74+
75+
@Override
76+
public FragmentRendering.Builder header(String headerName, String... headerValues) {
77+
initHeaders().put(headerName, Arrays.asList(headerValues));
78+
return this;
79+
}
80+
81+
@Override
82+
public FragmentRendering.Builder headers(Consumer<HttpHeaders> headersConsumer) {
83+
headersConsumer.accept(initHeaders());
84+
return this;
85+
}
86+
87+
private HttpHeaders initHeaders() {
88+
if (this.headers == null) {
89+
this.headers = new HttpHeaders();
90+
}
91+
return this.headers;
92+
}
93+
94+
@Override
95+
public FragmentRendering build() {
96+
return new DefaultFragmentRendering(
97+
this.status, (this.headers != null ? this.headers : HttpHeaders.EMPTY), this.fragments);
98+
}
99+
100+
101+
/**
102+
* Default implementation of {@link FragmentRendering}.
103+
*/
104+
private record DefaultFragmentRendering(@Nullable HttpStatusCode status, HttpHeaders headers, Flux<Fragment> fragments)
105+
implements FragmentRendering {
106+
}
107+
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2002-2024 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+
* https://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.web.reactive.result.view;
18+
19+
import java.util.Map;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Container for a model and a view for use with {@link FragmentRendering} and
26+
* multi-view rendering. For full page rendering with a single model and view,
27+
* use {@link Rendering}.
28+
*
29+
* @author Rossen Stoyanchev
30+
* @since 6.2
31+
* @see FragmentRendering
32+
*/
33+
public final class Fragment {
34+
35+
@Nullable
36+
private final String viewName;
37+
38+
@Nullable
39+
private final View view;
40+
41+
private final Map<String, Object> model;
42+
43+
44+
private Fragment(@Nullable String viewName, @Nullable View view, Map<String, Object> model) {
45+
this.viewName = viewName;
46+
this.view = view;
47+
this.model = model;
48+
}
49+
50+
51+
/**
52+
* Whether this Fragment contains a resolved {@link View} instance.
53+
*/
54+
public boolean isResolved() {
55+
return (this.view != null);
56+
}
57+
58+
/**
59+
* Return the view name of the Fragment, or {@code null} if not set.
60+
*/
61+
@Nullable
62+
public String viewName() {
63+
return this.viewName;
64+
}
65+
66+
/**
67+
* Return the resolved {@link View} instance. This should be called only
68+
* after an {@link #isResolved()} check.
69+
*/
70+
public View view() {
71+
Assert.state(this.view != null, "View not resolved");
72+
return this.view;
73+
}
74+
75+
/**
76+
* Return the model for this Fragment.
77+
*/
78+
public Map<String, Object> model() {
79+
return this.model;
80+
}
81+
82+
@Override
83+
public String toString() {
84+
return "Fragment [view=" + formatView() + "; model=" + this.model + "]";
85+
}
86+
87+
private String formatView() {
88+
return (isResolved() ? "\"" + view() + "\"" : "[" + viewName() + "]");
89+
}
90+
91+
92+
/**
93+
* Create a Fragment with a view name and a model.
94+
*/
95+
public static Fragment create(String viewName, Map<String, Object> model) {
96+
return new Fragment(viewName, null, model);
97+
}
98+
99+
/**
100+
* Create a Fragment with a resolved {@link View} instance and a model.
101+
*/
102+
public static Fragment create(View view, Map<String, Object> model) {
103+
return new Fragment(null, view, model);
104+
}
105+
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2002-2024 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+
* https://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.web.reactive.result.view;
18+
19+
import java.util.Collection;
20+
import java.util.function.Consumer;
21+
22+
import org.reactivestreams.Publisher;
23+
import reactor.core.publisher.Flux;
24+
25+
import org.springframework.core.ReactiveAdapterRegistry;
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.http.HttpStatusCode;
28+
import org.springframework.lang.Nullable;
29+
30+
/**
31+
* Public API for HTML rendering from a collection or from a stream of
32+
* {@link Fragment}s each with its own view and model. For use with
33+
* view technologies such as <a href="https://htmx.org/">htmx</a> where multiple
34+
* page fragments may be rendered in a single response. Supported as a return
35+
* value from a WebFlux controller method.
36+
*
37+
* <p>For full page rendering with a single model and view, use {@link Rendering}.
38+
*
39+
* @author Rossen Stoyanchev
40+
* @since 6.2
41+
*/
42+
public interface FragmentRendering {
43+
44+
/**
45+
* Return the HTTP status to set the response to.
46+
*/
47+
@Nullable
48+
HttpStatusCode status();
49+
50+
/**
51+
* Return headers to add to the response.
52+
*/
53+
HttpHeaders headers();
54+
55+
/**
56+
* Return the fragments to render.
57+
*/
58+
Flux<Fragment> fragments();
59+
60+
61+
/**
62+
* Create a builder to render with a collection of Fragments.
63+
*/
64+
static Builder fromCollection(Collection<Fragment> fragments) {
65+
return new DefaultFragmentRenderingBuilder(fragments);
66+
}
67+
68+
/**
69+
* Create a builder to render with a {@link Publisher} of Fragments.
70+
*/
71+
static <P extends Publisher<Fragment>> Builder fromPublisher(P fragments) {
72+
return new DefaultFragmentRenderingBuilder(fragments);
73+
}
74+
75+
/**
76+
* Variant of {@link #fromPublisher(Publisher)} that allows using any
77+
* producer that can be resolved to {@link Publisher} via
78+
* {@link ReactiveAdapterRegistry}.
79+
*/
80+
static Builder fromProducer(Object fragments) {
81+
return new DefaultFragmentRenderingBuilder(fragments);
82+
}
83+
84+
85+
/**
86+
* Defines a builder for {@link FragmentRendering}.
87+
*/
88+
interface Builder {
89+
90+
/**
91+
* Specify the status to use for the response.
92+
* @param status the status to set
93+
* @return this builder
94+
*/
95+
Builder status(HttpStatusCode status);
96+
97+
/**
98+
* Add the given, single header value under the given name.
99+
* @param headerName the header name
100+
* @param headerValues the header value(s)
101+
* @return this builder
102+
*/
103+
Builder header(String headerName, String... headerValues);
104+
105+
/**
106+
* Provides access to every header declared so far with the possibility
107+
* to add, replace, or remove values.
108+
* @param headersConsumer the consumer to provide access to
109+
* @return this builder
110+
*/
111+
Builder headers(Consumer<HttpHeaders> headersConsumer);
112+
113+
/**
114+
* Build the {@link FragmentRendering} instance.
115+
*/
116+
FragmentRendering build();
117+
}
118+
119+
}

0 commit comments

Comments
 (0)