001/* 002 * Copyright 2002-2016 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.context.request; 018 019import java.util.HashSet; 020import java.util.Map; 021import java.util.Set; 022import java.util.concurrent.ConcurrentHashMap; 023import javax.servlet.http.HttpServletRequest; 024import javax.servlet.http.HttpServletResponse; 025import javax.servlet.http.HttpSession; 026 027import org.springframework.util.Assert; 028import org.springframework.util.NumberUtils; 029import org.springframework.util.StringUtils; 030import org.springframework.web.util.WebUtils; 031 032/** 033 * Servlet-based implementation of the {@link RequestAttributes} interface. 034 * 035 * <p>Accesses objects from servlet request and HTTP session scope, 036 * with no distinction between "session" and "global session". 037 * 038 * @author Juergen Hoeller 039 * @since 2.0 040 * @see javax.servlet.ServletRequest#getAttribute 041 * @see javax.servlet.http.HttpSession#getAttribute 042 */ 043public class ServletRequestAttributes extends AbstractRequestAttributes { 044 045 /** 046 * Constant identifying the {@link String} prefixed to the name of a 047 * destruction callback when it is stored in a {@link HttpSession}. 048 */ 049 public static final String DESTRUCTION_CALLBACK_NAME_PREFIX = 050 ServletRequestAttributes.class.getName() + ".DESTRUCTION_CALLBACK."; 051 052 protected static final Set<Class<?>> immutableValueTypes = new HashSet<Class<?>>(16); 053 054 static { 055 immutableValueTypes.addAll(NumberUtils.STANDARD_NUMBER_TYPES); 056 immutableValueTypes.add(Boolean.class); 057 immutableValueTypes.add(Character.class); 058 immutableValueTypes.add(String.class); 059 } 060 061 062 private final HttpServletRequest request; 063 064 private HttpServletResponse response; 065 066 private volatile HttpSession session; 067 068 private final Map<String, Object> sessionAttributesToUpdate = new ConcurrentHashMap<String, Object>(1); 069 070 071 /** 072 * Create a new ServletRequestAttributes instance for the given request. 073 * @param request current HTTP request 074 */ 075 public ServletRequestAttributes(HttpServletRequest request) { 076 Assert.notNull(request, "Request must not be null"); 077 this.request = request; 078 } 079 080 /** 081 * Create a new ServletRequestAttributes instance for the given request. 082 * @param request current HTTP request 083 * @param response current HTTP response (for optional exposure) 084 */ 085 public ServletRequestAttributes(HttpServletRequest request, HttpServletResponse response) { 086 this(request); 087 this.response = response; 088 } 089 090 091 /** 092 * Exposes the native {@link HttpServletRequest} that we're wrapping. 093 */ 094 public final HttpServletRequest getRequest() { 095 return this.request; 096 } 097 098 /** 099 * Exposes the native {@link HttpServletResponse} that we're wrapping (if any). 100 */ 101 public final HttpServletResponse getResponse() { 102 return this.response; 103 } 104 105 /** 106 * Exposes the {@link HttpSession} that we're wrapping. 107 * @param allowCreate whether to allow creation of a new session if none exists yet 108 */ 109 protected final HttpSession getSession(boolean allowCreate) { 110 if (isRequestActive()) { 111 HttpSession session = this.request.getSession(allowCreate); 112 this.session = session; 113 return session; 114 } 115 else { 116 // Access through stored session reference, if any... 117 HttpSession session = this.session; 118 if (session == null) { 119 if (allowCreate) { 120 throw new IllegalStateException( 121 "No session found and request already completed - cannot create new session!"); 122 } 123 else { 124 session = this.request.getSession(false); 125 this.session = session; 126 } 127 } 128 return session; 129 } 130 } 131 132 133 @Override 134 public Object getAttribute(String name, int scope) { 135 if (scope == SCOPE_REQUEST) { 136 if (!isRequestActive()) { 137 throw new IllegalStateException( 138 "Cannot ask for request attribute - request is not active anymore!"); 139 } 140 return this.request.getAttribute(name); 141 } 142 else { 143 HttpSession session = getSession(false); 144 if (session != null) { 145 try { 146 Object value = session.getAttribute(name); 147 if (value != null) { 148 this.sessionAttributesToUpdate.put(name, value); 149 } 150 return value; 151 } 152 catch (IllegalStateException ex) { 153 // Session invalidated - shouldn't usually happen. 154 } 155 } 156 return null; 157 } 158 } 159 160 @Override 161 public void setAttribute(String name, Object value, int scope) { 162 if (scope == SCOPE_REQUEST) { 163 if (!isRequestActive()) { 164 throw new IllegalStateException( 165 "Cannot set request attribute - request is not active anymore!"); 166 } 167 this.request.setAttribute(name, value); 168 } 169 else { 170 HttpSession session = getSession(true); 171 this.sessionAttributesToUpdate.remove(name); 172 session.setAttribute(name, value); 173 } 174 } 175 176 @Override 177 public void removeAttribute(String name, int scope) { 178 if (scope == SCOPE_REQUEST) { 179 if (isRequestActive()) { 180 this.request.removeAttribute(name); 181 removeRequestDestructionCallback(name); 182 } 183 } 184 else { 185 HttpSession session = getSession(false); 186 if (session != null) { 187 this.sessionAttributesToUpdate.remove(name); 188 try { 189 session.removeAttribute(name); 190 // Remove any registered destruction callback as well. 191 session.removeAttribute(DESTRUCTION_CALLBACK_NAME_PREFIX + name); 192 } 193 catch (IllegalStateException ex) { 194 // Session invalidated - shouldn't usually happen. 195 } 196 } 197 } 198 } 199 200 @Override 201 public String[] getAttributeNames(int scope) { 202 if (scope == SCOPE_REQUEST) { 203 if (!isRequestActive()) { 204 throw new IllegalStateException( 205 "Cannot ask for request attributes - request is not active anymore!"); 206 } 207 return StringUtils.toStringArray(this.request.getAttributeNames()); 208 } 209 else { 210 HttpSession session = getSession(false); 211 if (session != null) { 212 try { 213 return StringUtils.toStringArray(session.getAttributeNames()); 214 } 215 catch (IllegalStateException ex) { 216 // Session invalidated - shouldn't usually happen. 217 } 218 } 219 return new String[0]; 220 } 221 } 222 223 @Override 224 public void registerDestructionCallback(String name, Runnable callback, int scope) { 225 if (scope == SCOPE_REQUEST) { 226 registerRequestDestructionCallback(name, callback); 227 } 228 else { 229 registerSessionDestructionCallback(name, callback); 230 } 231 } 232 233 @Override 234 public Object resolveReference(String key) { 235 if (REFERENCE_REQUEST.equals(key)) { 236 return this.request; 237 } 238 else if (REFERENCE_SESSION.equals(key)) { 239 return getSession(true); 240 } 241 else { 242 return null; 243 } 244 } 245 246 @Override 247 public String getSessionId() { 248 return getSession(true).getId(); 249 } 250 251 @Override 252 public Object getSessionMutex() { 253 return WebUtils.getSessionMutex(getSession(true)); 254 } 255 256 257 /** 258 * Update all accessed session attributes through {@code session.setAttribute} 259 * calls, explicitly indicating to the container that they might have been modified. 260 */ 261 @Override 262 protected void updateAccessedSessionAttributes() { 263 if (!this.sessionAttributesToUpdate.isEmpty()) { 264 // Update all affected session attributes. 265 HttpSession session = getSession(false); 266 if (session != null) { 267 try { 268 for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) { 269 String name = entry.getKey(); 270 Object newValue = entry.getValue(); 271 Object oldValue = session.getAttribute(name); 272 if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) { 273 session.setAttribute(name, newValue); 274 } 275 } 276 } 277 catch (IllegalStateException ex) { 278 // Session invalidated - shouldn't usually happen. 279 } 280 } 281 this.sessionAttributesToUpdate.clear(); 282 } 283 } 284 285 /** 286 * Determine whether the given value is to be considered as an immutable session 287 * attribute, that is, doesn't have to be re-set via {@code session.setAttribute} 288 * since its value cannot meaningfully change internally. 289 * <p>The default implementation returns {@code true} for {@code String}, 290 * {@code Character}, {@code Boolean} and standard {@code Number} values. 291 * @param name the name of the attribute 292 * @param value the corresponding value to check 293 * @return {@code true} if the value is to be considered as immutable for the 294 * purposes of session attribute management; {@code false} otherwise 295 * @see #updateAccessedSessionAttributes() 296 */ 297 protected boolean isImmutableSessionAttribute(String name, Object value) { 298 return (value == null || immutableValueTypes.contains(value.getClass())); 299 } 300 301 /** 302 * Register the given callback as to be executed after session termination. 303 * <p>Note: The callback object should be serializable in order to survive 304 * web app restarts. 305 * @param name the name of the attribute to register the callback for 306 * @param callback the callback to be executed for destruction 307 */ 308 protected void registerSessionDestructionCallback(String name, Runnable callback) { 309 HttpSession session = getSession(true); 310 session.setAttribute(DESTRUCTION_CALLBACK_NAME_PREFIX + name, 311 new DestructionCallbackBindingListener(callback)); 312 } 313 314 315 @Override 316 public String toString() { 317 return this.request.toString(); 318 } 319 320}