Browse Source

Add FreeMarker View, ViewResolver and basic hierarchy

pull/1111/head
Rossen Stoyanchev 9 years ago
parent
commit
29db80c3e8
  1. 2
      spring-web-reactive/build.gradle
  2. 2
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java
  3. 85
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java
  4. 166
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java
  5. 168
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java
  6. 14
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java
  7. 116
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java
  8. 39
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java
  9. 116
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java
  10. 219
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java
  11. 58
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java
  12. 150
      spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java
  13. 1
      spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl

2
spring-web-reactive/build.gradle

@ -84,6 +84,7 @@ dependencies { @@ -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 { @@ -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"

2
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java

@ -66,7 +66,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @@ -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) {

85
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java

@ -0,0 +1,85 @@ @@ -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() + "]";
}
}

166
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java

@ -0,0 +1,166 @@ @@ -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();
}
}

168
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java

@ -0,0 +1,168 @@ @@ -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);
}
}

14
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java

@ -24,6 +24,7 @@ import java.util.Optional; @@ -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; @@ -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 { @@ -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)

116
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java

@ -0,0 +1,116 @@ @@ -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;
}
}

39
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java

@ -0,0 +1,39 @@ @@ -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();
}

116
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java

@ -0,0 +1,116 @@ @@ -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">
* &lt;#import "/spring.ftl" as spring/&gt;
* &lt;@spring.bind "person.age"/&gt;
* 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;
}
}

219
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java

@ -0,0 +1,219 @@ @@ -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));
}
}

58
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java

@ -0,0 +1,58 @@ @@ -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;
}
}

150
spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java

@ -0,0 +1,150 @@ @@ -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);
}
}

1
spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl

@ -0,0 +1 @@ @@ -0,0 +1 @@
<html><body>${hello}</body></html>
Loading…
Cancel
Save