ServletScopes.java revision a23937e0145d7bb4cc0c2169d21023bedee3fdb2
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.inject.Key;
20import com.google.inject.OutOfScopeException;
21import com.google.inject.Provider;
22import com.google.inject.Scope;
23import com.google.common.collect.Maps;
24import com.google.common.base.Preconditions;
25import java.util.Map;
26import java.util.concurrent.Callable;
27import javax.servlet.http.HttpServletRequest;
28import javax.servlet.http.HttpSession;
29
30/**
31 * Servlet scopes.
32 *
33 * @author crazybob@google.com (Bob Lee)
34 */
35public class ServletScopes {
36
37  private ServletScopes() {}
38
39  /** A sentinel attribute value representing null. */
40  enum NullObject { INSTANCE }
41
42  /**
43   * HTTP servlet request scope.
44   */
45  public static final Scope REQUEST = new Scope() {
46    public <T> Provider<T> scope(Key<T> key, final Provider<T> creator) {
47      final String name = key.toString();
48      return new Provider<T>() {
49        public T get() {
50          // Check if the alternate request scope should be used, if no HTTP
51          // request is in progress.
52          if (null == GuiceFilter.localContext.get()) {
53
54            // NOTE(user): We don't need to synchronize on the scope map
55            // unlike the HTTP request because we're the only ones who have
56            // a reference to it, and it is only available via a threadlocal.
57            Map<String, Object> scopeMap = requestScopeContext.get();
58            if (null != scopeMap) {
59              @SuppressWarnings("unchecked")
60              T t = (T) scopeMap.get(name);
61
62              // Accounts for @Nullable providers.
63              if (NullObject.INSTANCE == t) {
64                return null;
65              }
66
67              if (t == null) {
68                t = creator.get();
69                // Store a sentinel for provider-given null values.
70                scopeMap.put(name, t != null ? t : NullObject.INSTANCE);
71              }
72
73              return t;
74            } // else: fall into normal HTTP request scope and out of scope
75              // exception is thrown.
76          }
77
78          HttpServletRequest request = GuiceFilter.getRequest();
79
80          synchronized (request) {
81            Object obj = request.getAttribute(name);
82            if (NullObject.INSTANCE == obj) {
83              return null;
84            }
85            @SuppressWarnings("unchecked")
86            T t = (T) obj;
87            if (t == null) {
88              t = creator.get();
89              request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
90            }
91            return t;
92          }
93        }
94
95        public String toString() {
96          return String.format("%s[%s]", creator, REQUEST);
97        }
98      };
99    }
100
101    public String toString() {
102      return "ServletScopes.REQUEST";
103    }
104  };
105
106  /**
107   * HTTP session scope.
108   */
109  public static final Scope SESSION = new Scope() {
110    public <T> Provider<T> scope(Key<T> key, final Provider<T> creator) {
111      final String name = key.toString();
112      return new Provider<T>() {
113        public T get() {
114          HttpSession session = GuiceFilter.getRequest().getSession();
115          synchronized (session) {
116            Object obj = session.getAttribute(name);
117            if (NullObject.INSTANCE == obj) {
118              return null;
119            }
120            @SuppressWarnings("unchecked")
121            T t = (T) obj;
122            if (t == null) {
123              t = creator.get();
124              session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
125            }
126            return t;
127          }
128        }
129        public String toString() {
130          return String.format("%s[%s]", creator, SESSION);
131        }
132      };
133    }
134
135    public String toString() {
136      return "ServletScopes.SESSION";
137    }
138  };
139
140  /**
141   * Wraps the given callable in a contextual callable that "continues" the
142   * HTTP request in another thread. This acts as a way of transporting
143   * request context data from the request processing thread to to worker
144   * threads.
145   * <p>
146   * There are some limitations:
147   * <ul>
148   *   <li>Derived objects (i.e. anything marked @RequestScoped will not be
149   *      transported.</li>
150   *   <li>State changes to the HttpServletRequest after this method is called
151   *      will not be seen in the continued thread.</li>
152   *   <li>Only the HttpServletRequest, ServletContext and request parameter
153   *      map are available in the continued thread. The response and session
154   *      are not available.</li>
155   * </ul>
156   *
157   * @param callable code to be executed in another thread, which depends on
158   *     the request scope.
159   * @param seedMap the initial set of scoped instances for Guice to seed the
160   *     request scope with.  To seed a key with null, use {@code null} as
161   *     the value.
162   * @return a callable that will invoke the given callable, making the request
163   *     context available to it.
164   * @throws OutOfScopeException if this method is called from a non-request
165   *     thread, or if the request has completed.
166   *
167   * @since 3.0
168   */
169  public static <T> Callable<T> continueRequest(final Callable<T> callable,
170      final Map<Key<?>, Object> seedMap) {
171    Preconditions.checkArgument(null != seedMap,
172        "Seed map cannot be null, try passing in Collections.emptyMap() instead.");
173
174    // Snapshot the seed map and add all the instances to our continuing HTTP request.
175    final ContinuingHttpServletRequest continuingRequest =
176        new ContinuingHttpServletRequest(GuiceFilter.getRequest());
177    for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) {
178      Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue());
179      continuingRequest.setAttribute(entry.getKey().toString(), value);
180    }
181
182    return new Callable<T>() {
183      private HttpServletRequest request = continuingRequest;
184
185      public T call() throws Exception {
186        GuiceFilter.Context context = GuiceFilter.localContext.get();
187        Preconditions.checkState(null == context,
188            "Cannot continue request in the same thread as a HTTP request!");
189
190        // Only set up the request continuation if we're running in a
191        // new vanilla thread.
192        GuiceFilter.localContext.set(new GuiceFilter.Context(request, null));
193        try {
194          return callable.call();
195        } finally {
196          // Clear the copied context if we set one up.
197          if (null == context) {
198            GuiceFilter.localContext.remove();
199          }
200        }
201      }
202    };
203  }
204
205  /**
206   * A threadlocal scope map for non-http request scopes. The {@link #REQUEST}
207   * scope falls back to this scope map if no http request is available, and
208   * requires {@link #scopeRequest} to be called as an alertnative.
209   */
210  private static final ThreadLocal<Map<String, Object>> requestScopeContext
211      = new ThreadLocal<Map<String, Object>>();
212
213  /**
214   * Scopes the given callable inside a request scope. This is not the same
215   * as the HTTP request scope, but is used if no HTTP request scope is in
216   * progress. In this way, keys can be scoped as @RequestScoped and exist
217   * in non-HTTP requests (for example: RPC requests) as well as in HTTP
218   * request threads.
219   *
220   * @param callable code to be executed which depends on the request scope.
221   *     Typically in another thread, but not necessarily so.
222   * @param seedMap the initial set of scoped instances for Guice to seed the
223   *     request scope with.  To seed a key with null, use {@code null} as
224   *     the value.
225   * @return a callable that when called will run inside the a request scope
226   *     that exposes the instances in the {@code seedMap} as scoped keys.
227   * @since 3.0
228   */
229  public static <T> Callable<T> scopeRequest(final Callable<T> callable,
230      Map<Key<?>, Object> seedMap) {
231    Preconditions.checkArgument(null != seedMap,
232        "Seed map cannot be null, try passing in Collections.emptyMap() instead.");
233
234    // Copy the seed values into our local scope map.
235    final Map<String, Object> scopeMap = Maps.newHashMap();
236    for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) {
237      Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue());
238      scopeMap.put(entry.getKey().toString(), value);
239    }
240
241    return new Callable<T>() {
242      public T call() throws Exception {
243        Preconditions.checkState(null == GuiceFilter.localContext.get(),
244            "An HTTP request is already in progress, cannot scope a new request in this thread.");
245        Preconditions.checkState(null == requestScopeContext.get(),
246            "A request scope is already in progress, cannot scope a new request in this thread.");
247
248        requestScopeContext.set(scopeMap);
249
250        try {
251          return callable.call();
252        } finally {
253          requestScopeContext.remove();
254        }
255      }
256    };
257  }
258
259  /**
260   * Validates the key and object, ensuring the value matches the key type, and
261   * canonicalizing null objects to the null sentinel.
262   */
263  private static Object validateAndCanonicalizeValue(Key<?> key, Object object) {
264    if (object == null || object == NullObject.INSTANCE) {
265      return NullObject.INSTANCE;
266    }
267
268    if (!key.getTypeLiteral().getRawType().isInstance(object)) {
269      throw new IllegalArgumentException("Value[" + object + "] of type["
270          + object.getClass().getName() + "] is not compatible with key[" + key + "]");
271    }
272
273    return object;
274  }
275}
276