ServletScopes.java revision a23937e0145d7bb4cc0c2169d21023bedee3fdb2
1/** 2 * Copyright (C) 2006 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.google.inject.servlet; 18 19import com.google.inject.Key; 20import com.google.inject.OutOfScopeException; 21import com.google.inject.Provider; 22import com.google.inject.Scope; 23import com.google.common.collect.Maps; 24import com.google.common.base.Preconditions; 25import java.util.Map; 26import java.util.concurrent.Callable; 27import javax.servlet.http.HttpServletRequest; 28import javax.servlet.http.HttpSession; 29 30/** 31 * Servlet scopes. 32 * 33 * @author crazybob@google.com (Bob Lee) 34 */ 35public class ServletScopes { 36 37 private ServletScopes() {} 38 39 /** A sentinel attribute value representing null. */ 40 enum NullObject { INSTANCE } 41 42 /** 43 * HTTP servlet request scope. 44 */ 45 public static final Scope REQUEST = new Scope() { 46 public <T> Provider<T> scope(Key<T> key, final Provider<T> creator) { 47 final String name = key.toString(); 48 return new Provider<T>() { 49 public T get() { 50 // Check if the alternate request scope should be used, if no HTTP 51 // request is in progress. 52 if (null == GuiceFilter.localContext.get()) { 53 54 // NOTE(user): We don't need to synchronize on the scope map 55 // unlike the HTTP request because we're the only ones who have 56 // a reference to it, and it is only available via a threadlocal. 57 Map<String, Object> scopeMap = requestScopeContext.get(); 58 if (null != scopeMap) { 59 @SuppressWarnings("unchecked") 60 T t = (T) scopeMap.get(name); 61 62 // Accounts for @Nullable providers. 63 if (NullObject.INSTANCE == t) { 64 return null; 65 } 66 67 if (t == null) { 68 t = creator.get(); 69 // Store a sentinel for provider-given null values. 70 scopeMap.put(name, t != null ? t : NullObject.INSTANCE); 71 } 72 73 return t; 74 } // else: fall into normal HTTP request scope and out of scope 75 // exception is thrown. 76 } 77 78 HttpServletRequest request = GuiceFilter.getRequest(); 79 80 synchronized (request) { 81 Object obj = request.getAttribute(name); 82 if (NullObject.INSTANCE == obj) { 83 return null; 84 } 85 @SuppressWarnings("unchecked") 86 T t = (T) obj; 87 if (t == null) { 88 t = creator.get(); 89 request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); 90 } 91 return t; 92 } 93 } 94 95 public String toString() { 96 return String.format("%s[%s]", creator, REQUEST); 97 } 98 }; 99 } 100 101 public String toString() { 102 return "ServletScopes.REQUEST"; 103 } 104 }; 105 106 /** 107 * HTTP session scope. 108 */ 109 public static final Scope SESSION = new Scope() { 110 public <T> Provider<T> scope(Key<T> key, final Provider<T> creator) { 111 final String name = key.toString(); 112 return new Provider<T>() { 113 public T get() { 114 HttpSession session = GuiceFilter.getRequest().getSession(); 115 synchronized (session) { 116 Object obj = session.getAttribute(name); 117 if (NullObject.INSTANCE == obj) { 118 return null; 119 } 120 @SuppressWarnings("unchecked") 121 T t = (T) obj; 122 if (t == null) { 123 t = creator.get(); 124 session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); 125 } 126 return t; 127 } 128 } 129 public String toString() { 130 return String.format("%s[%s]", creator, SESSION); 131 } 132 }; 133 } 134 135 public String toString() { 136 return "ServletScopes.SESSION"; 137 } 138 }; 139 140 /** 141 * Wraps the given callable in a contextual callable that "continues" the 142 * HTTP request in another thread. This acts as a way of transporting 143 * request context data from the request processing thread to to worker 144 * threads. 145 * <p> 146 * There are some limitations: 147 * <ul> 148 * <li>Derived objects (i.e. anything marked @RequestScoped will not be 149 * transported.</li> 150 * <li>State changes to the HttpServletRequest after this method is called 151 * will not be seen in the continued thread.</li> 152 * <li>Only the HttpServletRequest, ServletContext and request parameter 153 * map are available in the continued thread. The response and session 154 * are not available.</li> 155 * </ul> 156 * 157 * @param callable code to be executed in another thread, which depends on 158 * the request scope. 159 * @param seedMap the initial set of scoped instances for Guice to seed the 160 * request scope with. To seed a key with null, use {@code null} as 161 * the value. 162 * @return a callable that will invoke the given callable, making the request 163 * context available to it. 164 * @throws OutOfScopeException if this method is called from a non-request 165 * thread, or if the request has completed. 166 * 167 * @since 3.0 168 */ 169 public static <T> Callable<T> continueRequest(final Callable<T> callable, 170 final Map<Key<?>, Object> seedMap) { 171 Preconditions.checkArgument(null != seedMap, 172 "Seed map cannot be null, try passing in Collections.emptyMap() instead."); 173 174 // Snapshot the seed map and add all the instances to our continuing HTTP request. 175 final ContinuingHttpServletRequest continuingRequest = 176 new ContinuingHttpServletRequest(GuiceFilter.getRequest()); 177 for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) { 178 Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue()); 179 continuingRequest.setAttribute(entry.getKey().toString(), value); 180 } 181 182 return new Callable<T>() { 183 private HttpServletRequest request = continuingRequest; 184 185 public T call() throws Exception { 186 GuiceFilter.Context context = GuiceFilter.localContext.get(); 187 Preconditions.checkState(null == context, 188 "Cannot continue request in the same thread as a HTTP request!"); 189 190 // Only set up the request continuation if we're running in a 191 // new vanilla thread. 192 GuiceFilter.localContext.set(new GuiceFilter.Context(request, null)); 193 try { 194 return callable.call(); 195 } finally { 196 // Clear the copied context if we set one up. 197 if (null == context) { 198 GuiceFilter.localContext.remove(); 199 } 200 } 201 } 202 }; 203 } 204 205 /** 206 * A threadlocal scope map for non-http request scopes. The {@link #REQUEST} 207 * scope falls back to this scope map if no http request is available, and 208 * requires {@link #scopeRequest} to be called as an alertnative. 209 */ 210 private static final ThreadLocal<Map<String, Object>> requestScopeContext 211 = new ThreadLocal<Map<String, Object>>(); 212 213 /** 214 * Scopes the given callable inside a request scope. This is not the same 215 * as the HTTP request scope, but is used if no HTTP request scope is in 216 * progress. In this way, keys can be scoped as @RequestScoped and exist 217 * in non-HTTP requests (for example: RPC requests) as well as in HTTP 218 * request threads. 219 * 220 * @param callable code to be executed which depends on the request scope. 221 * Typically in another thread, but not necessarily so. 222 * @param seedMap the initial set of scoped instances for Guice to seed the 223 * request scope with. To seed a key with null, use {@code null} as 224 * the value. 225 * @return a callable that when called will run inside the a request scope 226 * that exposes the instances in the {@code seedMap} as scoped keys. 227 * @since 3.0 228 */ 229 public static <T> Callable<T> scopeRequest(final Callable<T> callable, 230 Map<Key<?>, Object> seedMap) { 231 Preconditions.checkArgument(null != seedMap, 232 "Seed map cannot be null, try passing in Collections.emptyMap() instead."); 233 234 // Copy the seed values into our local scope map. 235 final Map<String, Object> scopeMap = Maps.newHashMap(); 236 for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) { 237 Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue()); 238 scopeMap.put(entry.getKey().toString(), value); 239 } 240 241 return new Callable<T>() { 242 public T call() throws Exception { 243 Preconditions.checkState(null == GuiceFilter.localContext.get(), 244 "An HTTP request is already in progress, cannot scope a new request in this thread."); 245 Preconditions.checkState(null == requestScopeContext.get(), 246 "A request scope is already in progress, cannot scope a new request in this thread."); 247 248 requestScopeContext.set(scopeMap); 249 250 try { 251 return callable.call(); 252 } finally { 253 requestScopeContext.remove(); 254 } 255 } 256 }; 257 } 258 259 /** 260 * Validates the key and object, ensuring the value matches the key type, and 261 * canonicalizing null objects to the null sentinel. 262 */ 263 private static Object validateAndCanonicalizeValue(Key<?> key, Object object) { 264 if (object == null || object == NullObject.INSTANCE) { 265 return NullObject.INSTANCE; 266 } 267 268 if (!key.getTypeLiteral().getRawType().isInstance(object)) { 269 throw new IllegalArgumentException("Value[" + object + "] of type[" 270 + object.getClass().getName() + "] is not compatible with key[" + key + "]"); 271 } 272 273 return object; 274 } 275} 276