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