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