Rossen Stoyanchev
9 years ago
13 changed files with 1134 additions and 2 deletions
@ -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() + "]"; |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
|
||||
} |
@ -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"> |
||||
* <#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 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue