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}