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