/*
* Copyright 2002-2017 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.result.view;
import java.util.Locale;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.PatternMatchUtils;
/**
* A {@link 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.result.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
* @author Sebastien Deleuze
* @since 5.0
*/
public class UrlBasedViewResolver extends ViewResolverSupport implements ViewResolver, InitializingBean {
/**
* Prefix for special view names that specify a redirect URL (usually
* to a controller after a form has been submitted and processed).
* Such view names will not be resolved in the configured default
* way but rather be treated as special shortcut.
*/
public static final String REDIRECT_URL_PREFIX = "redirect:";
private Class<?> viewClass;
private String prefix = "";
private String suffix = "";
private String[] viewNames;
private Function<String, RedirectView> redirectViewProvider = RedirectView::new;
private String requestContextAttribute;
/**
* 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;
}
/**
* Set the view names (or name patterns) that can be handled by this
* {@link ViewResolver}. View names can contain simple wildcards such that
* 'my*', '*Report' and '*Repo*' will all match the view name 'myReport'.
* @see #canHandle
*/
public void setViewNames(String... viewNames) {
this.viewNames = viewNames;
}
/**
* Return the view names (or name patterns) that can be handled by this
* {@link ViewResolver}.
*/
protected String[] getViewNames() {
return this.viewNames;
}
/**
* URL based {@link RedirectView} provider which can be used to provide, for example,
* redirect views with a custom default status code.
*/
public void setRedirectViewProvider(Function<String, RedirectView> redirectViewProvider) {
this.redirectViewProvider = redirectViewProvider;
}
/**
* Set the name of the RequestContext attribute for all views.
* @param requestContextAttribute name of the RequestContext attribute
* @see AbstractView#setRequestContextAttribute
*/
public void setRequestContextAttribute(String requestContextAttribute) {
this.requestContextAttribute = requestContextAttribute;
}
/**
* Return the name of the RequestContext attribute for all views, if any.
*/
protected String getRequestContextAttribute() {
return this.requestContextAttribute;
}
@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) {
if (!canHandle(viewName, locale)) {
return Mono.empty();
}
AbstractUrlBasedView urlBasedView;
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
urlBasedView = this.redirectViewProvider.apply(redirectUrl);
}
else {
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);
}
}
/**
* Indicates whether or not this {@link ViewResolver} can handle the
* supplied view name. If not, an empty result is returned. The default
* implementation checks against the configured {@link #setViewNames
* view names}.
* @param viewName the name of the view to retrieve
* @param locale the Locale to retrieve the view for
* @return whether this resolver applies to the specified view
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
*/
protected boolean canHandle(String viewName, Locale locale) {
String[] viewNames = getViewNames();
return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
}
/**
* 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.setRequestContextAttribute(getRequestContextAttribute());
view.setDefaultCharset(getDefaultCharset());
view.setUrl(getPrefix() + viewName + getSuffix());
return view;
}
private View applyLifecycleMethods(String viewName, AbstractView view) {
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
}
}