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}