1 /*
2  * Copyright (C) 2018 The Android Open Source Project
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 package com.android.tradefed.guice;
17 
18 import static com.google.common.base.Preconditions.checkState;
19 
20 import com.android.tradefed.config.IConfiguration;
21 
22 import com.google.common.collect.Maps;
23 import com.google.inject.Guice;
24 import com.google.inject.Injector;
25 import com.google.inject.Key;
26 import com.google.inject.OutOfScopeException;
27 import com.google.inject.Provider;
28 import com.google.inject.Scope;
29 import com.google.inject.Scopes;
30 
31 import java.util.Map;
32 
33 /**
34  * Scopes a single Tradefed invocation.
35  *
36  * <p>The scope can be initialized with one or more seed values by calling <code>seed(key, value)
37  * </code> before the injector will be called upon to provide for this key. A typical use is for a
38  * test invocation to enter/exit the scope, representing an invocation Scope, and seed configuration
39  * objects. For each key inserted with seed(), you must include a corresponding binding:
40  *
41  * <pre><code>
42  *   bind(key)
43  *       .toProvider(SimpleScope.<key.class>seededKeyProvider())
44  *       .in(InvocationScoped.class);
45  * </code></pre>
46  *
47  * FIXME: Possibly handle multi objects (like lists).
48  */
49 public class InvocationScope implements Scope {
50 
InvocationScope()51     public InvocationScope() {}
52 
53     private static final Provider<Object> SEEDED_KEY_PROVIDER =
54             new Provider<Object>() {
55                 @Override
56                 public Object get() {
57                     throw new IllegalStateException(
58                             "If you got here then it means that"
59                                     + " your code asked for scoped object which should have been"
60                                     + " explicitly seeded in this scope by calling"
61                                     + " SimpleScope.seed(), but was not.");
62                 }
63             };
64 
65     private static InvocationScope sDefaultInstance = null;
66 
getDefault()67     public static InvocationScope getDefault() {
68         if (sDefaultInstance == null) {
69             sDefaultInstance = new InvocationScope();
70         }
71         return sDefaultInstance;
72     }
73 
74     private final ThreadLocal<Map<Key<?>, Object>> values = new ThreadLocal<Map<Key<?>, Object>>();
75 
76     /** Start marking the scope of the Tradefed Invocation. */
enter()77     public void enter() {
78         checkState(values.get() == null, "A scoping block is already in progress");
79         values.set(Maps.<Key<?>, Object>newHashMap());
80     }
81 
82     /** Mark the end of the scope for the Tradefed Invocation. */
exit()83     public void exit() {
84         checkState(values.get() != null, "No scoping block in progress");
85         values.remove();
86     }
87 
88     /**
89      * Interface init between Tradefed and Guice: This is the place where TF object are seeded to
90      * the invocation scope to be used.
91      *
92      * @param config The Tradefed configuration.
93      */
seedConfiguration(IConfiguration config)94     public void seedConfiguration(IConfiguration config) {
95         // First seed the configuration itself
96         seed(IConfiguration.class, config);
97         // Then inject the seeded objects to the configuration.
98         injectToConfig(config);
99     }
100 
injectToConfig(IConfiguration config)101     private void injectToConfig(IConfiguration config) {
102         Injector injector = Guice.createInjector(new InvocationScopeModule(this));
103 
104         // TODO: inject to TF objects that could require it.
105         // Do injection against current test objects: This allows to pass the injector
106         for (Object obj : config.getTests()) {
107             injector.injectMembers(obj);
108         }
109     }
110 
111     /**
112      * Seed a key/value that will be available during the TF invocation scope to be used.
113      *
114      * @param key the key used to represent the object.
115      * @param value The actual object that will be available during the invocation.
116      */
seed(Key<T> key, T value)117     public <T> void seed(Key<T> key, T value) {
118         Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
119         checkState(
120                 !scopedObjects.containsKey(key),
121                 "A value for the key %s was "
122                         + "already seeded in this scope. Old value: %s New value: %s",
123                 key,
124                 scopedObjects.get(key),
125                 value);
126         scopedObjects.put(key, value);
127     }
128 
129     /**
130      * Seed a key/value that will be available during the TF invocation scope to be used.
131      *
132      * @param clazz the Class used to represent the object.
133      * @param value The actual object that will be available during the invocation.
134      */
seed(Class<T> clazz, T value)135     public <T> void seed(Class<T> clazz, T value) {
136         seed(Key.get(clazz), value);
137     }
138 
139     @Override
scope(final Key<T> key, final Provider<T> unscoped)140     public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
141         return new Provider<T>() {
142             @Override
143             public T get() {
144                 Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
145 
146                 @SuppressWarnings("unchecked")
147                 T current = (T) scopedObjects.get(key);
148                 if (current == null && !scopedObjects.containsKey(key)) {
149                     current = unscoped.get();
150 
151                     // don't remember proxies; these exist only to serve circular dependencies
152                     if (Scopes.isCircularProxy(current)) {
153                         return current;
154                     }
155 
156                     scopedObjects.put(key, current);
157                 }
158                 return current;
159             }
160         };
161     }
162 
163     private <T> Map<Key<?>, Object> getScopedObjectMap(Key<T> key) {
164         Map<Key<?>, Object> scopedObjects = values.get();
165         if (scopedObjects == null) {
166             throw new OutOfScopeException("Cannot access " + key + " outside of a scoping block");
167         }
168         return scopedObjects;
169     }
170 
171     /**
172      * Returns a provider that always throws exception complaining that the object in question must
173      * be seeded before it can be injected.
174      *
175      * @return typed provider
176      */
177     @SuppressWarnings({"unchecked"})
178     public static <T> Provider<T> seededKeyProvider() {
179         return (Provider<T>) SEEDED_KEY_PROVIDER;
180     }
181 }
182