blob: 54c120e5c4f8287bdd558c2577e3cede70c1d564 [file] [log] [blame]
crazyboblee3a09e292007-02-08 22:36:21 +00001/**
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
sberlinb7a02b02011-07-08 00:34:16 +000019import com.google.common.base.Preconditions;
guice.mirrorbot@gmail.come09d8bf2011-09-27 15:36:19 +000020import com.google.common.collect.ImmutableSet;
sberlinb7a02b02011-07-08 00:34:16 +000021import com.google.common.collect.Maps;
Sam Berlin883fe032014-03-10 12:49:05 -040022import com.google.common.collect.Maps.EntryTransformer;
Sam Berlinb2f55822012-01-13 18:20:50 -050023import com.google.inject.Binding;
24import com.google.inject.Injector;
crazyboblee3a09e292007-02-08 22:36:21 +000025import com.google.inject.Key;
dhanji1848a292010-09-13 22:18:14 +000026import com.google.inject.OutOfScopeException;
kevinb9nb6054822007-03-19 03:17:47 +000027import com.google.inject.Provider;
28import com.google.inject.Scope;
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +000029import com.google.inject.Scopes;
sberlinb7a02b02011-07-08 00:34:16 +000030
dhanji0693a152010-09-20 18:25:29 +000031import java.util.Map;
dhanji1848a292010-09-13 22:18:14 +000032import java.util.concurrent.Callable;
sberlinb7a02b02011-07-08 00:34:16 +000033
crazyboblee3a09e292007-02-08 22:36:21 +000034import javax.servlet.http.HttpServletRequest;
guice.mirrorbot@gmail.come09d8bf2011-09-27 15:36:19 +000035import javax.servlet.http.HttpServletResponse;
crazyboblee3a09e292007-02-08 22:36:21 +000036import 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
Sam Berlinb2f55822012-01-13 18:20:50 -050047 /**
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
Sam Berlin72460882013-12-06 17:05:17 -050050 * requires {@link #scopeRequest} to be called as an alternative.
Sam Berlinb2f55822012-01-13 18:20:50 -050051 */
Sam Berlin7dc62e52012-05-27 13:39:27 -040052 private static final ThreadLocal<Context> requestScopeContext
53 = new ThreadLocal<Context>();
Sam Berlinb2f55822012-01-13 18:20:50 -050054
limpbizkite93f2602009-08-12 19:24:11 +000055 /** A sentinel attribute value representing null. */
56 enum NullObject { INSTANCE }
57
crazyboblee3a09e292007-02-08 22:36:21 +000058 /**
crazyboblee3a09e292007-02-08 22:36:21 +000059 * HTTP servlet request scope.
60 */
61 public static final Scope REQUEST = new Scope() {
guice.mirrorbot@gmail.come09d8bf2011-09-27 15:36:19 +000062 public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
crazybobleebd9544e2007-02-25 20:32:11 +000063 return new Provider<T>() {
Jonathan Haber99233332014-10-03 07:07:03 -040064
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
crazyboblee3a09e292007-02-08 22:36:21 +000071 public T get() {
dhanjib8d25742010-09-20 18:41:51 +000072 // Check if the alternate request scope should be used, if no HTTP
73 // request is in progress.
dhanji0693a152010-09-20 18:25:29 +000074 if (null == GuiceFilter.localContext.get()) {
75
sberlinec761792011-06-29 22:04:31 +000076 // NOTE(dhanji): We don't need to synchronize on the scope map
dhanji0693a152010-09-20 18:25:29 +000077 // 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.
Sam Berlin7dc62e52012-05-27 13:39:27 -040079 Context context = requestScopeContext.get();
80 if (null != context) {
dhanji0693a152010-09-20 18:25:29 +000081 @SuppressWarnings("unchecked")
Sam Berlin5e5e2f52013-12-06 17:04:18 -050082 T t = (T) context.map.get(key);
dhanji0693a152010-09-20 18:25:29 +000083
84 // Accounts for @Nullable providers.
85 if (NullObject.INSTANCE == t) {
86 return null;
87 }
88
89 if (t == null) {
90 t = creator.get();
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +000091 if (!Scopes.isCircularProxy(t)) {
92 // Store a sentinel for provider-given null values.
Sam Berlin5e5e2f52013-12-06 17:04:18 -050093 context.map.put(key, t != null ? t : NullObject.INSTANCE);
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +000094 }
dhanji0693a152010-09-20 18:25:29 +000095 }
96
97 return t;
98 } // else: fall into normal HTTP request scope and out of scope
99 // exception is thrown.
100 }
101
guice.mirrorbot@gmail.come09d8bf2011-09-27 15:36:19 +0000102 // 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.
Sam Berlinc33e73c2014-03-10 12:50:34 -0400107 HttpServletRequest request = GuiceFilter.getOriginalRequest(key);
guice.mirrorbot@gmail.come09d8bf2011-09-27 15:36:19 +0000108 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 }
Sam Berlin5e5e2f52013-12-06 17:04:18 -0500113 String name = key.toString();
crazyboblee3a09e292007-02-08 22:36:21 +0000114 synchronized (request) {
limpbizkite93f2602009-08-12 19:24:11 +0000115 Object obj = request.getAttribute(name);
116 if (NullObject.INSTANCE == obj) {
117 return null;
118 }
crazyboblee3a09e292007-02-08 22:36:21 +0000119 @SuppressWarnings("unchecked")
limpbizkite93f2602009-08-12 19:24:11 +0000120 T t = (T) obj;
crazyboblee3a09e292007-02-08 22:36:21 +0000121 if (t == null) {
122 t = creator.get();
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +0000123 if (!Scopes.isCircularProxy(t)) {
124 request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
125 }
crazyboblee3a09e292007-02-08 22:36:21 +0000126 }
127 return t;
128 }
129 }
kevinb9n61e081c2007-03-22 07:04:03 +0000130
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +0000131 @Override
kevinb9n61e081c2007-03-22 07:04:03 +0000132 public String toString() {
kevinb9nda11d0d2007-04-20 15:58:38 +0000133 return String.format("%s[%s]", creator, REQUEST);
kevinb9n61e081c2007-03-22 07:04:03 +0000134 }
crazyboblee3a09e292007-02-08 22:36:21 +0000135 };
136 }
crazybobleef33d23e2007-02-12 04:17:48 +0000137
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +0000138 @Override
crazybobleef33d23e2007-02-12 04:17:48 +0000139 public String toString() {
crazyboblee91c37e32007-02-15 05:18:57 +0000140 return "ServletScopes.REQUEST";
crazybobleef33d23e2007-02-12 04:17:48 +0000141 }
crazyboblee3a09e292007-02-08 22:36:21 +0000142 };
143
144 /**
crazyboblee3a09e292007-02-08 22:36:21 +0000145 * HTTP session scope.
146 */
147 public static final Scope SESSION = new Scope() {
Sam Berlinc33e73c2014-03-10 12:50:34 -0400148 public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
crazyboblee3a09e292007-02-08 22:36:21 +0000149 final String name = key.toString();
crazybobleebd9544e2007-02-25 20:32:11 +0000150 return new Provider<T>() {
crazyboblee3a09e292007-02-08 22:36:21 +0000151 public T get() {
Sam Berlinc33e73c2014-03-10 12:50:34 -0400152 HttpSession session = GuiceFilter.getRequest(key).getSession();
crazyboblee3a09e292007-02-08 22:36:21 +0000153 synchronized (session) {
limpbizkite93f2602009-08-12 19:24:11 +0000154 Object obj = session.getAttribute(name);
155 if (NullObject.INSTANCE == obj) {
156 return null;
157 }
crazyboblee3a09e292007-02-08 22:36:21 +0000158 @SuppressWarnings("unchecked")
limpbizkite93f2602009-08-12 19:24:11 +0000159 T t = (T) obj;
crazyboblee3a09e292007-02-08 22:36:21 +0000160 if (t == null) {
161 t = creator.get();
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +0000162 if (!Scopes.isCircularProxy(t)) {
163 session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
164 }
crazyboblee3a09e292007-02-08 22:36:21 +0000165 }
166 return t;
167 }
168 }
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +0000169 @Override
kevinb9n61e081c2007-03-22 07:04:03 +0000170 public String toString() {
kevinb9nda11d0d2007-04-20 15:58:38 +0000171 return String.format("%s[%s]", creator, SESSION);
kevinb9n61e081c2007-03-22 07:04:03 +0000172 }
crazyboblee3a09e292007-02-08 22:36:21 +0000173 };
174 }
crazybobleef33d23e2007-02-12 04:17:48 +0000175
guice.mirrorbot@gmail.com05bf8e52011-09-27 15:37:30 +0000176 @Override
crazybobleef33d23e2007-02-12 04:17:48 +0000177 public String toString() {
crazyboblee91c37e32007-02-15 05:18:57 +0000178 return "ServletScopes.SESSION";
crazybobleef33d23e2007-02-12 04:17:48 +0000179 }
crazyboblee3a09e292007-02-08 22:36:21 +0000180 };
dhanji1848a292010-09-13 22:18:14 +0000181
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 *
Christian Edward Gruber9111f482013-05-15 13:16:23 -0700199 * <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 *
dhanji1848a292010-09-13 22:18:14 +0000202 * @param callable code to be executed in another thread, which depends on
203 * the request scope.
sberlin21967862010-11-24 14:52:33 +0000204 * @param seedMap the initial set of scoped instances for Guice to seed the
sberlinb886ce32010-12-01 02:59:06 +0000205 * request scope with. To seed a key with null, use {@code null} as
206 * the value.
dhanji1848a292010-09-13 22:18:14 +0000207 * @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.
sberlinec761792011-06-29 22:04:31 +0000211 *
sberlinc13b5452010-10-31 18:38:24 +0000212 * @since 3.0
dhanji1848a292010-09-13 22:18:14 +0000213 */
dhanji0693a152010-09-20 18:25:29 +0000214 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.");
Sam Berlin7dc62e52012-05-27 13:39:27 -0400218
dhanji0693a152010-09-20 18:25:29 +0000219 // Snapshot the seed map and add all the instances to our continuing HTTP request.
220 final ContinuingHttpServletRequest continuingRequest =
Sam Berlinc33e73c2014-03-10 12:50:34 -0400221 new ContinuingHttpServletRequest(
222 GuiceFilter.getRequest(Key.get(HttpServletRequest.class)));
dhanji0693a152010-09-20 18:25:29 +0000223 for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) {
sberlin21967862010-11-24 14:52:33 +0000224 Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue());
225 continuingRequest.setAttribute(entry.getKey().toString(), value);
dhanji0693a152010-09-20 18:25:29 +0000226 }
227
dhanji1848a292010-09-13 22:18:14 +0000228 return new Callable<T>() {
dhanji1848a292010-09-13 22:18:14 +0000229 public T call() throws Exception {
Christian Edward Gruber9111f482013-05-15 13:16:23 -0700230 checkScopingState(null == GuiceFilter.localContext.get(),
dhanji0693a152010-09-20 18:25:29 +0000231 "Cannot continue request in the same thread as a HTTP request!");
Sam Berlin7dc62e52012-05-27 13:39:27 -0400232 return new GuiceFilter.Context(continuingRequest, continuingRequest, null)
233 .call(callable);
234 }
235 };
236 }
dhanji0693a152010-09-20 18:25:29 +0000237
Sam Berlin7dc62e52012-05-27 13:39:27 -0400238 /**
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 *
Sam Berlin4a4d8252014-05-10 10:34:44 -0400248 * <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.
Sam Berlin7dc62e52012-05-27 13:39:27 -0400252 *
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.
Ben McCannbac730f2015-04-21 16:21:01 -0700259 * @since 4.0
Sam Berlin7dc62e52012-05-27 13:39:27 -0400260 */
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);
dhanji1848a292010-09-13 22:18:14 +0000287 }
288 };
289 }
dhanji0693a152010-09-20 18:25:29 +0000290
291 /**
Sam Berlinb2f55822012-01-13 18:20:50 -0500292 * 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.
Ben McCannbac730f2015-04-21 16:21:01 -0700297 *
298 * @since 4.0
dhanji0693a152010-09-20 18:25:29 +0000299 */
Sam Berlinb2f55822012-01-13 18:20:50 -0500300 public static boolean isRequestScoped(Binding<?> binding) {
Sam Berlin04cdfd92012-01-17 11:29:38 -0500301 return Scopes.isScoped(binding, ServletScopes.REQUEST, RequestScoped.class);
Sam Berlinb2f55822012-01-13 18:20:50 -0500302 }
dhanji0693a152010-09-20 18:25:29 +0000303
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 *
Christian Edward Gruber9111f482013-05-15 13:16:23 -0700311 * <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 *
dhanji0693a152010-09-20 18:25:29 +0000314 * @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
sberlinb886ce32010-12-01 02:59:06 +0000317 * request scope with. To seed a key with null, use {@code null} as
318 * the value.
dhanji0693a152010-09-20 18:25:29 +0000319 * @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.
sberlinc13b5452010-10-31 18:38:24 +0000321 * @since 3.0
dhanji0693a152010-09-20 18:25:29 +0000322 */
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.
Sam Berlin7dc62e52012-05-27 13:39:27 -0400329 final Context context = new Context();
Sam Berlin883fe032014-03-10 12:49:05 -0400330 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);
dhanji0693a152010-09-20 18:25:29 +0000337
338 return new Callable<T>() {
339 public T call() throws Exception {
Christian Edward Gruber9111f482013-05-15 13:16:23 -0700340 checkScopingState(null == GuiceFilter.localContext.get(),
dhanji0693a152010-09-20 18:25:29 +0000341 "An HTTP request is already in progress, cannot scope a new request in this thread.");
Christian Edward Gruber9111f482013-05-15 13:16:23 -0700342 checkScopingState(null == requestScopeContext.get(),
dhanji0693a152010-09-20 18:25:29 +0000343 "A request scope is already in progress, cannot scope a new request in this thread.");
Sam Berlin7dc62e52012-05-27 13:39:27 -0400344 return context.call(callable);
dhanji0693a152010-09-20 18:25:29 +0000345 }
346 };
347 }
sberlin21967862010-11-24 14:52:33 +0000348
349 /**
sberlin21967862010-11-24 14:52:33 +0000350 * 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 }
Sam Berlin7dc62e52012-05-27 13:39:27 -0400365
366 private static class Context {
Sam Berlin5e5e2f52013-12-06 17:04:18 -0500367 final Map<Key, Object> map = Maps.newHashMap();
Sam Berlin7dc62e52012-05-27 13:39:27 -0400368
Sam Berlin4a4d8252014-05-10 10:34:44 -0400369 // Synchronized to prevent two threads from using the same request
370 // scope concurrently.
371 synchronized <T> T call(Callable<T> callable) throws Exception {
Sam Berlin7dc62e52012-05-27 13:39:27 -0400372 Context previous = requestScopeContext.get();
373 requestScopeContext.set(this);
374 try {
375 return callable.call();
376 } finally {
Sam Berlin7dc62e52012-05-27 13:39:27 -0400377 requestScopeContext.set(previous);
378 }
379 }
380 }
Christian Edward Gruber9111f482013-05-15 13:16:23 -0700381
382 private static void checkScopingState(boolean condition, String msg) {
383 if (!condition) {
384 throw new ScopingException(msg);
385 }
386 }
crazyboblee3a09e292007-02-08 22:36:21 +0000387}