| /* |
| * Copyright (C) 2006 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.inject.servlet; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Maps.EntryTransformer; |
| import com.google.inject.Binding; |
| import com.google.inject.Injector; |
| import com.google.inject.Key; |
| import com.google.inject.OutOfScopeException; |
| import com.google.inject.Provider; |
| import com.google.inject.Scope; |
| import com.google.inject.Scopes; |
| import java.util.Map; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.servlet.http.HttpSession; |
| |
| /** |
| * Servlet scopes. |
| * |
| * @author crazybob@google.com (Bob Lee) |
| */ |
| public class ServletScopes { |
| |
| private ServletScopes() {} |
| |
| /** |
| * A threadlocal scope map for non-http request scopes. The {@link #REQUEST} scope falls back to |
| * this scope map if no http request is available, and requires {@link #scopeRequest} to be called |
| * as an alternative. |
| */ |
| private static final ThreadLocal<Context> requestScopeContext = new ThreadLocal<>(); |
| |
| /** A sentinel attribute value representing null. */ |
| enum NullObject { |
| INSTANCE |
| } |
| |
| /** HTTP servlet request scope. */ |
| public static final Scope REQUEST = new RequestScope(); |
| |
| private static final class RequestScope implements Scope { |
| @Override |
| public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { |
| return new Provider<T>() { |
| |
| /** Keys bound in request-scope which are handled directly by GuiceFilter. */ |
| private final ImmutableSet<Key<?>> REQUEST_CONTEXT_KEYS = |
| ImmutableSet.of( |
| Key.get(HttpServletRequest.class), |
| Key.get(HttpServletResponse.class), |
| new Key<Map<String, String[]>>(RequestParameters.class) {}); |
| |
| @Override |
| public T get() { |
| // Check if the alternate request scope should be used, if no HTTP |
| // request is in progress. |
| if (null == GuiceFilter.localContext.get()) { |
| |
| // NOTE(dhanji): We don't need to synchronize on the scope map |
| // unlike the HTTP request because we're the only ones who have |
| // a reference to it, and it is only available via a threadlocal. |
| Context context = requestScopeContext.get(); |
| if (null != context) { |
| @SuppressWarnings("unchecked") |
| T t = (T) context.map.get(key); |
| |
| // Accounts for @Nullable providers. |
| if (NullObject.INSTANCE == t) { |
| return null; |
| } |
| |
| if (t == null) { |
| t = creator.get(); |
| if (!Scopes.isCircularProxy(t)) { |
| // Store a sentinel for provider-given null values. |
| context.map.put(key, t != null ? t : NullObject.INSTANCE); |
| } |
| } |
| |
| return t; |
| } // else: fall into normal HTTP request scope and out of scope |
| // exception is thrown. |
| } |
| |
| // Always synchronize and get/set attributes on the underlying request |
| // object since Filters may wrap the request and change the value of |
| // {@code GuiceFilter.getRequest()}. |
| // |
| // This _correctly_ throws up if the thread is out of scope. |
| HttpServletRequest request = GuiceFilter.getOriginalRequest(key); |
| if (REQUEST_CONTEXT_KEYS.contains(key)) { |
| // Don't store these keys as attributes, since they are handled by |
| // GuiceFilter itself. |
| return creator.get(); |
| } |
| String name = key.toString(); |
| synchronized (request) { |
| Object obj = request.getAttribute(name); |
| if (NullObject.INSTANCE == obj) { |
| return null; |
| } |
| @SuppressWarnings("unchecked") |
| T t = (T) obj; |
| if (t == null) { |
| t = creator.get(); |
| if (!Scopes.isCircularProxy(t)) { |
| request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); |
| } |
| } |
| return t; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s]", creator, REQUEST); |
| } |
| }; |
| } |
| |
| @Override |
| public String toString() { |
| return "ServletScopes.REQUEST"; |
| } |
| } |
| |
| /** HTTP session scope. */ |
| public static final Scope SESSION = new SessionScope(); |
| |
| private static final class SessionScope implements Scope { |
| @Override |
| public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { |
| final String name = key.toString(); |
| return new Provider<T>() { |
| @Override |
| public T get() { |
| HttpSession session = GuiceFilter.getRequest(key).getSession(); |
| synchronized (session) { |
| Object obj = session.getAttribute(name); |
| if (NullObject.INSTANCE == obj) { |
| return null; |
| } |
| @SuppressWarnings("unchecked") |
| T t = (T) obj; |
| if (t == null) { |
| t = creator.get(); |
| if (!Scopes.isCircularProxy(t)) { |
| session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); |
| } |
| } |
| return t; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s]", creator, SESSION); |
| } |
| }; |
| } |
| |
| @Override |
| public String toString() { |
| return "ServletScopes.SESSION"; |
| } |
| } |
| |
| /** |
| * Wraps the given callable in a contextual callable that "continues" the HTTP request in another |
| * thread. This acts as a way of transporting request context data from the request processing |
| * thread to to worker threads. |
| * |
| * <p>There are some limitations: |
| * |
| * <ul> |
| * <li>Derived objects (i.e. anything marked @RequestScoped will not be transported. |
| * <li>State changes to the HttpServletRequest after this method is called will not be seen in the |
| * continued thread. |
| * <li>Only the HttpServletRequest, ServletContext and request parameter map are available in the |
| * continued thread. The response and session are not available. |
| * </ul> |
| * |
| * <p>The returned callable will throw a {@link ScopingException} when called if the HTTP request |
| * scope is still active on the current thread. |
| * |
| * @param callable code to be executed in another thread, which depends on the request scope. |
| * @param seedMap the initial set of scoped instances for Guice to seed the request scope with. To |
| * seed a key with null, use {@code null} as the value. |
| * @return a callable that will invoke the given callable, making the request context available to |
| * it. |
| * @throws OutOfScopeException if this method is called from a non-request thread, or if the |
| * request has completed. |
| * @since 3.0 |
| * @deprecated You probably want to use {@code transferRequest} instead |
| */ |
| @Deprecated |
| public static <T> Callable<T> continueRequest(Callable<T> callable, Map<Key<?>, Object> seedMap) { |
| return wrap(callable, continueRequest(seedMap)); |
| } |
| |
| private static RequestScoper continueRequest(Map<Key<?>, Object> seedMap) { |
| Preconditions.checkArgument( |
| null != seedMap, "Seed map cannot be null, try passing in Collections.emptyMap() instead."); |
| |
| // Snapshot the seed map and add all the instances to our continuing HTTP request. |
| final ContinuingHttpServletRequest continuingRequest = |
| new ContinuingHttpServletRequest(GuiceFilter.getRequest(Key.get(HttpServletRequest.class))); |
| for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) { |
| Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue()); |
| continuingRequest.setAttribute(entry.getKey().toString(), value); |
| } |
| |
| return new RequestScoper() { |
| @Override |
| public CloseableScope open() { |
| checkScopingState( |
| null == GuiceFilter.localContext.get(), |
| "Cannot continue request in the same thread as a HTTP request!"); |
| return new GuiceFilter.Context(continuingRequest, continuingRequest, null).open(); |
| } |
| }; |
| } |
| |
| /** |
| * Wraps the given callable in a contextual callable that "transfers" the request to another |
| * thread. This acts as a way of transporting request context data from the current thread to a |
| * future thread. |
| * |
| * <p>As opposed to {@link #continueRequest}, this method propagates all existing scoped objects. |
| * The primary use case is in server implementations where you can detach the request processing |
| * thread while waiting for data, and reattach to a different thread to finish processing at a |
| * later time. |
| * |
| * <p>Because request-scoped objects are not typically thread-safe, the callable returned by this |
| * method must not be run on a different thread until the current request scope has terminated. |
| * The returned callable will block until the current thread has released the request scope. |
| * |
| * @param callable code to be executed in another thread, which depends on the request scope. |
| * @return a callable that will invoke the given callable, making the request context available to |
| * it. |
| * @throws OutOfScopeException if this method is called from a non-request thread, or if the |
| * request has completed. |
| * @since 4.0 |
| */ |
| public static <T> Callable<T> transferRequest(Callable<T> callable) { |
| return wrap(callable, transferRequest()); |
| } |
| |
| /** |
| * Returns an object that "transfers" the request to another thread. This acts as a way of |
| * transporting request context data from the current thread to a future thread. The transferred |
| * scope is the one active for the thread that calls this method. A later call to {@code open()} |
| * activates the transferred the scope, including propagating any objects scoped at that time. |
| * |
| * <p>As opposed to {@link #continueRequest}, this method propagates all existing scoped objects. |
| * The primary use case is in server implementations where you can detach the request processing |
| * thread while waiting for data, and reattach to a different thread to finish processing at a |
| * later time. |
| * |
| * <p>Because request-scoped objects are not typically thread-safe, it is important to avoid |
| * applying the same request scope concurrently. The returned Scoper will block on open until the |
| * current thread has released the request scope. |
| * |
| * @return an object that when opened will initiate the request scope |
| * @throws OutOfScopeException if this method is called from a non-request thread, or if the |
| * request has completed. |
| * @since 4.1 |
| */ |
| public static RequestScoper transferRequest() { |
| return (GuiceFilter.localContext.get() != null) |
| ? transferHttpRequest() |
| : transferNonHttpRequest(); |
| } |
| |
| private static RequestScoper transferHttpRequest() { |
| final GuiceFilter.Context context = GuiceFilter.localContext.get(); |
| if (context == null) { |
| throw new OutOfScopeException("Not in a request scope"); |
| } |
| return context; |
| } |
| |
| private static RequestScoper transferNonHttpRequest() { |
| final Context context = requestScopeContext.get(); |
| if (context == null) { |
| throw new OutOfScopeException("Not in a request scope"); |
| } |
| return context; |
| } |
| |
| /** |
| * Returns true if {@code binding} is request-scoped. If the binding is a {@link |
| * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (i. e. it |
| * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will |
| * also return true if the target binding is request-scoped. |
| * |
| * @since 4.0 |
| */ |
| public static boolean isRequestScoped(Binding<?> binding) { |
| return Scopes.isScoped(binding, ServletScopes.REQUEST, RequestScoped.class); |
| } |
| |
| /** |
| * Scopes the given callable inside a request scope. This is not the same as the HTTP request |
| * scope, but is used if no HTTP request scope is in progress. In this way, keys can be scoped |
| * as @RequestScoped and exist in non-HTTP requests (for example: RPC requests) as well as in HTTP |
| * request threads. |
| * |
| * <p>The returned callable will throw a {@link ScopingException} when called if there is a |
| * request scope already active on the current thread. |
| * |
| * @param callable code to be executed which depends on the request scope. Typically in another |
| * thread, but not necessarily so. |
| * @param seedMap the initial set of scoped instances for Guice to seed the request scope with. To |
| * seed a key with null, use {@code null} as the value. |
| * @return a callable that when called will run inside the a request scope that exposes the |
| * instances in the {@code seedMap} as scoped keys. |
| * @since 3.0 |
| */ |
| public static <T> Callable<T> scopeRequest(Callable<T> callable, Map<Key<?>, Object> seedMap) { |
| return wrap(callable, scopeRequest(seedMap)); |
| } |
| |
| /** |
| * Returns an object that will apply request scope to a block of code. This is not the same as the |
| * HTTP request scope, but is used if no HTTP request scope is in progress. In this way, keys can |
| * be scoped as @RequestScoped and exist in non-HTTP requests (for example: RPC requests) as well |
| * as in HTTP request threads. |
| * |
| * <p>The returned object will throw a {@link ScopingException} when opened if there is a request |
| * scope already active on the current thread. |
| * |
| * @param seedMap the initial set of scoped instances for Guice to seed the request scope with. To |
| * seed a key with null, use {@code null} as the value. |
| * @return an object that when opened will initiate the request scope |
| * @since 4.1 |
| */ |
| public static RequestScoper scopeRequest(Map<Key<?>, Object> seedMap) { |
| Preconditions.checkArgument( |
| null != seedMap, "Seed map cannot be null, try passing in Collections.emptyMap() instead."); |
| |
| // Copy the seed values into our local scope map. |
| final Context context = new Context(); |
| Map<Key<?>, Object> validatedAndCanonicalizedMap = |
| Maps.transformEntries( |
| seedMap, |
| new EntryTransformer<Key<?>, Object, Object>() { |
| @Override |
| public Object transformEntry(Key<?> key, Object value) { |
| return validateAndCanonicalizeValue(key, value); |
| } |
| }); |
| context.map.putAll(validatedAndCanonicalizedMap); |
| return new RequestScoper() { |
| @Override |
| public CloseableScope open() { |
| checkScopingState( |
| null == GuiceFilter.localContext.get(), |
| "An HTTP request is already in progress, cannot scope a new request in this thread."); |
| checkScopingState( |
| null == requestScopeContext.get(), |
| "A request scope is already in progress, cannot scope a new request in this thread."); |
| return context.open(); |
| } |
| }; |
| } |
| |
| /** |
| * Validates the key and object, ensuring the value matches the key type, and canonicalizing null |
| * objects to the null sentinel. |
| */ |
| private static Object validateAndCanonicalizeValue(Key<?> key, Object object) { |
| if (object == null || object == NullObject.INSTANCE) { |
| return NullObject.INSTANCE; |
| } |
| |
| if (!key.getTypeLiteral().getRawType().isInstance(object)) { |
| throw new IllegalArgumentException( |
| "Value[" |
| + object |
| + "] of type[" |
| + object.getClass().getName() |
| + "] is not compatible with key[" |
| + key |
| + "]"); |
| } |
| |
| return object; |
| } |
| |
| private static class Context implements RequestScoper { |
| final Map<Key, Object> map = Maps.newHashMap(); |
| |
| // Synchronized to prevent two threads from using the same request |
| // scope concurrently. |
| final Lock lock = new ReentrantLock(); |
| |
| @Override |
| public CloseableScope open() { |
| lock.lock(); |
| final Context previous = requestScopeContext.get(); |
| requestScopeContext.set(this); |
| return new CloseableScope() { |
| @Override |
| public void close() { |
| requestScopeContext.set(previous); |
| lock.unlock(); |
| } |
| }; |
| } |
| } |
| |
| private static void checkScopingState(boolean condition, String msg) { |
| if (!condition) { |
| throw new ScopingException(msg); |
| } |
| } |
| |
| private static final <T> Callable<T> wrap( |
| final Callable<T> delegate, final RequestScoper requestScoper) { |
| return new Callable<T>() { |
| @Override |
| public T call() throws Exception { |
| RequestScoper.CloseableScope scope = requestScoper.open(); |
| try { |
| return delegate.call(); |
| } finally { |
| scope.close(); |
| } |
| } |
| }; |
| } |
| } |