crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 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 | |
sberlin | b7a02b0 | 2011-07-08 00:34:16 +0000 | [diff] [blame] | 19 | import com.google.common.base.Preconditions; |
guice.mirrorbot@gmail.com | e09d8bf | 2011-09-27 15:36:19 +0000 | [diff] [blame] | 20 | import com.google.common.collect.ImmutableSet; |
sberlin | b7a02b0 | 2011-07-08 00:34:16 +0000 | [diff] [blame] | 21 | import com.google.common.collect.Maps; |
Sam Berlin | 883fe03 | 2014-03-10 12:49:05 -0400 | [diff] [blame] | 22 | import com.google.common.collect.Maps.EntryTransformer; |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 23 | import com.google.inject.Binding; |
| 24 | import com.google.inject.Injector; |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 25 | import com.google.inject.Key; |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 26 | import com.google.inject.OutOfScopeException; |
kevinb9n | b605482 | 2007-03-19 03:17:47 +0000 | [diff] [blame] | 27 | import com.google.inject.Provider; |
| 28 | import com.google.inject.Scope; |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 29 | import com.google.inject.Scopes; |
sberlin | b7a02b0 | 2011-07-08 00:34:16 +0000 | [diff] [blame] | 30 | |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 31 | import java.util.Map; |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 32 | import java.util.concurrent.Callable; |
sberlin | b7a02b0 | 2011-07-08 00:34:16 +0000 | [diff] [blame] | 33 | |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 34 | import javax.servlet.http.HttpServletRequest; |
guice.mirrorbot@gmail.com | e09d8bf | 2011-09-27 15:36:19 +0000 | [diff] [blame] | 35 | import javax.servlet.http.HttpServletResponse; |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 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 | |
| 45 | private ServletScopes() {} |
| 46 | |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 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 |
Sam Berlin | 7246088 | 2013-12-06 17:05:17 -0500 | [diff] [blame] | 50 | * requires {@link #scopeRequest} to be called as an alternative. |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 51 | */ |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 52 | private static final ThreadLocal<Context> requestScopeContext |
| 53 | = new ThreadLocal<Context>(); |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 54 | |
limpbizkit | e93f260 | 2009-08-12 19:24:11 +0000 | [diff] [blame] | 55 | /** A sentinel attribute value representing null. */ |
| 56 | enum NullObject { INSTANCE } |
| 57 | |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 58 | /** |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 59 | * HTTP servlet request scope. |
| 60 | */ |
| 61 | public static final Scope REQUEST = new Scope() { |
guice.mirrorbot@gmail.com | e09d8bf | 2011-09-27 15:36:19 +0000 | [diff] [blame] | 62 | public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { |
crazyboblee | bd9544e | 2007-02-25 20:32:11 +0000 | [diff] [blame] | 63 | return new Provider<T>() { |
Jonathan Haber | 9923333 | 2014-10-03 07:07:03 -0400 | [diff] [blame] | 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 | |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 71 | public T get() { |
dhanji | b8d2574 | 2010-09-20 18:41:51 +0000 | [diff] [blame] | 72 | // Check if the alternate request scope should be used, if no HTTP |
| 73 | // request is in progress. |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 74 | if (null == GuiceFilter.localContext.get()) { |
| 75 | |
sberlin | ec76179 | 2011-06-29 22:04:31 +0000 | [diff] [blame] | 76 | // NOTE(dhanji): We don't need to synchronize on the scope map |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 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. |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 79 | Context context = requestScopeContext.get(); |
| 80 | if (null != context) { |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 81 | @SuppressWarnings("unchecked") |
Sam Berlin | 5e5e2f5 | 2013-12-06 17:04:18 -0500 | [diff] [blame] | 82 | T t = (T) context.map.get(key); |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 83 | |
| 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.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 91 | if (!Scopes.isCircularProxy(t)) { |
| 92 | // Store a sentinel for provider-given null values. |
Sam Berlin | 5e5e2f5 | 2013-12-06 17:04:18 -0500 | [diff] [blame] | 93 | context.map.put(key, t != null ? t : NullObject.INSTANCE); |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 94 | } |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 95 | } |
| 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.com | e09d8bf | 2011-09-27 15:36:19 +0000 | [diff] [blame] | 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. |
Sam Berlin | c33e73c | 2014-03-10 12:50:34 -0400 | [diff] [blame] | 107 | HttpServletRequest request = GuiceFilter.getOriginalRequest(key); |
guice.mirrorbot@gmail.com | e09d8bf | 2011-09-27 15:36:19 +0000 | [diff] [blame] | 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 | } |
Sam Berlin | 5e5e2f5 | 2013-12-06 17:04:18 -0500 | [diff] [blame] | 113 | String name = key.toString(); |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 114 | synchronized (request) { |
limpbizkit | e93f260 | 2009-08-12 19:24:11 +0000 | [diff] [blame] | 115 | Object obj = request.getAttribute(name); |
| 116 | if (NullObject.INSTANCE == obj) { |
| 117 | return null; |
| 118 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 119 | @SuppressWarnings("unchecked") |
limpbizkit | e93f260 | 2009-08-12 19:24:11 +0000 | [diff] [blame] | 120 | T t = (T) obj; |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 121 | if (t == null) { |
| 122 | t = creator.get(); |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 123 | if (!Scopes.isCircularProxy(t)) { |
| 124 | request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); |
| 125 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 126 | } |
| 127 | return t; |
| 128 | } |
| 129 | } |
kevinb9n | 61e081c | 2007-03-22 07:04:03 +0000 | [diff] [blame] | 130 | |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 131 | @Override |
kevinb9n | 61e081c | 2007-03-22 07:04:03 +0000 | [diff] [blame] | 132 | public String toString() { |
kevinb9n | da11d0d | 2007-04-20 15:58:38 +0000 | [diff] [blame] | 133 | return String.format("%s[%s]", creator, REQUEST); |
kevinb9n | 61e081c | 2007-03-22 07:04:03 +0000 | [diff] [blame] | 134 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 135 | }; |
| 136 | } |
crazyboblee | f33d23e | 2007-02-12 04:17:48 +0000 | [diff] [blame] | 137 | |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 138 | @Override |
crazyboblee | f33d23e | 2007-02-12 04:17:48 +0000 | [diff] [blame] | 139 | public String toString() { |
crazyboblee | 91c37e3 | 2007-02-15 05:18:57 +0000 | [diff] [blame] | 140 | return "ServletScopes.REQUEST"; |
crazyboblee | f33d23e | 2007-02-12 04:17:48 +0000 | [diff] [blame] | 141 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 142 | }; |
| 143 | |
| 144 | /** |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 145 | * HTTP session scope. |
| 146 | */ |
| 147 | public static final Scope SESSION = new Scope() { |
Sam Berlin | c33e73c | 2014-03-10 12:50:34 -0400 | [diff] [blame] | 148 | public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 149 | final String name = key.toString(); |
crazyboblee | bd9544e | 2007-02-25 20:32:11 +0000 | [diff] [blame] | 150 | return new Provider<T>() { |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 151 | public T get() { |
Sam Berlin | c33e73c | 2014-03-10 12:50:34 -0400 | [diff] [blame] | 152 | HttpSession session = GuiceFilter.getRequest(key).getSession(); |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 153 | synchronized (session) { |
limpbizkit | e93f260 | 2009-08-12 19:24:11 +0000 | [diff] [blame] | 154 | Object obj = session.getAttribute(name); |
| 155 | if (NullObject.INSTANCE == obj) { |
| 156 | return null; |
| 157 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 158 | @SuppressWarnings("unchecked") |
limpbizkit | e93f260 | 2009-08-12 19:24:11 +0000 | [diff] [blame] | 159 | T t = (T) obj; |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 160 | if (t == null) { |
| 161 | t = creator.get(); |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 162 | if (!Scopes.isCircularProxy(t)) { |
| 163 | session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); |
| 164 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 165 | } |
| 166 | return t; |
| 167 | } |
| 168 | } |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 169 | @Override |
kevinb9n | 61e081c | 2007-03-22 07:04:03 +0000 | [diff] [blame] | 170 | public String toString() { |
kevinb9n | da11d0d | 2007-04-20 15:58:38 +0000 | [diff] [blame] | 171 | return String.format("%s[%s]", creator, SESSION); |
kevinb9n | 61e081c | 2007-03-22 07:04:03 +0000 | [diff] [blame] | 172 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 173 | }; |
| 174 | } |
crazyboblee | f33d23e | 2007-02-12 04:17:48 +0000 | [diff] [blame] | 175 | |
guice.mirrorbot@gmail.com | 05bf8e5 | 2011-09-27 15:37:30 +0000 | [diff] [blame] | 176 | @Override |
crazyboblee | f33d23e | 2007-02-12 04:17:48 +0000 | [diff] [blame] | 177 | public String toString() { |
crazyboblee | 91c37e3 | 2007-02-15 05:18:57 +0000 | [diff] [blame] | 178 | return "ServletScopes.SESSION"; |
crazyboblee | f33d23e | 2007-02-12 04:17:48 +0000 | [diff] [blame] | 179 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 180 | }; |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 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 | * |
Christian Edward Gruber | 9111f48 | 2013-05-15 13:16:23 -0700 | [diff] [blame] | 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 | * |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 202 | * @param callable code to be executed in another thread, which depends on |
| 203 | * the request scope. |
sberlin | 2196786 | 2010-11-24 14:52:33 +0000 | [diff] [blame] | 204 | * @param seedMap the initial set of scoped instances for Guice to seed the |
sberlin | b886ce3 | 2010-12-01 02:59:06 +0000 | [diff] [blame] | 205 | * request scope with. To seed a key with null, use {@code null} as |
| 206 | * the value. |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 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. |
sberlin | ec76179 | 2011-06-29 22:04:31 +0000 | [diff] [blame] | 211 | * |
sberlin | c13b545 | 2010-10-31 18:38:24 +0000 | [diff] [blame] | 212 | * @since 3.0 |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 213 | */ |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 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."); |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 218 | |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 219 | // Snapshot the seed map and add all the instances to our continuing HTTP request. |
| 220 | final ContinuingHttpServletRequest continuingRequest = |
Sam Berlin | c33e73c | 2014-03-10 12:50:34 -0400 | [diff] [blame] | 221 | new ContinuingHttpServletRequest( |
| 222 | GuiceFilter.getRequest(Key.get(HttpServletRequest.class))); |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 223 | for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) { |
sberlin | 2196786 | 2010-11-24 14:52:33 +0000 | [diff] [blame] | 224 | Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue()); |
| 225 | continuingRequest.setAttribute(entry.getKey().toString(), value); |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 226 | } |
| 227 | |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 228 | return new Callable<T>() { |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 229 | public T call() throws Exception { |
Christian Edward Gruber | 9111f48 | 2013-05-15 13:16:23 -0700 | [diff] [blame] | 230 | checkScopingState(null == GuiceFilter.localContext.get(), |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 231 | "Cannot continue request in the same thread as a HTTP request!"); |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 232 | return new GuiceFilter.Context(continuingRequest, continuingRequest, null) |
| 233 | .call(callable); |
| 234 | } |
| 235 | }; |
| 236 | } |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 237 | |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 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 | * |
Sam Berlin | 4a4d825 | 2014-05-10 10:34:44 -0400 | [diff] [blame] | 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. |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 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. |
Ben McCann | bac730f | 2015-04-21 16:21:01 -0700 | [diff] [blame^] | 259 | * @since 4.0 |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 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); |
dhanji | 1848a29 | 2010-09-13 22:18:14 +0000 | [diff] [blame] | 287 | } |
| 288 | }; |
| 289 | } |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 290 | |
| 291 | /** |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 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. |
Ben McCann | bac730f | 2015-04-21 16:21:01 -0700 | [diff] [blame^] | 297 | * |
| 298 | * @since 4.0 |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 299 | */ |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 300 | public static boolean isRequestScoped(Binding<?> binding) { |
Sam Berlin | 04cdfd9 | 2012-01-17 11:29:38 -0500 | [diff] [blame] | 301 | return Scopes.isScoped(binding, ServletScopes.REQUEST, RequestScoped.class); |
Sam Berlin | b2f5582 | 2012-01-13 18:20:50 -0500 | [diff] [blame] | 302 | } |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 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 | * |
Christian Edward Gruber | 9111f48 | 2013-05-15 13:16:23 -0700 | [diff] [blame] | 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 | * |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 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 |
sberlin | b886ce3 | 2010-12-01 02:59:06 +0000 | [diff] [blame] | 317 | * request scope with. To seed a key with null, use {@code null} as |
| 318 | * the value. |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 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. |
sberlin | c13b545 | 2010-10-31 18:38:24 +0000 | [diff] [blame] | 321 | * @since 3.0 |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 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. |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 329 | final Context context = new Context(); |
Sam Berlin | 883fe03 | 2014-03-10 12:49:05 -0400 | [diff] [blame] | 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); |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 337 | |
| 338 | return new Callable<T>() { |
| 339 | public T call() throws Exception { |
Christian Edward Gruber | 9111f48 | 2013-05-15 13:16:23 -0700 | [diff] [blame] | 340 | checkScopingState(null == GuiceFilter.localContext.get(), |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 341 | "An HTTP request is already in progress, cannot scope a new request in this thread."); |
Christian Edward Gruber | 9111f48 | 2013-05-15 13:16:23 -0700 | [diff] [blame] | 342 | checkScopingState(null == requestScopeContext.get(), |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 343 | "A request scope is already in progress, cannot scope a new request in this thread."); |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 344 | return context.call(callable); |
dhanji | 0693a15 | 2010-09-20 18:25:29 +0000 | [diff] [blame] | 345 | } |
| 346 | }; |
| 347 | } |
sberlin | 2196786 | 2010-11-24 14:52:33 +0000 | [diff] [blame] | 348 | |
| 349 | /** |
sberlin | 2196786 | 2010-11-24 14:52:33 +0000 | [diff] [blame] | 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 | } |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 365 | |
| 366 | private static class Context { |
Sam Berlin | 5e5e2f5 | 2013-12-06 17:04:18 -0500 | [diff] [blame] | 367 | final Map<Key, Object> map = Maps.newHashMap(); |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 368 | |
Sam Berlin | 4a4d825 | 2014-05-10 10:34:44 -0400 | [diff] [blame] | 369 | // Synchronized to prevent two threads from using the same request |
| 370 | // scope concurrently. |
| 371 | synchronized <T> T call(Callable<T> callable) throws Exception { |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 372 | Context previous = requestScopeContext.get(); |
| 373 | requestScopeContext.set(this); |
| 374 | try { |
| 375 | return callable.call(); |
| 376 | } finally { |
Sam Berlin | 7dc62e5 | 2012-05-27 13:39:27 -0400 | [diff] [blame] | 377 | requestScopeContext.set(previous); |
| 378 | } |
| 379 | } |
| 380 | } |
Christian Edward Gruber | 9111f48 | 2013-05-15 13:16:23 -0700 | [diff] [blame] | 381 | |
| 382 | private static void checkScopingState(boolean condition, String msg) { |
| 383 | if (!condition) { |
| 384 | throw new ScopingException(msg); |
| 385 | } |
| 386 | } |
crazyboblee | 3a09e29 | 2007-02-08 22:36:21 +0000 | [diff] [blame] | 387 | } |