/* * Copyright 2009 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.template.soy.shared.internal; import com.google.inject.Key; import com.google.inject.OutOfScopeException; import com.google.inject.Provider; import com.google.inject.Scope; import java.util.ArrayDeque; import java.util.HashMap; import java.util.Map; import javax.annotation.CheckReturnValue; /** * Scopes a single execution of a block of code. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * * <p>Apply this scope with a try/finally block: * * <pre> * scope.enter(); * try (GuiceSimpleScope.InScope inScope = scope.enter()) { * // explicitly seed some seed objects * inScope.seed(SomeObject.class, someObject); * // create and access scoped objects * ... * } * </pre> * * The scope can be initialized with one or more seed values by calling {@code seed(key, value)} or * {@code seed(class, value)} before the injector will be called upon to provide for this key. * * <p>For each key seeded with seed(), you must include a corresponding binding: * * <pre> * bind(key) * .toProvider(GuiceSimpleScope.<KeyClass>getUnscopedProvider()) * .in(ScopeAnnotation.class); * </pre> * */ public final class GuiceSimpleScope implements Scope { /** Represents {@code null} in the scope map. */ private static final Object NULL_SENTINEL = new Object(); /** * An autoclosable object that can be used to seed and exit scopes. * * <p>Obtain an instance with {@link GuiceSimpleScope#enter()}. */ public final class InScope implements AutoCloseable { private boolean isClosed; private final Thread openThread = Thread.currentThread(); private final ArrayDeque<HashMap<Key<?>, Object>> deque; InScope(ArrayDeque<HashMap<Key<?>, Object>> deque) { this.deque = deque; } /** * Seeds a value in the current occurrence of this scope. * * @param key The key to seed. * @param value The value for the key. */ public <T> void seed(Key<T> key, T value) { checkOpenAndOnCorrectThread(); HashMap<Key<?>, Object> scopedObjects = deque.peek(); Object prev = scopedObjects.put(key, value == null ? NULL_SENTINEL : value); if (prev != null) { throw new IllegalStateException( String.format( "A value for the key %s was already seeded in this scope. Old value: %s " + "New value: %s", key, prev, value)); } } /** * Seeds a value in the current occurrence of this scope. * * @param clazz The class to seed. * @param value The value for the key. */ public <T> void seed(Class<T> clazz, T value) { seed(Key.get(clazz), value); } /** Exits the scope */ @Override public void close() { checkOpenAndOnCorrectThread(); isClosed = true; deque.pop(); } private void checkOpenAndOnCorrectThread() { if (isClosed) { throw new IllegalStateException("called close() more than once!"); } if (Thread.currentThread() != openThread) { throw new IllegalStateException("cannot move the scope to another thread"); } } } /** Provider to use as the unscoped provider for scoped parameters. Always throws exception. */ private static final Provider<Object> UNSCOPED_PROVIDER = new Provider<Object>() { @Override public Object get() { throw new IllegalStateException( "If you got here then it means that your code asked for scoped object which should" + " have been explicitly seeded in this scope by calling GuiceSimpleScope.seed()," + " but was not."); } }; /** * Returns a provider that always throws exception complaining that the object in question must be * seeded before it can be injected. * * @return typed provider */ @SuppressWarnings("unchecked") public static <T> Provider<T> getUnscopedProvider() { return (Provider<T>) UNSCOPED_PROVIDER; } /** The ThreadLocal holding all the values in scope. */ private final ThreadLocal<ArrayDeque<HashMap<Key<?>, Object>>> scopedValuesTl = new ThreadLocal<>(); /** Enters an occurrence of this scope. */ @CheckReturnValue public InScope enter() { ArrayDeque<HashMap<Key<?>, Object>> stack = scopedValuesTl.get(); if (stack == null) { stack = new ArrayDeque<>(); scopedValuesTl.set(stack); } stack.push(new HashMap<Key<?>, Object>()); return new InScope(stack); } @Override public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscopedProvider) { return new Provider<T>() { @Override public T get() { ArrayDeque<HashMap<Key<?>, Object>> arrayDeque = scopedValuesTl.get(); if (arrayDeque == null || arrayDeque.isEmpty()) { throw new OutOfScopeException("Cannot access " + key + " outside of a scoping block"); } Map<Key<?>, Object> scopedValues = arrayDeque.peek(); Object value = scopedValues.get(key); if (value == null) { value = unscopedProvider.get(); scopedValues.put(key, value == null ? NULL_SENTINEL : value); } @SuppressWarnings("unchecked") T typedValue = value == NULL_SENTINEL ? null : (T) value; return typedValue; } }; } }