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}