• 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 
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