15 changed files with 870 additions and 7 deletions
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Flux; |
||||
|
||||
import org.springframework.core.ReactiveAdapter; |
||||
import org.springframework.core.ReactiveAdapterRegistry; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatusCode; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Default implementation of {@link FragmentRendering.Builder}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 6.2 |
||||
*/ |
||||
class DefaultFragmentRenderingBuilder implements FragmentRendering.Builder { |
||||
|
||||
private final Flux<Fragment> fragments; |
||||
|
||||
@Nullable |
||||
private HttpStatusCode status; |
||||
|
||||
@Nullable |
||||
private HttpHeaders headers; |
||||
|
||||
|
||||
DefaultFragmentRenderingBuilder(Collection<Fragment> fragments) { |
||||
this(Flux.fromIterable(fragments)); |
||||
} |
||||
|
||||
DefaultFragmentRenderingBuilder(Object fragments) { |
||||
this(adaptProducer(fragments)); |
||||
} |
||||
|
||||
DefaultFragmentRenderingBuilder(Publisher<Fragment> fragments) { |
||||
this.fragments = Flux.from(fragments); |
||||
} |
||||
|
||||
private static Publisher<Fragment> adaptProducer(Object fragments) { |
||||
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(fragments.getClass()); |
||||
Assert.isTrue(adapter != null, "Unknown producer " + fragments.getClass()); |
||||
return adapter.toPublisher(fragments); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public FragmentRendering.Builder status(HttpStatusCode status) { |
||||
this.status = status; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public FragmentRendering.Builder header(String headerName, String... headerValues) { |
||||
initHeaders().put(headerName, Arrays.asList(headerValues)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public FragmentRendering.Builder headers(Consumer<HttpHeaders> headersConsumer) { |
||||
headersConsumer.accept(initHeaders()); |
||||
return this; |
||||
} |
||||
|
||||
private HttpHeaders initHeaders() { |
||||
if (this.headers == null) { |
||||
this.headers = new HttpHeaders(); |
||||
} |
||||
return this.headers; |
||||
} |
||||
|
||||
@Override |
||||
public FragmentRendering build() { |
||||
return new DefaultFragmentRendering( |
||||
this.status, (this.headers != null ? this.headers : HttpHeaders.EMPTY), this.fragments); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Default implementation of {@link FragmentRendering}. |
||||
*/ |
||||
private record DefaultFragmentRendering(@Nullable HttpStatusCode status, HttpHeaders headers, Flux<Fragment> fragments) |
||||
implements FragmentRendering { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Container for a model and a view for use with {@link FragmentRendering} and |
||||
* multi-view rendering. For full page rendering with a single model and view, |
||||
* use {@link Rendering}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 6.2 |
||||
* @see FragmentRendering |
||||
*/ |
||||
public final class Fragment { |
||||
|
||||
@Nullable |
||||
private final String viewName; |
||||
|
||||
@Nullable |
||||
private final View view; |
||||
|
||||
private final Map<String, Object> model; |
||||
|
||||
|
||||
private Fragment(@Nullable String viewName, @Nullable View view, Map<String, Object> model) { |
||||
this.viewName = viewName; |
||||
this.view = view; |
||||
this.model = model; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Whether this Fragment contains a resolved {@link View} instance. |
||||
*/ |
||||
public boolean isResolved() { |
||||
return (this.view != null); |
||||
} |
||||
|
||||
/** |
||||
* Return the view name of the Fragment, or {@code null} if not set. |
||||
*/ |
||||
@Nullable |
||||
public String viewName() { |
||||
return this.viewName; |
||||
} |
||||
|
||||
/** |
||||
* Return the resolved {@link View} instance. This should be called only |
||||
* after an {@link #isResolved()} check. |
||||
*/ |
||||
public View view() { |
||||
Assert.state(this.view != null, "View not resolved"); |
||||
return this.view; |
||||
} |
||||
|
||||
/** |
||||
* Return the model for this Fragment. |
||||
*/ |
||||
public Map<String, Object> model() { |
||||
return this.model; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "Fragment [view=" + formatView() + "; model=" + this.model + "]"; |
||||
} |
||||
|
||||
private String formatView() { |
||||
return (isResolved() ? "\"" + view() + "\"" : "[" + viewName() + "]"); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Create a Fragment with a view name and a model. |
||||
*/ |
||||
public static Fragment create(String viewName, Map<String, Object> model) { |
||||
return new Fragment(viewName, null, model); |
||||
} |
||||
|
||||
/** |
||||
* Create a Fragment with a resolved {@link View} instance and a model. |
||||
*/ |
||||
public static Fragment create(View view, Map<String, Object> model) { |
||||
return new Fragment(null, view, model); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.Collection; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Flux; |
||||
|
||||
import org.springframework.core.ReactiveAdapterRegistry; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatusCode; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Public API for HTML rendering from a collection or from a stream of |
||||
* {@link Fragment}s each with its own view and model. For use with |
||||
* view technologies such as <a href="https://htmx.org/">htmx</a> where multiple |
||||
* page fragments may be rendered in a single response. Supported as a return |
||||
* value from a WebFlux controller method. |
||||
* |
||||
* <p>For full page rendering with a single model and view, use {@link Rendering}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 6.2 |
||||
*/ |
||||
public interface FragmentRendering { |
||||
|
||||
/** |
||||
* Return the HTTP status to set the response to. |
||||
*/ |
||||
@Nullable |
||||
HttpStatusCode status(); |
||||
|
||||
/** |
||||
* Return headers to add to the response. |
||||
*/ |
||||
HttpHeaders headers(); |
||||
|
||||
/** |
||||
* Return the fragments to render. |
||||
*/ |
||||
Flux<Fragment> fragments(); |
||||
|
||||
|
||||
/** |
||||
* Create a builder to render with a collection of Fragments. |
||||
*/ |
||||
static Builder fromCollection(Collection<Fragment> fragments) { |
||||
return new DefaultFragmentRenderingBuilder(fragments); |
||||
} |
||||
|
||||
/** |
||||
* Create a builder to render with a {@link Publisher} of Fragments. |
||||
*/ |
||||
static <P extends Publisher<Fragment>> Builder fromPublisher(P fragments) { |
||||
return new DefaultFragmentRenderingBuilder(fragments); |
||||
} |
||||
|
||||
/** |
||||
* Variant of {@link #fromPublisher(Publisher)} that allows using any |
||||
* producer that can be resolved to {@link Publisher} via |
||||
* {@link ReactiveAdapterRegistry}. |
||||
*/ |
||||
static Builder fromProducer(Object fragments) { |
||||
return new DefaultFragmentRenderingBuilder(fragments); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Defines a builder for {@link FragmentRendering}. |
||||
*/ |
||||
interface Builder { |
||||
|
||||
/** |
||||
* Specify the status to use for the response. |
||||
* @param status the status to set |
||||
* @return this builder |
||||
*/ |
||||
Builder status(HttpStatusCode status); |
||||
|
||||
/** |
||||
* Add the given, single header value under the given name. |
||||
* @param headerName the header name |
||||
* @param headerValues the header value(s) |
||||
* @return this builder |
||||
*/ |
||||
Builder header(String headerName, String... headerValues); |
||||
|
||||
/** |
||||
* Provides access to every header declared so far with the possibility |
||||
* to add, replace, or remove values. |
||||
* @param headersConsumer the consumer to provide access to |
||||
* @return this builder |
||||
*/ |
||||
Builder headers(Consumer<HttpHeaders> headersConsumer); |
||||
|
||||
/** |
||||
* Build the {@link FragmentRendering} instance. |
||||
*/ |
||||
FragmentRendering build(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.time.Duration; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest; |
||||
import org.junit.jupiter.params.provider.Arguments; |
||||
import org.junit.jupiter.params.provider.MethodSource; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.core.scheduler.Schedulers; |
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.support.ResourceBundleMessageSource; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.web.reactive.BindingContext; |
||||
import org.springframework.web.reactive.HandlerResult; |
||||
import org.springframework.web.reactive.accept.HeaderContentTypeResolver; |
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; |
||||
import org.springframework.web.reactive.result.view.script.ScriptTemplateConfigurer; |
||||
import org.springframework.web.reactive.result.view.script.ScriptTemplateViewResolver; |
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; |
||||
import org.springframework.web.testfixture.server.MockServerWebExchange; |
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; |
||||
import static org.junit.jupiter.api.Named.named; |
||||
import static org.springframework.web.testfixture.method.ResolvableMethod.on; |
||||
|
||||
/** |
||||
* Tests for multi-view rendering through {@link ViewResolutionResultHandler}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class FragmentResolutionResultHandlerTests { |
||||
|
||||
static Stream<Arguments> arguments() { |
||||
Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo")); |
||||
Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar")); |
||||
return Stream.of( |
||||
Arguments.of(named("Flux", |
||||
FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic())) |
||||
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) |
||||
.build())), |
||||
Arguments.of(named("List", |
||||
FragmentRendering.fromCollection(List.of(f1, f2)) |
||||
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) |
||||
.build())) |
||||
);} |
||||
|
||||
|
||||
@ParameterizedTest |
||||
@MethodSource("arguments") |
||||
void render(FragmentRendering rendering) { |
||||
|
||||
Locale locale = Locale.ENGLISH; |
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build(); |
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request); |
||||
|
||||
HandlerResult result = new HandlerResult( |
||||
new Handler(), rendering, on(Handler.class).resolveReturnType(FragmentRendering.class), |
||||
new BindingContext()); |
||||
|
||||
String body = initHandler().handleResult(exchange, result) |
||||
.then(Mono.defer(() -> exchange.getResponse().getBodyAsString())) |
||||
.block(Duration.ofSeconds(60)); |
||||
|
||||
assertThat(body).isEqualTo("<p>Hello Foo</p><p>Hello Bar</p>"); |
||||
} |
||||
|
||||
private ViewResolutionResultHandler initHandler() { |
||||
|
||||
AnnotationConfigApplicationContext context = |
||||
new AnnotationConfigApplicationContext(ScriptTemplatingConfiguration.class); |
||||
|
||||
String prefix = "org/springframework/web/reactive/result/view/script/kotlin/"; |
||||
ScriptTemplateViewResolver viewResolver = new ScriptTemplateViewResolver(prefix, ".kts"); |
||||
viewResolver.setApplicationContext(context); |
||||
|
||||
RequestedContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver(); |
||||
return new ViewResolutionResultHandler(List.of(viewResolver), contentTypeResolver); |
||||
} |
||||
|
||||
|
||||
private static class Handler { |
||||
|
||||
FragmentRendering rendering() { return null; } |
||||
|
||||
} |
||||
|
||||
|
||||
@Configuration |
||||
static class ScriptTemplatingConfiguration { |
||||
|
||||
@Bean |
||||
public ScriptTemplateConfigurer kotlinScriptConfigurer() { |
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); |
||||
configurer.setEngineName("kotlin"); |
||||
configurer.setScripts("org/springframework/web/reactive/result/view/script/kotlin/render.kts"); |
||||
configurer.setRenderFunction("render"); |
||||
return configurer; |
||||
} |
||||
|
||||
@Bean |
||||
public ResourceBundleMessageSource messageSource() { |
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); |
||||
messageSource.setBasename("org/springframework/web/reactive/result/view/script/messages"); |
||||
return messageSource; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
import org.springframework.web.reactive.result.view.script.* |
||||
|
||||
"""<p>${i18n("hello")} $foo</p>""" |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
import org.springframework.web.reactive.result.view.script.* |
||||
|
||||
"""<p>${i18n("hello")} $bar</p>""" |
||||
@ -0,0 +1,173 @@
@@ -0,0 +1,173 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.servlet.view; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Collection; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
|
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.ServletOutputStream; |
||||
import jakarta.servlet.WriteListener; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import jakarta.servlet.http.HttpServletResponseWrapper; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.servlet.ModelAndView; |
||||
import org.springframework.web.servlet.SmartView; |
||||
import org.springframework.web.servlet.View; |
||||
import org.springframework.web.servlet.ViewResolver; |
||||
|
||||
/** |
||||
* {@link View} that enables rendering of a collection of fragments, each with |
||||
* its own view and model, also inheriting common attributes from the top-level model. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 6.2 |
||||
*/ |
||||
public class FragmentsView implements SmartView { |
||||
|
||||
private final Collection<ModelAndView> modelAndViews; |
||||
|
||||
|
||||
/** |
||||
* Protected constructor to allow extension. |
||||
*/ |
||||
protected FragmentsView(Collection<ModelAndView> modelAndViews) { |
||||
this.modelAndViews = modelAndViews; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean isRedirectView() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public void resolveNestedViews(ViewResolver resolver, Locale locale) throws Exception { |
||||
for (ModelAndView mv : this.modelAndViews) { |
||||
View view = resolveView(resolver, locale, mv); |
||||
mv.setView(view); |
||||
} |
||||
} |
||||
|
||||
private static View resolveView(ViewResolver viewResolver, Locale locale, ModelAndView mv) throws Exception { |
||||
String viewName = mv.getViewName(); |
||||
View view = (viewName != null ? viewResolver.resolveViewName(viewName, locale) : mv.getView()); |
||||
if (view == null) { |
||||
throw new ServletException("Could not resolve view in " + mv); |
||||
} |
||||
return view; |
||||
} |
||||
|
||||
@Override |
||||
public void render( |
||||
@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) |
||||
throws Exception { |
||||
|
||||
if (model != null) { |
||||
model.forEach((key, value) -> |
||||
this.modelAndViews.forEach(mv -> mv.getModel().putIfAbsent(key, value))); |
||||
} |
||||
|
||||
HttpServletResponse nonClosingResponse = new NonClosingHttpServletResponse(response); |
||||
for (ModelAndView mv : this.modelAndViews) { |
||||
Assert.state(mv.getView() != null, "Expected View"); |
||||
mv.getView().render(mv.getModel(), request, nonClosingResponse); |
||||
response.flushBuffer(); |
||||
} |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "FragmentsView " + this.modelAndViews; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Factory method to create an instance with the given fragments. |
||||
* @param modelAndViews the {@link ModelAndView} to use |
||||
* @return the created {@code FragmentsView} instance |
||||
*/ |
||||
public static FragmentsView create(Collection<ModelAndView> modelAndViews) { |
||||
return new FragmentsView(modelAndViews); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Response wrapper that in turn applies {@link NonClosingServletOutputStream}. |
||||
*/ |
||||
private static final class NonClosingHttpServletResponse extends HttpServletResponseWrapper { |
||||
|
||||
@Nullable |
||||
private ServletOutputStream os; |
||||
|
||||
public NonClosingHttpServletResponse(HttpServletResponse response) { |
||||
super(response); |
||||
} |
||||
|
||||
@Override |
||||
public ServletOutputStream getOutputStream() throws IOException { |
||||
if (this.os == null) { |
||||
this.os = new NonClosingServletOutputStream(getResponse().getOutputStream()); |
||||
} |
||||
return this.os; |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* {@code OutputStream} that leaves the response open, ignoring calls to close it. |
||||
*/ |
||||
private static final class NonClosingServletOutputStream extends ServletOutputStream { |
||||
|
||||
private final ServletOutputStream os; |
||||
|
||||
public NonClosingServletOutputStream(ServletOutputStream os) { |
||||
this.os = os; |
||||
} |
||||
|
||||
@Override |
||||
public void write(int b) throws IOException { |
||||
this.os.write(b); |
||||
} |
||||
|
||||
@Override |
||||
public void write(byte[] b, int off, int len) throws IOException { |
||||
this.os.write(b, off, len); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isReady() { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public void close() { |
||||
} |
||||
|
||||
@Override |
||||
public void setWriteListener(WriteListener writeListener) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.servlet.view; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.condition.DisabledForJreRange; |
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.support.ResourceBundleMessageSource; |
||||
import org.springframework.web.servlet.ModelAndView; |
||||
import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer; |
||||
import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver; |
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest; |
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.junit.jupiter.api.condition.JRE.JAVA_21; |
||||
|
||||
/** |
||||
* Tests for rendering through {@link FragmentsView}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@DisabledForJreRange(min = JAVA_21, disabledReason = "Kotlin doesn't support Java 21+ yet") |
||||
public class FragmentsViewTests { |
||||
|
||||
|
||||
@Test |
||||
void render() throws Exception { |
||||
|
||||
AnnotationConfigApplicationContext context = |
||||
new AnnotationConfigApplicationContext(ScriptTemplatingConfiguration.class); |
||||
|
||||
String prefix = "org/springframework/web/servlet/view/script/kotlin/"; |
||||
ScriptTemplateViewResolver viewResolver = new ScriptTemplateViewResolver(prefix, ".kts"); |
||||
viewResolver.setApplicationContext(context); |
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
|
||||
FragmentsView view = FragmentsView.create(List.of( |
||||
new ModelAndView("fragment1", Map.of("foo", "Foo")), |
||||
new ModelAndView("fragment2", Map.of("bar", "Bar")))); |
||||
|
||||
view.resolveNestedViews(viewResolver, Locale.ENGLISH); |
||||
view.render(Collections.emptyMap(), request, response); |
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo("<p>Hello Foo</p><p>Hello Bar</p>"); |
||||
} |
||||
|
||||
|
||||
@Configuration |
||||
static class ScriptTemplatingConfiguration { |
||||
|
||||
@Bean |
||||
ScriptTemplateConfigurer kotlinScriptConfigurer() { |
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); |
||||
configurer.setEngineName("kotlin"); |
||||
configurer.setScripts("org/springframework/web/servlet/view/script/kotlin/render.kts"); |
||||
configurer.setRenderFunction("render"); |
||||
return configurer; |
||||
} |
||||
|
||||
@Bean |
||||
ResourceBundleMessageSource messageSource() { |
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); |
||||
messageSource.setBasename("org/springframework/web/servlet/view/script/messages"); |
||||
return messageSource; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
import org.springframework.web.servlet.view.script.* |
||||
|
||||
"""<p>${i18n("hello")} $foo</p>""" |
||||
Loading…
Reference in new issue