• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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