001/* 002 * Copyright 2002-2018 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.servlet.config.annotation; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.List; 023 024import org.springframework.beans.factory.BeanFactoryUtils; 025import org.springframework.beans.factory.BeanInitializationException; 026import org.springframework.context.ApplicationContext; 027import org.springframework.core.Ordered; 028import org.springframework.lang.Nullable; 029import org.springframework.util.CollectionUtils; 030import org.springframework.util.ObjectUtils; 031import org.springframework.web.accept.ContentNegotiationManager; 032import org.springframework.web.servlet.View; 033import org.springframework.web.servlet.ViewResolver; 034import org.springframework.web.servlet.view.BeanNameViewResolver; 035import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; 036import org.springframework.web.servlet.view.InternalResourceViewResolver; 037import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; 038import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; 039import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer; 040import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver; 041import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer; 042import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver; 043import org.springframework.web.servlet.view.tiles3.TilesConfigurer; 044import org.springframework.web.servlet.view.tiles3.TilesViewResolver; 045 046/** 047 * Assist with the configuration of a chain of 048 * {@link org.springframework.web.servlet.ViewResolver ViewResolver} instances. 049 * This class is expected to be used via {@link WebMvcConfigurer#configureViewResolvers}. 050 * 051 * @author Sebastien Deleuze 052 * @author Rossen Stoyanchev 053 * @since 4.1 054 */ 055public class ViewResolverRegistry { 056 057 @Nullable 058 private ContentNegotiationManager contentNegotiationManager; 059 060 @Nullable 061 private ApplicationContext applicationContext; 062 063 @Nullable 064 private ContentNegotiatingViewResolver contentNegotiatingResolver; 065 066 private final List<ViewResolver> viewResolvers = new ArrayList<>(4); 067 068 @Nullable 069 private Integer order; 070 071 072 /** 073 * Class constructor with {@link ContentNegotiationManager} and {@link ApplicationContext}. 074 * @since 4.3.12 075 */ 076 public ViewResolverRegistry( 077 ContentNegotiationManager contentNegotiationManager, @Nullable ApplicationContext context) { 078 079 this.contentNegotiationManager = contentNegotiationManager; 080 this.applicationContext = context; 081 } 082 083 084 /** 085 * Whether any view resolvers have been registered. 086 */ 087 public boolean hasRegistrations() { 088 return (this.contentNegotiatingResolver != null || !this.viewResolvers.isEmpty()); 089 } 090 091 /** 092 * Enable use of a {@link ContentNegotiatingViewResolver} to front all other 093 * configured view resolvers and select among all selected Views based on 094 * media types requested by the client (e.g. in the Accept header). 095 * <p>If invoked multiple times the provided default views will be added to 096 * any other default views that may have been configured already. 097 * @see ContentNegotiatingViewResolver#setDefaultViews 098 */ 099 public void enableContentNegotiation(View... defaultViews) { 100 initContentNegotiatingViewResolver(defaultViews); 101 } 102 103 /** 104 * Enable use of a {@link ContentNegotiatingViewResolver} to front all other 105 * configured view resolvers and select among all selected Views based on 106 * media types requested by the client (e.g. in the Accept header). 107 * <p>If invoked multiple times the provided default views will be added to 108 * any other default views that may have been configured already. 109 * @see ContentNegotiatingViewResolver#setDefaultViews 110 */ 111 public void enableContentNegotiation(boolean useNotAcceptableStatus, View... defaultViews) { 112 ContentNegotiatingViewResolver vr = initContentNegotiatingViewResolver(defaultViews); 113 vr.setUseNotAcceptableStatusCode(useNotAcceptableStatus); 114 } 115 116 private ContentNegotiatingViewResolver initContentNegotiatingViewResolver(View[] defaultViews) { 117 // ContentNegotiatingResolver in the registry: elevate its precedence! 118 this.order = (this.order != null ? this.order : Ordered.HIGHEST_PRECEDENCE); 119 120 if (this.contentNegotiatingResolver != null) { 121 if (!ObjectUtils.isEmpty(defaultViews) && 122 !CollectionUtils.isEmpty(this.contentNegotiatingResolver.getDefaultViews())) { 123 List<View> views = new ArrayList<>(this.contentNegotiatingResolver.getDefaultViews()); 124 views.addAll(Arrays.asList(defaultViews)); 125 this.contentNegotiatingResolver.setDefaultViews(views); 126 } 127 } 128 else { 129 this.contentNegotiatingResolver = new ContentNegotiatingViewResolver(); 130 this.contentNegotiatingResolver.setDefaultViews(Arrays.asList(defaultViews)); 131 this.contentNegotiatingResolver.setViewResolvers(this.viewResolvers); 132 if (this.contentNegotiationManager != null) { 133 this.contentNegotiatingResolver.setContentNegotiationManager(this.contentNegotiationManager); 134 } 135 } 136 return this.contentNegotiatingResolver; 137 } 138 139 /** 140 * Register JSP view resolver using a default view name prefix of "/WEB-INF/" 141 * and a default suffix of ".jsp". 142 * <p>When this method is invoked more than once, each call will register a 143 * new ViewResolver instance. Note that since it's not easy to determine 144 * if a JSP exists without forwarding to it, using multiple JSP-based view 145 * resolvers only makes sense in combination with the "viewNames" property 146 * on the resolver indicating which view names are handled by which resolver. 147 */ 148 public UrlBasedViewResolverRegistration jsp() { 149 return jsp("/WEB-INF/", ".jsp"); 150 } 151 152 /** 153 * Register JSP view resolver with the specified prefix and suffix. 154 * <p>When this method is invoked more than once, each call will register a 155 * new ViewResolver instance. Note that since it's not easy to determine 156 * if a JSP exists without forwarding to it, using multiple JSP-based view 157 * resolvers only makes sense in combination with the "viewNames" property 158 * on the resolver indicating which view names are handled by which resolver. 159 */ 160 public UrlBasedViewResolverRegistration jsp(String prefix, String suffix) { 161 InternalResourceViewResolver resolver = new InternalResourceViewResolver(); 162 resolver.setPrefix(prefix); 163 resolver.setSuffix(suffix); 164 this.viewResolvers.add(resolver); 165 return new UrlBasedViewResolverRegistration(resolver); 166 } 167 168 /** 169 * Register Tiles 3.x view resolver. 170 * <p><strong>Note</strong> that you must also configure Tiles by adding a 171 * {@link org.springframework.web.servlet.view.tiles3.TilesConfigurer} bean. 172 */ 173 public UrlBasedViewResolverRegistration tiles() { 174 if (!checkBeanOfType(TilesConfigurer.class)) { 175 throw new BeanInitializationException("In addition to a Tiles view resolver " + 176 "there must also be a single TilesConfigurer bean in this web application context " + 177 "(or its parent)."); 178 } 179 TilesRegistration registration = new TilesRegistration(); 180 this.viewResolvers.add(registration.getViewResolver()); 181 return registration; 182 } 183 184 /** 185 * Register a FreeMarker view resolver with an empty default view name 186 * prefix and a default suffix of ".ftl". 187 * <p><strong>Note</strong> that you must also configure FreeMarker by adding a 188 * {@link org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer} bean. 189 */ 190 public UrlBasedViewResolverRegistration freeMarker() { 191 if (!checkBeanOfType(FreeMarkerConfigurer.class)) { 192 throw new BeanInitializationException("In addition to a FreeMarker view resolver " + 193 "there must also be a single FreeMarkerConfig bean in this web application context " + 194 "(or its parent): FreeMarkerConfigurer is the usual implementation. " + 195 "This bean may be given any name."); 196 } 197 FreeMarkerRegistration registration = new FreeMarkerRegistration(); 198 this.viewResolvers.add(registration.getViewResolver()); 199 return registration; 200 } 201 202 /** 203 * Register a Groovy markup view resolver with an empty default view name 204 * prefix and a default suffix of ".tpl". 205 */ 206 public UrlBasedViewResolverRegistration groovy() { 207 if (!checkBeanOfType(GroovyMarkupConfigurer.class)) { 208 throw new BeanInitializationException("In addition to a Groovy markup view resolver " + 209 "there must also be a single GroovyMarkupConfig bean in this web application context " + 210 "(or its parent): GroovyMarkupConfigurer is the usual implementation. " + 211 "This bean may be given any name."); 212 } 213 GroovyMarkupRegistration registration = new GroovyMarkupRegistration(); 214 this.viewResolvers.add(registration.getViewResolver()); 215 return registration; 216 } 217 218 /** 219 * Register a script template view resolver with an empty default view name prefix and suffix. 220 * @since 4.2 221 */ 222 public UrlBasedViewResolverRegistration scriptTemplate() { 223 if (!checkBeanOfType(ScriptTemplateConfigurer.class)) { 224 throw new BeanInitializationException("In addition to a script template view resolver " + 225 "there must also be a single ScriptTemplateConfig bean in this web application context " + 226 "(or its parent): ScriptTemplateConfigurer is the usual implementation. " + 227 "This bean may be given any name."); 228 } 229 ScriptRegistration registration = new ScriptRegistration(); 230 this.viewResolvers.add(registration.getViewResolver()); 231 return registration; 232 } 233 234 /** 235 * Register a bean name view resolver that interprets view names as the names 236 * of {@link org.springframework.web.servlet.View} beans. 237 */ 238 public void beanName() { 239 BeanNameViewResolver resolver = new BeanNameViewResolver(); 240 this.viewResolvers.add(resolver); 241 } 242 243 /** 244 * Register a {@link ViewResolver} bean instance. This may be useful to 245 * configure a custom (or 3rd party) resolver implementation. It may also be 246 * used as an alternative to other registration methods in this class when 247 * they don't expose some more advanced property that needs to be set. 248 */ 249 public void viewResolver(ViewResolver viewResolver) { 250 if (viewResolver instanceof ContentNegotiatingViewResolver) { 251 throw new BeanInitializationException( 252 "addViewResolver cannot be used to configure a ContentNegotiatingViewResolver. " + 253 "Please use the method enableContentNegotiation instead."); 254 } 255 this.viewResolvers.add(viewResolver); 256 } 257 258 /** 259 * ViewResolver's registered through this registry are encapsulated in an 260 * instance of {@link org.springframework.web.servlet.view.ViewResolverComposite 261 * ViewResolverComposite} and follow the order of registration. 262 * This property determines the order of the ViewResolverComposite itself 263 * relative to any additional ViewResolver's (not registered here) present in 264 * the Spring configuration 265 * <p>By default this property is not set, which means the resolver is ordered 266 * at {@link Ordered#LOWEST_PRECEDENCE} unless content negotiation is enabled 267 * in which case the order (if not set explicitly) is changed to 268 * {@link Ordered#HIGHEST_PRECEDENCE}. 269 */ 270 public void order(int order) { 271 this.order = order; 272 } 273 274 275 private boolean checkBeanOfType(Class<?> beanType) { 276 return (this.applicationContext == null || 277 !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors( 278 this.applicationContext, beanType, false, false))); 279 } 280 281 protected int getOrder() { 282 return (this.order != null ? this.order : Ordered.LOWEST_PRECEDENCE); 283 } 284 285 protected List<ViewResolver> getViewResolvers() { 286 if (this.contentNegotiatingResolver != null) { 287 return Collections.<ViewResolver>singletonList(this.contentNegotiatingResolver); 288 } 289 else { 290 return this.viewResolvers; 291 } 292 } 293 294 295 private static class TilesRegistration extends UrlBasedViewResolverRegistration { 296 297 public TilesRegistration() { 298 super(new TilesViewResolver()); 299 } 300 } 301 302 private static class FreeMarkerRegistration extends UrlBasedViewResolverRegistration { 303 304 public FreeMarkerRegistration() { 305 super(new FreeMarkerViewResolver()); 306 getViewResolver().setSuffix(".ftl"); 307 } 308 } 309 310 311 private static class GroovyMarkupRegistration extends UrlBasedViewResolverRegistration { 312 313 public GroovyMarkupRegistration() { 314 super(new GroovyMarkupViewResolver()); 315 getViewResolver().setSuffix(".tpl"); 316 } 317 } 318 319 320 private static class ScriptRegistration extends UrlBasedViewResolverRegistration { 321 322 public ScriptRegistration() { 323 super(new ScriptTemplateViewResolver()); 324 getViewResolver(); 325 } 326 } 327 328}