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.method.support;
018
019import java.util.HashSet;
020import java.util.Map;
021import java.util.Set;
022
023import org.springframework.http.HttpStatus;
024import org.springframework.lang.Nullable;
025import org.springframework.ui.Model;
026import org.springframework.ui.ModelMap;
027import org.springframework.validation.support.BindingAwareModelMap;
028import org.springframework.web.bind.support.SessionStatus;
029import org.springframework.web.bind.support.SimpleSessionStatus;
030
031/**
032 * Records model and view related decisions made by
033 * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} and
034 * {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers} during the course of invocation of
035 * a controller method.
036 *
037 * <p>The {@link #setRequestHandled} flag can be used to indicate the request
038 * has been handled directly and view resolution is not required.
039 *
040 * <p>A default {@link Model} is automatically created at instantiation.
041 * An alternate model instance may be provided via {@link #setRedirectModel}
042 * for use in a redirect scenario. When {@link #setRedirectModelScenario} is set
043 * to {@code true} signalling a redirect scenario, the {@link #getModel()}
044 * returns the redirect model instead of the default model.
045 *
046 * @author Rossen Stoyanchev
047 * @author Juergen Hoeller
048 * @since 3.1
049 */
050public class ModelAndViewContainer {
051
052        private boolean ignoreDefaultModelOnRedirect = false;
053
054        @Nullable
055        private Object view;
056
057        private final ModelMap defaultModel = new BindingAwareModelMap();
058
059        @Nullable
060        private ModelMap redirectModel;
061
062        private boolean redirectModelScenario = false;
063
064        @Nullable
065        private HttpStatus status;
066
067        private final Set<String> noBinding = new HashSet<>(4);
068
069        private final Set<String> bindingDisabled = new HashSet<>(4);
070
071        private final SessionStatus sessionStatus = new SimpleSessionStatus();
072
073        private boolean requestHandled = false;
074
075
076        /**
077         * By default the content of the "default" model is used both during
078         * rendering and redirect scenarios. Alternatively controller methods
079         * can declare an argument of type {@code RedirectAttributes} and use
080         * it to provide attributes to prepare the redirect URL.
081         * <p>Setting this flag to {@code true} guarantees the "default" model is
082         * never used in a redirect scenario even if a RedirectAttributes argument
083         * is not declared. Setting it to {@code false} means the "default" model
084         * may be used in a redirect if the controller method doesn't declare a
085         * RedirectAttributes argument.
086         * <p>The default setting is {@code false}.
087         */
088        public void setIgnoreDefaultModelOnRedirect(boolean ignoreDefaultModelOnRedirect) {
089                this.ignoreDefaultModelOnRedirect = ignoreDefaultModelOnRedirect;
090        }
091
092        /**
093         * Set a view name to be resolved by the DispatcherServlet via a ViewResolver.
094         * Will override any pre-existing view name or View.
095         */
096        public void setViewName(@Nullable String viewName) {
097                this.view = viewName;
098        }
099
100        /**
101         * Return the view name to be resolved by the DispatcherServlet via a
102         * ViewResolver, or {@code null} if a View object is set.
103         */
104        @Nullable
105        public String getViewName() {
106                return (this.view instanceof String ? (String) this.view : null);
107        }
108
109        /**
110         * Set a View object to be used by the DispatcherServlet.
111         * Will override any pre-existing view name or View.
112         */
113        public void setView(@Nullable Object view) {
114                this.view = view;
115        }
116
117        /**
118         * Return the View object, or {@code null} if we using a view name
119         * to be resolved by the DispatcherServlet via a ViewResolver.
120         */
121        @Nullable
122        public Object getView() {
123                return this.view;
124        }
125
126        /**
127         * Whether the view is a view reference specified via a name to be
128         * resolved by the DispatcherServlet via a ViewResolver.
129         */
130        public boolean isViewReference() {
131                return (this.view instanceof String);
132        }
133
134        /**
135         * Return the model to use -- either the "default" or the "redirect" model.
136         * The default model is used if {@code redirectModelScenario=false} or
137         * there is no redirect model (i.e. RedirectAttributes was not declared as
138         * a method argument) and {@code ignoreDefaultModelOnRedirect=false}.
139         */
140        public ModelMap getModel() {
141                if (useDefaultModel()) {
142                        return this.defaultModel;
143                }
144                else {
145                        if (this.redirectModel == null) {
146                                this.redirectModel = new ModelMap();
147                        }
148                        return this.redirectModel;
149                }
150        }
151
152        /**
153         * Whether to use the default model or the redirect model.
154         */
155        private boolean useDefaultModel() {
156                return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
157        }
158
159        /**
160         * Return the "default" model created at instantiation.
161         * <p>In general it is recommended to use {@link #getModel()} instead which
162         * returns either the "default" model (template rendering) or the "redirect"
163         * model (redirect URL preparation). Use of this method may be needed for
164         * advanced cases when access to the "default" model is needed regardless,
165         * e.g. to save model attributes specified via {@code @SessionAttributes}.
166         * @return the default model (never {@code null})
167         * @since 4.1.4
168         */
169        public ModelMap getDefaultModel() {
170                return this.defaultModel;
171        }
172
173        /**
174         * Provide a separate model instance to use in a redirect scenario.
175         * <p>The provided additional model however is not used unless
176         * {@link #setRedirectModelScenario} gets set to {@code true}
177         * to signal an actual redirect scenario.
178         */
179        public void setRedirectModel(ModelMap redirectModel) {
180                this.redirectModel = redirectModel;
181        }
182
183        /**
184         * Whether the controller has returned a redirect instruction, e.g. a
185         * "redirect:" prefixed view name, a RedirectView instance, etc.
186         */
187        public void setRedirectModelScenario(boolean redirectModelScenario) {
188                this.redirectModelScenario = redirectModelScenario;
189        }
190
191        /**
192         * Provide an HTTP status that will be passed on to with the
193         * {@code ModelAndView} used for view rendering purposes.
194         * @since 4.3
195         */
196        public void setStatus(@Nullable HttpStatus status) {
197                this.status = status;
198        }
199
200        /**
201         * Return the configured HTTP status, if any.
202         * @since 4.3
203         */
204        @Nullable
205        public HttpStatus getStatus() {
206                return this.status;
207        }
208
209        /**
210         * Programmatically register an attribute for which data binding should not occur,
211         * not even for a subsequent {@code @ModelAttribute} declaration.
212         * @param attributeName the name of the attribute
213         * @since 4.3
214         */
215        public void setBindingDisabled(String attributeName) {
216                this.bindingDisabled.add(attributeName);
217        }
218
219        /**
220         * Whether binding is disabled for the given model attribute.
221         * @since 4.3
222         */
223        public boolean isBindingDisabled(String name) {
224                return (this.bindingDisabled.contains(name) || this.noBinding.contains(name));
225        }
226
227        /**
228         * Register whether data binding should occur for a corresponding model attribute,
229         * corresponding to an {@code @ModelAttribute(binding=true/false)} declaration.
230         * <p>Note: While this flag will be taken into account by {@link #isBindingDisabled},
231         * a hard {@link #setBindingDisabled} declaration will always override it.
232         * @param attributeName the name of the attribute
233         * @since 4.3.13
234         */
235        public void setBinding(String attributeName, boolean enabled) {
236                if (!enabled) {
237                        this.noBinding.add(attributeName);
238                }
239                else {
240                        this.noBinding.remove(attributeName);
241                }
242        }
243
244        /**
245         * Return the {@link SessionStatus} instance to use that can be used to
246         * signal that session processing is complete.
247         */
248        public SessionStatus getSessionStatus() {
249                return this.sessionStatus;
250        }
251
252        /**
253         * Whether the request has been handled fully within the handler, e.g.
254         * {@code @ResponseBody} method, and therefore view resolution is not
255         * necessary. This flag can also be set when controller methods declare an
256         * argument of type {@code ServletResponse} or {@code OutputStream}).
257         * <p>The default value is {@code false}.
258         */
259        public void setRequestHandled(boolean requestHandled) {
260                this.requestHandled = requestHandled;
261        }
262
263        /**
264         * Whether the request has been handled fully within the handler.
265         */
266        public boolean isRequestHandled() {
267                return this.requestHandled;
268        }
269
270        /**
271         * Add the supplied attribute to the underlying model.
272         * A shortcut for {@code getModel().addAttribute(String, Object)}.
273         */
274        public ModelAndViewContainer addAttribute(String name, @Nullable Object value) {
275                getModel().addAttribute(name, value);
276                return this;
277        }
278
279        /**
280         * Add the supplied attribute to the underlying model.
281         * A shortcut for {@code getModel().addAttribute(Object)}.
282         */
283        public ModelAndViewContainer addAttribute(Object value) {
284                getModel().addAttribute(value);
285                return this;
286        }
287
288        /**
289         * Copy all attributes to the underlying model.
290         * A shortcut for {@code getModel().addAllAttributes(Map)}.
291         */
292        public ModelAndViewContainer addAllAttributes(@Nullable Map<String, ?> attributes) {
293                getModel().addAllAttributes(attributes);
294                return this;
295        }
296
297        /**
298         * Copy attributes in the supplied {@code Map} with existing objects of
299         * the same name taking precedence (i.e. not getting replaced).
300         * A shortcut for {@code getModel().mergeAttributes(Map<String, ?>)}.
301         */
302        public ModelAndViewContainer mergeAttributes(@Nullable Map<String, ?> attributes) {
303                getModel().mergeAttributes(attributes);
304                return this;
305        }
306
307        /**
308         * Remove the given attributes from the model.
309         */
310        public ModelAndViewContainer removeAttributes(@Nullable Map<String, ?> attributes) {
311                if (attributes != null) {
312                        for (String key : attributes.keySet()) {
313                                getModel().remove(key);
314                        }
315                }
316                return this;
317        }
318
319        /**
320         * Whether the underlying model contains the given attribute name.
321         * A shortcut for {@code getModel().containsAttribute(String)}.
322         */
323        public boolean containsAttribute(String name) {
324                return getModel().containsAttribute(name);
325        }
326
327
328        /**
329         * Return diagnostic information.
330         */
331        @Override
332        public String toString() {
333                StringBuilder sb = new StringBuilder("ModelAndViewContainer: ");
334                if (!isRequestHandled()) {
335                        if (isViewReference()) {
336                                sb.append("reference to view with name '").append(this.view).append("'");
337                        }
338                        else {
339                                sb.append("View is [").append(this.view).append(']');
340                        }
341                        if (useDefaultModel()) {
342                                sb.append("; default model ");
343                        }
344                        else {
345                                sb.append("; redirect model ");
346                        }
347                        sb.append(getModel());
348                }
349                else {
350                        sb.append("Request handled directly");
351                }
352                return sb.toString();
353        }
354
355}