Add FreeMarker View, ViewResolver and basic hierarchy
This commit is contained in:
@@ -84,6 +84,7 @@ dependencies {
|
||||
compile "io.projectreactor:reactor-core:${reactorVersion}"
|
||||
compile "commons-logging:commons-logging:1.2"
|
||||
|
||||
optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker
|
||||
optional 'io.reactivex:rxjava:1.1.0'
|
||||
optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT"
|
||||
optional "com.fasterxml.jackson.core:jackson-databind:2.6.2"
|
||||
@@ -95,6 +96,7 @@ dependencies {
|
||||
optional 'io.undertow:undertow-core:1.3.5.Final'
|
||||
optional "org.eclipse.jetty:jetty-server:${jettyVersion}"
|
||||
optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}"
|
||||
optional("org.freemarker:freemarker:2.3.23")
|
||||
|
||||
provided "javax.servlet:javax.servlet-api:3.1.0"
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
|
||||
|
||||
private final Map<Encoder<?>, List<MediaType>> mediaTypesByEncoder;
|
||||
|
||||
private int order = 0;
|
||||
private int order = 0; // TODO: should be MAX_VALUE
|
||||
|
||||
|
||||
public ResponseBodyResultHandler(List<Encoder<?>> encoders, ConversionService service) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* Abstract base class for URL-based views. Provides a consistent way of
|
||||
* holding the URL that a View wraps, in the form of a "url" bean property.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public abstract class AbstractUrlBasedView extends AbstractView implements InitializingBean {
|
||||
|
||||
private String url;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for use as a bean.
|
||||
*/
|
||||
protected AbstractUrlBasedView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new AbstractUrlBasedView with the given URL.
|
||||
*/
|
||||
protected AbstractUrlBasedView(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the URL of the resource that this view wraps.
|
||||
* The URL must be appropriate for the concrete View implementation.
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URL of the resource that this view wraps.
|
||||
*/
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (getUrl() == null) {
|
||||
throw new IllegalArgumentException("Property 'url' is required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the resource for the configured URL actually exists.
|
||||
* @param locale the desired Locale that we're looking for
|
||||
* @return {@code false} if the resource exists
|
||||
* {@code false} if we know that it does not exist
|
||||
* @throws Exception if the resource exists but is invalid (e.g. could not be parsed)
|
||||
*/
|
||||
public abstract boolean checkResourceExists(Locale locale) throws Exception;
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + "; URL [" + getUrl() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferAllocator;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.View;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public abstract class AbstractView implements View, ApplicationContextAware {
|
||||
|
||||
/** Logger that is available to subclasses */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
private final List<MediaType> mediaTypes = new ArrayList<>(4);
|
||||
|
||||
private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
|
||||
public AbstractView() {
|
||||
this.mediaTypes.add(ViewResolverSupport.DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the supported media types for this view.
|
||||
* Default is "text/html;charset=UTF-8".
|
||||
*/
|
||||
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
|
||||
Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' is required.");
|
||||
this.mediaTypes.clear();
|
||||
if (supportedMediaTypes != null) {
|
||||
this.mediaTypes.addAll(supportedMediaTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured media types supported by this view.
|
||||
*/
|
||||
@Override
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return this.mediaTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link DataBufferAllocator} to use for write I/O.
|
||||
* <p>By default this is set to {@link DefaultDataBufferAllocator}.
|
||||
* @param bufferAllocator the allocator to use
|
||||
*/
|
||||
public void setBufferAllocator(DataBufferAllocator bufferAllocator) {
|
||||
Assert.notNull(bufferAllocator, "'bufferAllocator' is required.");
|
||||
this.bufferAllocator = bufferAllocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured buffer allocator, never {@code null}.
|
||||
*/
|
||||
public DataBufferAllocator getBufferAllocator() {
|
||||
return this.bufferAllocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the model to render.
|
||||
* @param result the result from handler execution
|
||||
* @param contentType the content type selected to render with which should
|
||||
* match one of the {@link #getSupportedMediaTypes() supported media types}.
|
||||
* @param exchange the current exchange
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Flux<DataBuffer> render(HandlerResult result, Optional<MediaType> contentType,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Rendering view with model " + result.getModel());
|
||||
}
|
||||
|
||||
if (contentType.isPresent()) {
|
||||
exchange.getResponse().getHeaders().setContentType(contentType.get());
|
||||
}
|
||||
|
||||
Map<String, Object> mergedModel = getModelAttributes(result, exchange);
|
||||
return renderInternal(mergedModel, exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the model to use for rendering.
|
||||
* <p>The default implementation creates a combined output Map that includes
|
||||
* model as well as static attributes with the former taking precedence.
|
||||
*/
|
||||
protected Map<String, Object> getModelAttributes(HandlerResult result, ServerWebExchange exchange) {
|
||||
ModelMap model = result.getModel();
|
||||
int size = (model != null ? model.size() : 0);
|
||||
|
||||
Map<String, Object> attributes = new LinkedHashMap<>(size);
|
||||
if (model != null) {
|
||||
attributes.putAll(model);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must implement this method to actually render the view.
|
||||
* @param renderAttributes combined output Map (never {@code null}),
|
||||
* with dynamic values taking precedence over static attributes
|
||||
* @param exchange current exchange
|
||||
*/
|
||||
protected abstract Flux<DataBuffer> renderInternal(Map<String, Object> renderAttributes,
|
||||
ServerWebExchange exchange);
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.web.reactive.View;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.web.reactive.ViewResolver ViewResolver} that
|
||||
* allow direct resolution of symbolic view names to URLs without explicit
|
||||
* mapping definition. This is useful if symbolic names match the names of view
|
||||
* resources in a straightforward manner (i.e. the symbolic name is the unique
|
||||
* part of the resource's filename), without the need for a dedicated mapping
|
||||
* to be defined for each view.
|
||||
*
|
||||
* <p>Supports {@link AbstractUrlBasedView} subclasses like
|
||||
* {@link org.springframework.web.reactive.view.freemarker.FreeMarkerView}.
|
||||
* The view class for all views generated by this resolver can be specified
|
||||
* via the "viewClass" property.
|
||||
*
|
||||
* <p>View names can either be resource URLs themselves, or get augmented by a
|
||||
* specified prefix and/or suffix. Exporting an attribute that holds the
|
||||
* RequestContext to all views is explicitly supported.
|
||||
*
|
||||
* <p>Example: prefix="templates/", suffix=".ftl", viewname="test" ->
|
||||
* "templates/test.ftl"
|
||||
*
|
||||
* <p>As a special feature, redirect URLs can be specified via the "redirect:"
|
||||
* prefix. E.g.: "redirect:myAction" will trigger a redirect to the given
|
||||
* URL, rather than resolution as standard view name. This is typically used
|
||||
* for redirecting to a controller URL after finishing a form workflow.
|
||||
*
|
||||
* <p>Note: This class does not support localized resolution, i.e. resolving
|
||||
* a symbolic view name to different resources depending on the current locale.
|
||||
* * @author Rossen Stoyanchev
|
||||
*/
|
||||
public class UrlBasedViewResolver extends ViewResolverSupport implements InitializingBean {
|
||||
|
||||
private Class<?> viewClass;
|
||||
|
||||
private String prefix = "";
|
||||
|
||||
private String suffix = "";
|
||||
|
||||
|
||||
/**
|
||||
* Set the view class to instantiate through {@link #createUrlBasedView(String)}.
|
||||
* @param viewClass a class that is assignable to the required view class
|
||||
* which by default is AbstractUrlBasedView.
|
||||
*/
|
||||
public void setViewClass(Class<?> viewClass) {
|
||||
if (viewClass == null || !requiredViewClass().isAssignableFrom(viewClass)) {
|
||||
String name = (viewClass != null ? viewClass.getName() : null);
|
||||
throw new IllegalArgumentException("Given view class [" + name + "] " +
|
||||
"is not of type [" + requiredViewClass().getName() + "]");
|
||||
}
|
||||
this.viewClass = viewClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the view class to be used to create views.
|
||||
*/
|
||||
protected Class<?> getViewClass() {
|
||||
return this.viewClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the required type of view for this resolver.
|
||||
* This implementation returns {@link AbstractUrlBasedView}.
|
||||
* @see AbstractUrlBasedView
|
||||
*/
|
||||
protected Class<?> requiredViewClass() {
|
||||
return AbstractUrlBasedView.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prefix that gets prepended to view names when building a URL.
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = (prefix != null ? prefix : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the prefix that gets prepended to view names when building a URL.
|
||||
*/
|
||||
protected String getPrefix() {
|
||||
return this.prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suffix that gets appended to view names when building a URL.
|
||||
*/
|
||||
public void setSuffix(String suffix) {
|
||||
this.suffix = (suffix != null ? suffix : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the suffix that gets appended to view names when building a URL.
|
||||
*/
|
||||
protected String getSuffix() {
|
||||
return this.suffix;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (getViewClass() == null) {
|
||||
throw new IllegalArgumentException("Property 'viewClass' is required");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<View> resolveViewName(String viewName, Locale locale) {
|
||||
AbstractUrlBasedView urlBasedView = createUrlBasedView(viewName);
|
||||
View view = applyLifecycleMethods(viewName, urlBasedView);
|
||||
try {
|
||||
return (urlBasedView.checkResourceExists(locale) ? Mono.just(view) : Mono.empty());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new View instance of the specified view class and configures it.
|
||||
* Does <i>not</i> perform any lookup for pre-defined View instances.
|
||||
* <p>Spring lifecycle methods as defined by the bean container do not have to
|
||||
* be called here; those will be applied by the {@code loadView} method
|
||||
* after this method returns.
|
||||
* <p>Subclasses will typically call {@code super.buildView(viewName)}
|
||||
* first, before setting further properties themselves. {@code loadView}
|
||||
* will then apply Spring lifecycle methods at the end of this process.
|
||||
* @param viewName the name of the view to build
|
||||
* @return the View instance
|
||||
*/
|
||||
protected AbstractUrlBasedView createUrlBasedView(String viewName) {
|
||||
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
|
||||
view.setSupportedMediaTypes(getSupportedMediaTypes());
|
||||
view.setBufferAllocator(getBufferAllocator());
|
||||
view.setUrl(getPrefix() + viewName + getSuffix());
|
||||
return view;
|
||||
}
|
||||
|
||||
private View applyLifecycleMethods(String viewName, AbstractView view) {
|
||||
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import java.util.Optional;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -45,12 +46,14 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ViewResolverResultHandler implements HandlerResultHandler {
|
||||
public class ViewResolverResultHandler implements HandlerResultHandler, Ordered {
|
||||
|
||||
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private int order = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
public ViewResolverResultHandler(List<ViewResolver> resolvers, ConversionService service) {
|
||||
Assert.notEmpty(resolvers, "At least one ViewResolver is required.");
|
||||
@@ -67,6 +70,15 @@ public class ViewResolverResultHandler implements HandlerResultHandler {
|
||||
return Collections.unmodifiableList(this.viewResolvers);
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
|
||||
// TODO: @ModelAttribute return value, declared Object return value (either String or View)
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.DataBufferAllocator;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.ViewResolver;
|
||||
|
||||
/**
|
||||
* Base class for {@code ViewResolver} implementations with shared properties.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3
|
||||
*/
|
||||
public abstract class ViewResolverSupport implements ViewResolver, ApplicationContextAware, Ordered {
|
||||
|
||||
public static final MediaType DEFAULT_CONTENT_TYPE = MediaType.parseMediaType("text/html;charset=UTF-8");
|
||||
|
||||
|
||||
private List<MediaType> mediaTypes = new ArrayList<>(4);
|
||||
|
||||
private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private int order = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
public ViewResolverSupport() {
|
||||
this.mediaTypes.add(DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the supported media types for this view.
|
||||
* Default is "text/html;charset=UTF-8".
|
||||
*/
|
||||
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
|
||||
Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' is required.");
|
||||
this.mediaTypes.clear();
|
||||
if (supportedMediaTypes != null) {
|
||||
this.mediaTypes.addAll(supportedMediaTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured media types supported by this view.
|
||||
*/
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return this.mediaTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link DataBufferAllocator} to use for write I/O.
|
||||
* <p>By default this is set to {@link DefaultDataBufferAllocator}.
|
||||
* @param bufferAllocator the allocator to use
|
||||
*/
|
||||
public void setBufferAllocator(DataBufferAllocator bufferAllocator) {
|
||||
Assert.notNull(bufferAllocator, "'bufferAllocator' is required.");
|
||||
this.bufferAllocator = bufferAllocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured buffer allocator, never {@code null}.
|
||||
*/
|
||||
public DataBufferAllocator getBufferAllocator() {
|
||||
return this.bufferAllocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order in which this {@link ViewResolver}
|
||||
* is evaluated.
|
||||
*/
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the order in which this {@link ViewResolver} is evaluated.
|
||||
*/
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view.freemarker;
|
||||
|
||||
import freemarker.template.Configuration;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that configure and manage a
|
||||
* FreeMarker Configuration object in a web environment. Detected and
|
||||
* used by {@link FreeMarkerView}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public interface FreeMarkerConfig {
|
||||
|
||||
/**
|
||||
* Return the FreeMarker Configuration object for the current
|
||||
* web application context.
|
||||
* <p>A FreeMarker Configuration object may be used to set FreeMarker
|
||||
* properties and shared objects, and allows to retrieve templates.
|
||||
* @return the FreeMarker Configuration
|
||||
*/
|
||||
Configuration getConfiguration();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view.freemarker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import freemarker.cache.ClassTemplateLoader;
|
||||
import freemarker.cache.TemplateLoader;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.TemplateException;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory;
|
||||
|
||||
/**
|
||||
* Configures FreeMarker for web usage via the "configLocation" and/or
|
||||
* "freemarkerSettings" and/or "templateLoaderPath" properties.
|
||||
* The simplest way to use this class is to specify just a "templateLoaderPath"
|
||||
* (e.g. "classpath:templates"); you do not need any further configuration then.
|
||||
*
|
||||
* <p>This bean must be included in the application context of any application
|
||||
* using {@link FreeMarkerView}. It exists purely to configure FreeMarker.
|
||||
* It is not meant to be referenced by application components but just internally
|
||||
* by {@code FreeMarkerView}. Implements {@link FreeMarkerConfig} to be found by
|
||||
* {@code FreeMarkerView} without depending on the bean name the configurer.
|
||||
*
|
||||
* <p>Note that you can also refer to a pre-configured FreeMarker Configuration
|
||||
* instance via the "configuration" property. This allows to share a FreeMarker
|
||||
* Configuration for web and email usage for example.
|
||||
*
|
||||
* <p>TODO: macros
|
||||
*
|
||||
* <p>This configurer registers a template loader for this package, allowing to
|
||||
* reference the "spring.ftl" macro library contained in this package:
|
||||
*
|
||||
* <pre class="code">
|
||||
* <#import "/spring.ftl" as spring/>
|
||||
* <@spring.bind "person.age"/>
|
||||
* age is ${spring.status.value}</pre>
|
||||
*
|
||||
* Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
|
||||
implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware {
|
||||
|
||||
private Configuration configuration;
|
||||
|
||||
|
||||
public FreeMarkerConfigurer() {
|
||||
setDefaultEncoding("UTF-8");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a pre-configured Configuration to use for the FreeMarker web config,
|
||||
* e.g. a shared one for web and email usage. If this is not set,
|
||||
* FreeMarkerConfigurationFactory's properties (inherited by this class)
|
||||
* have to be specified.
|
||||
*/
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize FreeMarkerConfigurationFactory's Configuration
|
||||
* if not overridden by a pre-configured FreeMarker Configuation.
|
||||
* <p>Sets up a ClassTemplateLoader to use for loading Spring macros.
|
||||
* @see #createConfiguration
|
||||
* @see #setConfiguration
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws IOException, TemplateException {
|
||||
if (this.configuration == null) {
|
||||
this.configuration = createConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation registers an additional ClassTemplateLoader
|
||||
* for the Spring-provided macros, added to the end of the list.
|
||||
*/
|
||||
@Override
|
||||
protected void postProcessTemplateLoaders(List<TemplateLoader> templateLoaders) {
|
||||
templateLoaders.add(new ClassTemplateLoader(FreeMarkerConfigurer.class, ""));
|
||||
logger.info("ClassTemplateLoader for Spring macros added to FreeMarker configuration");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the Configuration object wrapped by this bean.
|
||||
*/
|
||||
@Override
|
||||
public Configuration getConfiguration() {
|
||||
return this.configuration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view.freemarker;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import freemarker.core.ParseException;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.DefaultObjectWrapperBuilder;
|
||||
import freemarker.template.ObjectWrapper;
|
||||
import freemarker.template.SimpleHash;
|
||||
import freemarker.template.Template;
|
||||
import freemarker.template.Version;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.web.reactive.view.AbstractUrlBasedView;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code View} implementation that uses the FreeMarker template engine.
|
||||
*
|
||||
* <p>Depends on a single {@link FreeMarkerConfig} object such as
|
||||
* {@link FreeMarkerConfigurer} being accessible in the application context.
|
||||
* Alternatively set the FreeMarker configuration can be set directly on this
|
||||
* class via {@link #setConfiguration}.
|
||||
*
|
||||
* <p>The {@link #setUrl(String) url} property is the location of the FreeMarker
|
||||
* template relative to the FreeMarkerConfigurer's
|
||||
* {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}.
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FreeMarkerView extends AbstractUrlBasedView {
|
||||
|
||||
private Configuration configuration;
|
||||
|
||||
private String encoding;
|
||||
|
||||
|
||||
/**
|
||||
* Set the FreeMarker Configuration to be used by this view.
|
||||
* <p>Typically this property is not set directly. Instead a single
|
||||
* {@link FreeMarkerConfig} is expected in the Spring application context
|
||||
* which is used to obtain the FreeMarker configuration.
|
||||
*/
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FreeMarker configuration used by this view.
|
||||
*/
|
||||
protected Configuration getConfiguration() {
|
||||
return this.configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the encoding of the FreeMarker template file.
|
||||
* <p>By default {@link FreeMarkerConfigurer} sets the default encoding in
|
||||
* the FreeMarker configuration to "UTF-8". It's recommended to specify the
|
||||
* encoding in the FreeMarker Configuration rather than per template if all
|
||||
* your templates share a common encoding.
|
||||
*/
|
||||
public void setEncoding(String encoding) {
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the encoding for the FreeMarker template.
|
||||
*/
|
||||
protected String getEncoding() {
|
||||
return this.encoding;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
super.afterPropertiesSet();
|
||||
if (getConfiguration() == null) {
|
||||
FreeMarkerConfig config = autodetectConfiguration();
|
||||
setConfiguration(config.getConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext.
|
||||
* @return the Configuration instance to use for FreeMarkerViews
|
||||
* @throws BeansException if no Configuration instance could be found
|
||||
* @see #setConfiguration
|
||||
*/
|
||||
protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
|
||||
try {
|
||||
return BeanFactoryUtils.beanOfTypeIncludingAncestors(
|
||||
getApplicationContext(), FreeMarkerConfig.class, true, false);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
throw new ApplicationContextException(
|
||||
"Must define a single FreeMarkerConfig bean in this web application context " +
|
||||
"(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
|
||||
"This bean may be given any name.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that the FreeMarker template used for this view exists and is valid.
|
||||
* <p>Can be overridden to customize the behavior, for example in case of
|
||||
* multiple templates to be rendered into a single view.
|
||||
*/
|
||||
@Override
|
||||
public boolean checkResourceExists(Locale locale) throws Exception {
|
||||
try {
|
||||
// Check that we can get the template, even if we might subsequently get it again.
|
||||
getTemplate(locale);
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No FreeMarker view found for URL: " + getUrl());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (ParseException ex) {
|
||||
throw new ApplicationContextException(
|
||||
"Failed to parse FreeMarker template for URL [" + getUrl() + "]", ex);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ApplicationContextException(
|
||||
"Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Flux<DataBuffer> renderInternal(Map<String, Object> renderAttributes, ServerWebExchange exchange) {
|
||||
// Expose all standard FreeMarker hash models.
|
||||
SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Rendering FreeMarker template [" + getUrl() + "].");
|
||||
}
|
||||
Locale locale = Locale.getDefault(); // TODO
|
||||
DataBuffer dataBuffer = getBufferAllocator().allocateBuffer();
|
||||
try {
|
||||
Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream());
|
||||
getTemplate(locale).process(freeMarkerModel, writer);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
String message = "Could not load FreeMarker template for URL [" + getUrl() + "]";
|
||||
return Flux.error(new IllegalStateException(message, ex));
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
return Flux.error(ex);
|
||||
}
|
||||
return Flux.just(dataBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a FreeMarker template model for the given model Map.
|
||||
* <p>The default implementation builds a {@link SimpleHash}.
|
||||
* @param model the model to use for rendering
|
||||
* @param exchange current exchange
|
||||
* @return the FreeMarker template model, as a {@link SimpleHash} or subclass thereof
|
||||
*/
|
||||
protected SimpleHash getTemplateModel(Map<String, Object> model, ServerWebExchange exchange) {
|
||||
SimpleHash fmModel = new SimpleHash(getObjectWrapper());
|
||||
fmModel.putAll(model);
|
||||
return fmModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured FreeMarker {@link ObjectWrapper}, or the
|
||||
* {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified.
|
||||
* @see freemarker.template.Configuration#getObjectWrapper()
|
||||
*/
|
||||
protected ObjectWrapper getObjectWrapper() {
|
||||
ObjectWrapper ow = getConfiguration().getObjectWrapper();
|
||||
Version version = Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;
|
||||
return (ow != null ? ow : new DefaultObjectWrapperBuilder(version).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the FreeMarker template for the given locale,
|
||||
* to be rendering by this view.
|
||||
* <p>By default, the template specified by the "url" bean property
|
||||
* will be retrieved.
|
||||
* @param locale the current locale
|
||||
* @return the FreeMarker template to render
|
||||
*/
|
||||
protected Template getTemplate(Locale locale) throws IOException {
|
||||
return (getEncoding() != null ?
|
||||
getConfiguration().getTemplate(getUrl(), locale, getEncoding()) :
|
||||
getConfiguration().getTemplate(getUrl(), locale));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view.freemarker;
|
||||
|
||||
import org.springframework.web.reactive.view.UrlBasedViewResolver;
|
||||
|
||||
/**
|
||||
* A {@code ViewResolver} for resolving {@link FreeMarkerView} instances, i.e.
|
||||
* FreeMarker templates and custom subclasses of it.
|
||||
*
|
||||
* <p>The view class for all views generated by this resolver can be specified
|
||||
* via the "viewClass" property. See {@link UrlBasedViewResolver} for details.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/public class FreeMarkerViewResolver extends UrlBasedViewResolver {
|
||||
|
||||
|
||||
/**
|
||||
* Simple constructor.
|
||||
*/
|
||||
public FreeMarkerViewResolver() {
|
||||
setViewClass(requiredViewClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor with a prefix and suffix.
|
||||
* @param suffix the suffix to prepend view names with
|
||||
* @param prefix the prefix to prepend view names with
|
||||
*/
|
||||
public FreeMarkerViewResolver(String prefix, String suffix) {
|
||||
setViewClass(requiredViewClass());
|
||||
setPrefix(prefix);
|
||||
setSuffix(suffix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Requires {@link FreeMarkerView}.
|
||||
*/
|
||||
@Override
|
||||
protected Class<?> requiredViewClass() {
|
||||
return FreeMarkerView.class;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.view.freemarker;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import freemarker.template.Configuration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FreeMarkerViewTests {
|
||||
|
||||
public static final String TEMPLATE_PATH = "classpath*:org/springframework/web/reactive/view/freemarker/";
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private GenericApplicationContext context;
|
||||
|
||||
private Configuration freeMarkerConfig;
|
||||
|
||||
@Rule
|
||||
public final ExpectedException exception = ExpectedException.none();
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.context = new GenericApplicationContext();
|
||||
this.context.refresh();
|
||||
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreferFileSystemAccess(false);
|
||||
configurer.setTemplateLoaderPath(TEMPLATE_PATH);
|
||||
configurer.setResourceLoader(this.context);
|
||||
this.freeMarkerConfig = configurer.createConfiguration();
|
||||
|
||||
FreeMarkerView fv = new FreeMarkerView();
|
||||
fv.setApplicationContext(this.context);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void noFreeMarkerConfig() throws Exception {
|
||||
this.exception.expect(ApplicationContextException.class);
|
||||
this.exception.expectMessage("Must define a single FreeMarkerConfig bean");
|
||||
|
||||
FreeMarkerView view = new FreeMarkerView();
|
||||
view.setApplicationContext(this.context);
|
||||
view.setUrl("anythingButNull");
|
||||
view.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noTemplateName() throws Exception {
|
||||
this.exception.expect(IllegalArgumentException.class);
|
||||
this.exception.expectMessage("Property 'url' is required");
|
||||
|
||||
FreeMarkerView freeMarkerView = new FreeMarkerView();
|
||||
freeMarkerView.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkResourceExists() throws Exception {
|
||||
FreeMarkerView view = new FreeMarkerView();
|
||||
view.setConfiguration(this.freeMarkerConfig);
|
||||
view.setUrl("test.ftl");
|
||||
|
||||
assertTrue(view.checkResourceExists(Locale.US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void render() throws Exception {
|
||||
FreeMarkerView view = new FreeMarkerView();
|
||||
view.setConfiguration(this.freeMarkerConfig);
|
||||
view.setUrl("test.ftl");
|
||||
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
model.addAttribute("hello", "hi FreeMarker");
|
||||
HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model);
|
||||
Flux<DataBuffer> flux = view.render(result, Optional.empty(), this.exchange);
|
||||
|
||||
TestSubscriber<DataBuffer> subscriber = new TestSubscriber<>();
|
||||
subscriber.bindTo(flux).assertValuesWith(dataBuffer ->
|
||||
assertEquals("<html><body>hi FreeMarker</body></html>", asString(dataBuffer)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String asString(DataBuffer dataBuffer) {
|
||||
ByteBuffer byteBuffer = dataBuffer.asByteBuffer();
|
||||
final byte[] bytes = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(bytes);
|
||||
return new String(bytes, UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<html><body>${hello}</body></html>
|
||||
Reference in New Issue
Block a user