• 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