001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.reactive.result.view; 018 019import java.util.Locale; 020import java.util.function.Function; 021 022import reactor.core.publisher.Mono; 023 024import org.springframework.beans.BeanUtils; 025import org.springframework.beans.factory.InitializingBean; 026import org.springframework.context.ApplicationContext; 027import org.springframework.context.ApplicationContextAware; 028import org.springframework.lang.Nullable; 029import org.springframework.util.Assert; 030import org.springframework.util.PatternMatchUtils; 031 032/** 033 * A {@link ViewResolver} that allows direct resolution of symbolic view names 034 * to URLs without explicit mapping definitions. This is useful if symbolic names 035 * match the names of view resources in a straightforward manner (i.e. the 036 * symbolic name is the unique part of the resource's filename), without the need 037 * for a dedicated mapping to be defined for each view. 038 * 039 * <p>Supports {@link AbstractUrlBasedView} subclasses like 040 * {@link org.springframework.web.reactive.result.view.freemarker.FreeMarkerView}. 041 * The view class for all views generated by this resolver can be specified 042 * via the "viewClass" property. 043 * 044 * <p>View names can either be resource URLs themselves, or get augmented by a 045 * specified prefix and/or suffix. Exporting an attribute that holds the 046 * RequestContext to all views is explicitly supported. 047 * 048 * <p>Example: prefix="templates/", suffix=".ftl", viewname="test" -> 049 * "templates/test.ftl" 050 * 051 * <p>As a special feature, redirect URLs can be specified via the "redirect:" 052 * prefix. E.g.: "redirect:myAction" will trigger a redirect to the given 053 * URL, rather than resolution as standard view name. This is typically used 054 * for redirecting to a controller URL after finishing a form workflow. 055 * 056 * <p>Note: This class does not support localized resolution, i.e. resolving 057 * a symbolic view name to different resources depending on the current locale. 058 * 059 * @author Rossen Stoyanchev 060 * @author Sebastien Deleuze 061 * @author Juergen Hoeller 062 * @author Sam Brannen 063 * @since 5.0 064 */ 065public class UrlBasedViewResolver extends ViewResolverSupport 066 implements ViewResolver, ApplicationContextAware, InitializingBean { 067 068 /** 069 * Prefix for special view names that specify a redirect URL (usually 070 * to a controller after a form has been submitted and processed). 071 * Such view names will not be resolved in the configured default 072 * way but rather be treated as special shortcut. 073 */ 074 public static final String REDIRECT_URL_PREFIX = "redirect:"; 075 076 077 @Nullable 078 private Class<?> viewClass; 079 080 private String prefix = ""; 081 082 private String suffix = ""; 083 084 @Nullable 085 private String[] viewNames; 086 087 private Function<String, RedirectView> redirectViewProvider = RedirectView::new; 088 089 @Nullable 090 private String requestContextAttribute; 091 092 @Nullable 093 private ApplicationContext applicationContext; 094 095 096 /** 097 * Set the view class to instantiate through {@link #createView(String)}. 098 * @param viewClass a class that is assignable to the required view class 099 * which by default is AbstractUrlBasedView 100 */ 101 public void setViewClass(@Nullable Class<?> viewClass) { 102 if (viewClass != null && !requiredViewClass().isAssignableFrom(viewClass)) { 103 String name = viewClass.getName(); 104 throw new IllegalArgumentException("Given view class [" + name + "] " + 105 "is not of type [" + requiredViewClass().getName() + "]"); 106 } 107 this.viewClass = viewClass; 108 } 109 110 /** 111 * Return the view class to be used to create views. 112 */ 113 @Nullable 114 protected Class<?> getViewClass() { 115 return this.viewClass; 116 } 117 118 /** 119 * Return the required type of view for this resolver. 120 * This implementation returns {@link AbstractUrlBasedView}. 121 * @see AbstractUrlBasedView 122 */ 123 protected Class<?> requiredViewClass() { 124 return AbstractUrlBasedView.class; 125 } 126 127 /** 128 * Set the prefix that gets prepended to view names when building a URL. 129 */ 130 public void setPrefix(@Nullable String prefix) { 131 this.prefix = (prefix != null ? prefix : ""); 132 } 133 134 /** 135 * Return the prefix that gets prepended to view names when building a URL. 136 */ 137 protected String getPrefix() { 138 return this.prefix; 139 } 140 141 /** 142 * Set the suffix that gets appended to view names when building a URL. 143 */ 144 public void setSuffix(@Nullable String suffix) { 145 this.suffix = (suffix != null ? suffix : ""); 146 } 147 148 /** 149 * Return the suffix that gets appended to view names when building a URL. 150 */ 151 protected String getSuffix() { 152 return this.suffix; 153 } 154 155 /** 156 * Set the view names (or name patterns) that can be handled by this 157 * {@link ViewResolver}. View names can contain simple wildcards such that 158 * 'my*', '*Report' and '*Repo*' will all match the view name 'myReport'. 159 * @see #canHandle 160 */ 161 public void setViewNames(@Nullable String... viewNames) { 162 this.viewNames = viewNames; 163 } 164 165 /** 166 * Return the view names (or name patterns) that can be handled by this 167 * {@link ViewResolver}. 168 */ 169 @Nullable 170 protected String[] getViewNames() { 171 return this.viewNames; 172 } 173 174 /** 175 * URL based {@link RedirectView} provider which can be used to provide, for example, 176 * redirect views with a custom default status code. 177 */ 178 public void setRedirectViewProvider(Function<String, RedirectView> redirectViewProvider) { 179 this.redirectViewProvider = redirectViewProvider; 180 } 181 182 /** 183 * Set the name of the {@link RequestContext} attribute for all views. 184 * @param requestContextAttribute name of the RequestContext attribute 185 * @see AbstractView#setRequestContextAttribute 186 */ 187 public void setRequestContextAttribute(@Nullable String requestContextAttribute) { 188 this.requestContextAttribute = requestContextAttribute; 189 } 190 191 /** 192 * Return the name of the {@link RequestContext} attribute for all views, if any. 193 */ 194 @Nullable 195 protected String getRequestContextAttribute() { 196 return this.requestContextAttribute; 197 } 198 199 /** 200 * Accept the containing {@code ApplicationContext}, if any. 201 * <p>To be used for the initialization of newly created {@link View} instances, 202 * applying lifecycle callbacks and providing access to the containing environment. 203 * @see #setViewClass 204 * @see #createView 205 * @see #applyLifecycleMethods 206 */ 207 @Override 208 public void setApplicationContext(@Nullable ApplicationContext applicationContext) { 209 this.applicationContext = applicationContext; 210 } 211 212 /** 213 * Return the containing {@code ApplicationContext}, if any. 214 * @see #setApplicationContext 215 */ 216 @Nullable 217 public ApplicationContext getApplicationContext() { 218 return this.applicationContext; 219 } 220 221 222 @Override 223 public void afterPropertiesSet() throws Exception { 224 if (getViewClass() == null) { 225 throw new IllegalArgumentException("Property 'viewClass' is required"); 226 } 227 } 228 229 230 @Override 231 public Mono<View> resolveViewName(String viewName, Locale locale) { 232 if (!canHandle(viewName, locale)) { 233 return Mono.empty(); 234 } 235 236 AbstractUrlBasedView urlBasedView; 237 if (viewName.startsWith(REDIRECT_URL_PREFIX)) { 238 String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); 239 urlBasedView = this.redirectViewProvider.apply(redirectUrl); 240 } 241 else { 242 urlBasedView = createView(viewName); 243 } 244 245 View view = applyLifecycleMethods(viewName, urlBasedView); 246 try { 247 return (urlBasedView.checkResourceExists(locale) ? Mono.just(view) : Mono.empty()); 248 } 249 catch (Exception ex) { 250 return Mono.error(ex); 251 } 252 } 253 254 /** 255 * Indicates whether or not this {@link ViewResolver} can handle the supplied 256 * view name. If not, an empty result is returned. The default implementation 257 * checks against the configured {@link #setViewNames view names}. 258 * @param viewName the name of the view to retrieve 259 * @param locale the Locale to retrieve the view for 260 * @return whether this resolver applies to the specified view 261 * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) 262 */ 263 protected boolean canHandle(String viewName, Locale locale) { 264 String[] viewNames = getViewNames(); 265 return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName)); 266 } 267 268 /** 269 * Creates a new View instance of the specified view class and configures it. 270 * <p>Does <i>not</i> perform any lookup for pre-defined View instances. 271 * <p>Spring lifecycle methods as defined by the bean container do not have to 272 * be called here: They will be automatically applied afterwards, provided 273 * that an {@link #setApplicationContext ApplicationContext} is available. 274 * @param viewName the name of the view to build 275 * @return the View instance 276 * @see #getViewClass() 277 * @see #applyLifecycleMethods 278 */ 279 protected AbstractUrlBasedView createView(String viewName) { 280 Class<?> viewClass = getViewClass(); 281 Assert.state(viewClass != null, "No view class"); 282 283 AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); 284 view.setSupportedMediaTypes(getSupportedMediaTypes()); 285 view.setDefaultCharset(getDefaultCharset()); 286 view.setUrl(getPrefix() + viewName + getSuffix()); 287 288 String requestContextAttribute = getRequestContextAttribute(); 289 if (requestContextAttribute != null) { 290 view.setRequestContextAttribute(requestContextAttribute); 291 } 292 293 return view; 294 } 295 296 /** 297 * Apply the containing {@link ApplicationContext}'s lifecycle methods 298 * to the given {@link View} instance, if such a context is available. 299 * @param viewName the name of the view 300 * @param view the freshly created View instance, pre-configured with 301 * {@link AbstractUrlBasedView}'s properties 302 * @return the {@link View} instance to use (either the original one 303 * or a decorated variant) 304 * @see #getApplicationContext() 305 * @see ApplicationContext#getAutowireCapableBeanFactory() 306 * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#initializeBean 307 */ 308 protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) { 309 ApplicationContext context = getApplicationContext(); 310 if (context != null) { 311 Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName); 312 if (initialized instanceof View) { 313 return (View) initialized; 314 } 315 } 316 return view; 317 } 318 319}