ServletScopes.java revision 4a4d8257ed41d8e2be03fd59a6c9dcf00c2379de
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.common.base.Preconditions; 20import com.google.common.collect.ImmutableSet; 21import com.google.common.collect.Maps; 22import com.google.common.collect.Maps.EntryTransformer; 23import com.google.inject.Binding; 24import com.google.inject.Injector; 25import com.google.inject.Key; 26import com.google.inject.OutOfScopeException; 27import com.google.inject.Provider; 28import com.google.inject.Scope; 29import com.google.inject.Scopes; 30 31import java.util.Map; 32import java.util.concurrent.Callable; 33 34import javax.servlet.http.HttpServletRequest; 35import javax.servlet.http.HttpServletResponse; 36import javax.servlet.http.HttpSession; 37 38/** 39 * Servlet scopes. 40 * 41 * @author crazybob@google.com (Bob Lee) 42 */ 43public class ServletScopes { 44 45 private ServletScopes() {} 46 47 /** Keys bound in request-scope which are handled directly by GuiceFilter. */ 48 private static final ImmutableSet<Key<?>> REQUEST_CONTEXT_KEYS = ImmutableSet.of( 49 Key.get(HttpServletRequest.class), 50 Key.get(HttpServletResponse.class), 51 new Key<Map<String, String[]>>(RequestParameters.class) {}); 52 53 /** 54 * A threadlocal scope map for non-http request scopes. The {@link #REQUEST} 55 * scope falls back to this scope map if no http request is available, and 56 * requires {@link #scopeRequest} to be called as an alternative. 57 */ 58 private static final ThreadLocal<Context> requestScopeContext 59 = new ThreadLocal<Context>(); 60 61 /** A sentinel attribute value representing null. */ 62 enum NullObject { INSTANCE } 63 64 /** 65 * HTTP servlet request scope. 66 */ 67 public static final Scope REQUEST = new Scope() { 68 public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { 69 return new Provider<T>() { 70 public T get() { 71 // Check if the alternate request scope should be used, if no HTTP 72 // request is in progress. 73 if (null == GuiceFilter.localContext.get()) { 74 75 // NOTE(dhanji): We don't need to synchronize on the scope map 76 // unlike the HTTP request because we're the only ones who have 77 // a reference to it, and it is only available via a threadlocal. 78 Context context = requestScopeContext.get(); 79 if (null != context) { 80 @SuppressWarnings("unchecked") 81 T t = (T) context.map.get(key); 82 83 // Accounts for @Nullable providers. 84 if (NullObject.INSTANCE == t) { 85 return null; 86 } 87 88 if (t == null) { 89 t = creator.get(); 90 if (!Scopes.isCircularProxy(t)) { 91 // Store a sentinel for provider-given null values. 92 context.map.put(key, t != null ? t : NullObject.INSTANCE); 93 } 94 } 95 96 return t; 97 } // else: fall into normal HTTP request scope and out of scope 98 // exception is thrown. 99 } 100 101 // Always synchronize and get/set attributes on the underlying request 102 // object since Filters may wrap the request and change the value of 103 // {@code GuiceFilter.getRequest()}. 104 // 105 // This _correctly_ throws up if the thread is out of scope. 106 HttpServletRequest request = GuiceFilter.getOriginalRequest(key); 107 if (REQUEST_CONTEXT_KEYS.contains(key)) { 108 // Don't store these keys as attributes, since they are handled by 109 // GuiceFilter itself. 110 return creator.get(); 111 } 112 String name = key.toString(); 113 synchronized (request) { 114 Object obj = request.getAttribute(name); 115 if (NullObject.INSTANCE == obj) { 116 return null; 117 } 118 @SuppressWarnings("unchecked") 119 T t = (T) obj; 120 if (t == null) { 121 t = creator.get(); 122 if (!Scopes.isCircularProxy(t)) { 123 request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); 124 } 125 } 126 return t; 127 } 128 } 129 130 @Override 131 public String toString() { 132 return String.format("%s[%s]", creator, REQUEST); 133 } 134 }; 135 } 136 137 @Override 138 public String toString() { 139 return "ServletScopes.REQUEST"; 140 } 141 }; 142 143 /** 144 * HTTP session scope. 145 */ 146 public static final Scope SESSION = new Scope() { 147 public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { 148 final String name = key.toString(); 149 return new Provider<T>() { 150 public T get() { 151 HttpSession session = GuiceFilter.getRequest(key).getSession(); 152 synchronized (session) { 153 Object obj = session.getAttribute(name); 154 if (NullObject.INSTANCE == obj) { 155 return null; 156 } 157 @SuppressWarnings("unchecked") 158 T t = (T) obj; 159 if (t == null) { 160 t = creator.get(); 161 if (!Scopes.isCircularProxy(t)) { 162 session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); 163 } 164 } 165 return t; 166 } 167 } 168 @Override 169 public String toString() { 170 return String.format("%s[%s]", creator, SESSION); 171 } 172 }; 173 } 174 175 @Override 176 public String toString() { 177 return "ServletScopes.SESSION"; 178 } 179 }; 180 181 /** 182 * Wraps the given callable in a contextual callable that "continues" the 183 * HTTP request in another thread. This acts as a way of transporting 184 * request context data from the request processing thread to to worker 185 * threads. 186 * <p> 187 * There are some limitations: 188 * <ul> 189 * <li>Derived objects (i.e. anything marked @RequestScoped will not be 190 * transported.</li> 191 * <li>State changes to the HttpServletRequest after this method is called 192 * will not be seen in the continued thread.</li> 193 * <li>Only the HttpServletRequest, ServletContext and request parameter 194 * map are available in the continued thread. The response and session 195 * are not available.</li> 196 * </ul> 197 * 198 * <p>The returned callable will throw a {@link ScopingException} when called 199 * if the HTTP request scope is still active on the current thread. 200 * 201 * @param callable code to be executed in another thread, which depends on 202 * the request scope. 203 * @param seedMap the initial set of scoped instances for Guice to seed the 204 * request scope with. To seed a key with null, use {@code null} as 205 * the value. 206 * @return a callable that will invoke the given callable, making the request 207 * context available to it. 208 * @throws OutOfScopeException if this method is called from a non-request 209 * thread, or if the request has completed. 210 * 211 * @since 3.0 212 */ 213 public static <T> Callable<T> continueRequest(final Callable<T> callable, 214 final Map<Key<?>, Object> seedMap) { 215 Preconditions.checkArgument(null != seedMap, 216 "Seed map cannot be null, try passing in Collections.emptyMap() instead."); 217 218 // Snapshot the seed map and add all the instances to our continuing HTTP request. 219 final ContinuingHttpServletRequest continuingRequest = 220 new ContinuingHttpServletRequest( 221 GuiceFilter.getRequest(Key.get(HttpServletRequest.class))); 222 for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) { 223 Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue()); 224 continuingRequest.setAttribute(entry.getKey().toString(), value); 225 } 226 227 return new Callable<T>() { 228 public T call() throws Exception { 229 checkScopingState(null == GuiceFilter.localContext.get(), 230 "Cannot continue request in the same thread as a HTTP request!"); 231 return new GuiceFilter.Context(continuingRequest, continuingRequest, null) 232 .call(callable); 233 } 234 }; 235 } 236 237 /** 238 * Wraps the given callable in a contextual callable that "transfers" the 239 * request to another thread. This acts as a way of transporting 240 * request context data from the current thread to a future thread. 241 * 242 * <p>As opposed to {@link #continueRequest}, this method propagates all 243 * existing scoped objects. The primary use case is in server implementations 244 * where you can detach the request processing thread while waiting for data, 245 * and reattach to a different thread to finish processing at a later time. 246 * 247 * <p>Because request-scoped objects are not typically thread-safe, the 248 * callable returned by this method must not be run on a different thread 249 * until the current request scope has terminated. The returned callable will 250 * block until the current thread has released the request scope. 251 * 252 * @param callable code to be executed in another thread, which depends on 253 * the request scope. 254 * @return a callable that will invoke the given callable, making the request 255 * context available to it. 256 * @throws OutOfScopeException if this method is called from a non-request 257 * thread, or if the request has completed. 258 */ 259 public static <T> Callable<T> transferRequest(Callable<T> callable) { 260 return (GuiceFilter.localContext.get() != null) 261 ? transferHttpRequest(callable) 262 : transferNonHttpRequest(callable); 263 } 264 265 private static <T> Callable<T> transferHttpRequest(final Callable<T> callable) { 266 final GuiceFilter.Context context = GuiceFilter.localContext.get(); 267 if (context == null) { 268 throw new OutOfScopeException("Not in a request scope"); 269 } 270 return new Callable<T>() { 271 public T call() throws Exception { 272 return context.call(callable); 273 } 274 }; 275 } 276 277 private static <T> Callable<T> transferNonHttpRequest(final Callable<T> callable) { 278 final Context context = requestScopeContext.get(); 279 if (context == null) { 280 throw new OutOfScopeException("Not in a request scope"); 281 } 282 return new Callable<T>() { 283 public T call() throws Exception { 284 return context.call(callable); 285 } 286 }; 287 } 288 289 /** 290 * Returns true if {@code binding} is request-scoped. If the binding is a 291 * {@link com.google.inject.spi.LinkedKeyBinding linked key binding} and 292 * belongs to an injector (i. e. it was retrieved via 293 * {@link Injector#getBinding Injector.getBinding()}), then this method will 294 * also return true if the target binding is request-scoped. 295 */ 296 public static boolean isRequestScoped(Binding<?> binding) { 297 return Scopes.isScoped(binding, ServletScopes.REQUEST, RequestScoped.class); 298 } 299 300 /** 301 * Scopes the given callable inside a request scope. This is not the same 302 * as the HTTP request scope, but is used if no HTTP request scope is in 303 * progress. In this way, keys can be scoped as @RequestScoped and exist 304 * in non-HTTP requests (for example: RPC requests) as well as in HTTP 305 * request threads. 306 * 307 * <p>The returned callable will throw a {@link ScopingException} when called 308 * if there is a request scope already active on the current thread. 309 * 310 * @param callable code to be executed which depends on the request scope. 311 * Typically in another thread, but not necessarily so. 312 * @param seedMap the initial set of scoped instances for Guice to seed the 313 * request scope with. To seed a key with null, use {@code null} as 314 * the value. 315 * @return a callable that when called will run inside the a request scope 316 * that exposes the instances in the {@code seedMap} as scoped keys. 317 * @since 3.0 318 */ 319 public static <T> Callable<T> scopeRequest(final Callable<T> callable, 320 Map<Key<?>, Object> seedMap) { 321 Preconditions.checkArgument(null != seedMap, 322 "Seed map cannot be null, try passing in Collections.emptyMap() instead."); 323 324 // Copy the seed values into our local scope map. 325 final Context context = new Context(); 326 Map<Key<?>, Object> validatedAndCanonicalizedMap = 327 Maps.transformEntries(seedMap, new EntryTransformer<Key<?>, Object, Object>() { 328 @Override public Object transformEntry(Key<?> key, Object value) { 329 return validateAndCanonicalizeValue(key, value); 330 } 331 }); 332 context.map.putAll(validatedAndCanonicalizedMap); 333 334 return new Callable<T>() { 335 public T call() throws Exception { 336 checkScopingState(null == GuiceFilter.localContext.get(), 337 "An HTTP request is already in progress, cannot scope a new request in this thread."); 338 checkScopingState(null == requestScopeContext.get(), 339 "A request scope is already in progress, cannot scope a new request in this thread."); 340 return context.call(callable); 341 } 342 }; 343 } 344 345 /** 346 * Validates the key and object, ensuring the value matches the key type, and 347 * canonicalizing null objects to the null sentinel. 348 */ 349 private static Object validateAndCanonicalizeValue(Key<?> key, Object object) { 350 if (object == null || object == NullObject.INSTANCE) { 351 return NullObject.INSTANCE; 352 } 353 354 if (!key.getTypeLiteral().getRawType().isInstance(object)) { 355 throw new IllegalArgumentException("Value[" + object + "] of type[" 356 + object.getClass().getName() + "] is not compatible with key[" + key + "]"); 357 } 358 359 return object; 360 } 361 362 private static class Context { 363 final Map<Key, Object> map = Maps.newHashMap(); 364 365 // Synchronized to prevent two threads from using the same request 366 // scope concurrently. 367 synchronized <T> T call(Callable<T> callable) throws Exception { 368 Context previous = requestScopeContext.get(); 369 requestScopeContext.set(this); 370 try { 371 return callable.call(); 372 } finally { 373 requestScopeContext.set(previous); 374 } 375 } 376 } 377 378 private static void checkScopingState(boolean condition, String msg) { 379 if (!condition) { 380 throw new ScopingException(msg); 381 } 382 } 383} 384