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