1/*
2 * Copyright (C) 2014 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.testing.fieldbinder;
18
19import com.google.common.base.Optional;
20import com.google.common.base.Preconditions;
21import com.google.inject.Binder;
22import com.google.inject.BindingAnnotation;
23import com.google.inject.Module;
24import com.google.inject.Provider;
25import com.google.inject.TypeLiteral;
26import com.google.inject.binder.AnnotatedBindingBuilder;
27import com.google.inject.binder.LinkedBindingBuilder;
28import com.google.inject.internal.Annotations;
29import com.google.inject.spi.Message;
30
31import java.lang.annotation.Annotation;
32import java.lang.reflect.Field;
33import java.lang.reflect.ParameterizedType;
34import java.lang.reflect.Type;
35
36/**
37 * Automatically creates Guice bindings for fields in an object annotated with {@link Bind}.
38 *
39 * <p>This module is intended for use in tests to reduce the code needed to bind local fields
40 * (usually mocks) for injection.
41 *
42 * <p>The following rules are followed in determining how fields are bound using this module:
43 *
44 * <ul>
45 * <li>
46 * For each {@link Bind} annotated field of an object and its superclasses, this module will bind
47 * that field's type to that field's value at injector creation time. This includes both instance
48 * and static fields.
49 * </li>
50 * <li>
51 * If {@link Bind#to} is specified, the field's value will be bound to the class specified by
52 * {@link Bind#to} instead of the field's actual type.
53 * </li>
54 * <li>
55 * If a {@link BindingAnnotation} or {@link javax.inject.Qualifier} is present on the field,
56 * that field will be bound using that annotation via {@link AnnotatedBindingBuilder#annotatedWith}.
57 * For example, {@code bind(Foo.class).annotatedWith(BarAnnotation.class).toInstance(theValue)}.
58 * It is an error to supply more than one {@link BindingAnnotation} or
59 * {@link javax.inject.Qualifier}.
60 * </li>
61 * <li>
62 * If the field is of type {@link Provider}, the field's value will be bound as a {@link Provider}
63 * using {@link LinkedBindingBuilder#toProvider} to the provider's parameterized type. For example,
64 * {@code Provider<Integer>} binds to {@link Integer}. Attempting to bind a non-parameterized
65 * {@link Provider} without a {@link Bind#to} clause is an error.
66 * </li>
67 * </ul>
68 *
69 * <p>Example use:
70 * <pre><code>
71 * public class TestFoo {
72 *   // bind(new TypeLiteral{@code <List<Object>>}() {}).toInstance(listOfObjects);
73 *   {@literal @}Bind private List{@code <Object>} listOfObjects = Lists.of();
74 *
75 *   // bind(String.class).toProvider(new Provider() { public String get() { return userName; }});
76 *   {@literal @}Bind(lazy = true) private String userName;
77 *
78 *   // bind(SuperClass.class).toInstance(aSubClass);
79 *   {@literal @}Bind(to = SuperClass.class) private SubClass aSubClass = new SubClass();
80 *
81 *   // bind(Object.class).annotatedWith(MyBindingAnnotation.class).toInstance(object2);
82 *   {@literal @}Bind
83 *   {@literal @}MyBindingAnnotation
84 *   private String myString = "hello";
85 *
86 *   // bind(Object.class).toProvider(myProvider);
87 *   {@literal @}Bind private Provider{@code <Object>} myProvider = getProvider();
88 *
89 *   {@literal @}Before public void setUp() {
90 *     Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this);
91 *   }
92 * }
93 * </code></pre>
94 *
95 * @see Bind
96 * @author eatnumber1@google.com (Russ Harmon)
97 */
98public final class BoundFieldModule implements Module {
99  private final Object instance;
100
101  // Note that binder is not initialized until configure() is called.
102  private Binder binder;
103
104  private BoundFieldModule(Object instance) {
105    this.instance = instance;
106  }
107
108  /**
109   * Create a BoundFieldModule which binds the {@link Bind} annotated fields of {@code instance}.
110   *
111   * @param instance the instance whose fields will be bound.
112   * @return a module which will bind the {@link Bind} annotated fields of {@code instance}.
113   */
114  public static BoundFieldModule of(Object instance) {
115    return new BoundFieldModule(instance);
116  }
117
118  private static class BoundFieldException extends RuntimeException {
119    private final Message message;
120
121    BoundFieldException(Message message) {
122      super(message.getMessage());
123      this.message = message;
124    }
125  }
126
127  private class BoundFieldInfo {
128    /** The field itself. */
129    final Field field;
130
131    /**
132     * The actual type of the field.
133     *
134     * <p>For example, {@code @Bind(to = Object.class) Number one = new Integer(1);} will be
135     * {@link Number}.
136     */
137    final TypeLiteral<?> type;
138
139    /** The {@link Bind} annotation which is present on the field. */
140    final Bind bindAnnotation;
141
142    /**
143     * The type this field will bind to.
144     *
145     * <p>For example, {@code @Bind(to = Object.class) Number one = new Integer(1);} will be
146     * {@link Object} and {@code @Bind Number one = new Integer(1);} will be {@link Number}.
147     */
148    final TypeLiteral<?> boundType;
149
150    /**
151     * The "natural" type of this field.
152     *
153     * <p>For example, {@code @Bind(to = Object.class) Number one = new Integer(1);} will be
154     * {@link Number}, and {@code @Bind(to = Object.class) Provider<Number> one = new Integer(1);}
155     * will be {@link Number}.
156     *
157     * @see #getNaturalFieldType
158     */
159    final Optional<TypeLiteral<?>> naturalType;
160
161    BoundFieldInfo(
162        Field field,
163        Bind bindAnnotation,
164        TypeLiteral<?> fieldType) {
165      this.field = field;
166      this.type = fieldType;
167      this.bindAnnotation = bindAnnotation;
168
169      field.setAccessible(true);
170
171      this.naturalType = getNaturalFieldType();
172      this.boundType = getBoundType();
173    }
174
175    private TypeLiteral<?> getBoundType() {
176      Class<?> bindClass = bindAnnotation.to();
177      // Bind#to's default value is Bind.class which is used to represent that no explicit binding
178      // type is requested.
179      if (bindClass == Bind.class) {
180        Preconditions.checkState(naturalType != null);
181        if (!this.naturalType.isPresent()) {
182          throwBoundFieldException(
183              field,
184              "Non parameterized Provider fields must have an explicit "
185              + "binding class via @Bind(to = Foo.class)");
186        }
187        return this.naturalType.get();
188      } else {
189        return TypeLiteral.get(bindClass);
190      }
191    }
192
193    /**
194     * Retrieves the type this field binds to naturally.
195     *
196     * <p>A field's "natural" type specifically ignores the to() method on the @Bind annotation, is
197     * the parameterized type if the field's actual type is a parameterized {@link Provider}, is
198     * {@link Optional#absent()} if this field is a non-parameterized {@link Provider} and otherwise
199     * is the field's actual type.
200     *
201     * @return the type this field binds to naturally, or {@link Optional#absent()} if this field is
202     * a non-parameterized {@link Provider}.
203     */
204    private Optional<TypeLiteral<?>> getNaturalFieldType() {
205      if (isTransparentProvider(type.getRawType())) {
206        Type providerType = type.getType();
207        if (providerType instanceof Class) {
208          return Optional.absent();
209        }
210        Preconditions.checkState(providerType instanceof ParameterizedType);
211        Type[] providerTypeArguments = ((ParameterizedType) providerType).getActualTypeArguments();
212        Preconditions.checkState(providerTypeArguments.length == 1);
213        return Optional.<TypeLiteral<?>>of(TypeLiteral.get(providerTypeArguments[0]));
214      } else {
215        return Optional.<TypeLiteral<?>>of(type);
216      }
217    }
218
219    Object getValue() {
220      try {
221        return field.get(instance);
222      } catch (IllegalAccessException e) {
223        // Since we called setAccessible(true) on this field in the constructor, this is a
224        // programming error if it occurs.
225        throw new AssertionError(e);
226      }
227    }
228  }
229
230  private static boolean hasInject(Field field) {
231    return field.isAnnotationPresent(javax.inject.Inject.class)
232        || field.isAnnotationPresent(com.google.inject.Inject.class);
233  }
234
235  /**
236   * Retrieve a {@link BoundFieldInfo}.
237   *
238   * <p>This returns a {@link BoundFieldInfo} if the field has a {@link Bind} annotation.
239   * Otherwise it returns {@link Optional#absent()}.
240   */
241  private Optional<BoundFieldInfo> getBoundFieldInfo(
242      TypeLiteral<?> containingClassType,
243      Field field) {
244    Bind bindAnnotation = field.getAnnotation(Bind.class);
245    if (bindAnnotation == null) {
246      return Optional.absent();
247    }
248    if (hasInject(field)) {
249      throwBoundFieldException(
250          field,
251          "Fields annotated with both @Bind and @Inject are illegal.");
252    }
253    return Optional.of(
254        new BoundFieldInfo(
255            field,
256            bindAnnotation,
257            containingClassType.getFieldType(field)));
258  }
259
260  private LinkedBindingBuilder<?> verifyBindingAnnotations(
261      Field field,
262      AnnotatedBindingBuilder<?> annotatedBinder) {
263    LinkedBindingBuilder<?> binderRet = annotatedBinder;
264    for (Annotation annotation : field.getAnnotations()) {
265      Class<? extends Annotation> annotationType = annotation.annotationType();
266      if (Annotations.isBindingAnnotation(annotationType)) {
267        // not returning here ensures that annotatedWith will be called multiple times if this field
268        // has multiple BindingAnnotations, relying on the binder to throw an error in this case.
269        binderRet = annotatedBinder.annotatedWith(annotation);
270      }
271    }
272    return binderRet;
273  }
274
275  /**
276   * Determines if {@code clazz} is a "transparent provider".
277   *
278   * <p>A transparent provider is a {@link com.google.inject.Provider} or
279   * {@link javax.inject.Provider} which binds to it's parameterized type when used as the argument
280   * to {@link Binder#bind}.
281   *
282   * <p>A {@link Provider} is transparent if the base class of that object is {@link Provider}. In
283   * other words, subclasses of {@link Provider} are not transparent. As a special case, if a
284   * {@link Provider} has no parameterized type but is otherwise transparent, then it is considered
285   * transparent.
286   */
287  private static boolean isTransparentProvider(Class<?> clazz) {
288    return com.google.inject.Provider.class == clazz || javax.inject.Provider.class == clazz;
289  }
290
291  private void bindField(final BoundFieldInfo fieldInfo) {
292    if (fieldInfo.naturalType.isPresent()) {
293      Class<?> naturalRawType = fieldInfo.naturalType.get().getRawType();
294      Class<?> boundRawType = fieldInfo.boundType.getRawType();
295      if (!boundRawType.isAssignableFrom(naturalRawType)) {
296        throwBoundFieldException(
297            fieldInfo.field,
298            "Requested binding type \"%s\" is not assignable from field binding type \"%s\"",
299            boundRawType.getName(),
300            naturalRawType.getName());
301      }
302    }
303
304    AnnotatedBindingBuilder<?> annotatedBinder = binder.bind(fieldInfo.boundType);
305    LinkedBindingBuilder<?> binder = verifyBindingAnnotations(fieldInfo.field, annotatedBinder);
306
307    // It's unfortunate that Field.get() just returns Object rather than the actual type (although
308    // that would be impossible) because as a result calling binder.toInstance or binder.toProvider
309    // is impossible to do without an unchecked cast. This is safe if fieldInfo.naturalType is
310    // present because compatibility is checked explicitly above, but is _unsafe_ if
311    // fieldInfo.naturalType is absent which occurrs when a non-parameterized Provider is used with
312    // @Bind(to = ...)
313    @SuppressWarnings("unchecked")
314    AnnotatedBindingBuilder<Object> binderUnsafe = (AnnotatedBindingBuilder<Object>) binder;
315
316    if (isTransparentProvider(fieldInfo.type.getRawType())) {
317      if (fieldInfo.bindAnnotation.lazy()) {
318        // We don't support this because it is confusing about when values are captured.
319        throwBoundFieldException(fieldInfo.field,
320            "'lazy' is incompatible with Provider valued fields");
321      }
322      // This is safe because we checked that the field's type is Provider above.
323      @SuppressWarnings("unchecked")
324      Provider<?> fieldValueUnsafe = (Provider<?>) getFieldValue(fieldInfo);
325      binderUnsafe.toProvider(fieldValueUnsafe);
326    } else if (fieldInfo.bindAnnotation.lazy()) {
327      binderUnsafe.toProvider(new Provider<Object>() {
328        @Override public Object get() {
329          return getFieldValue(fieldInfo);
330        }
331      });
332    } else {
333      binderUnsafe.toInstance(getFieldValue(fieldInfo));
334    }
335  }
336
337  private Object getFieldValue(final BoundFieldInfo fieldInfo) {
338    Object fieldValue = fieldInfo.getValue();
339    if (fieldValue == null) {
340      throwBoundFieldException(
341          fieldInfo.field,
342          "Binding to null values is not allowed. "
343              + "Use Providers.of(null) if this is your intended behavior.",
344              fieldInfo.field.getName());
345    }
346    return fieldValue;
347  }
348
349  private void throwBoundFieldException(Field field, String format, Object... args) {
350    Preconditions.checkNotNull(binder);
351    String source = String.format(
352        "%s field %s",
353        field.getDeclaringClass().getName(),
354        field.getName());
355    throw new BoundFieldException(new Message(source, String.format(format, args)));
356  }
357
358  @Override
359  public void configure(Binder binder) {
360    binder = binder.skipSources(BoundFieldModule.class);
361    this.binder = binder;
362
363    TypeLiteral<?> currentClassType = TypeLiteral.get(instance.getClass());
364    while (currentClassType.getRawType() != Object.class) {
365      for (Field field : currentClassType.getRawType().getDeclaredFields()) {
366        try {
367          Optional<BoundFieldInfo> fieldInfoOpt =
368              getBoundFieldInfo(currentClassType, field);
369          if (fieldInfoOpt.isPresent()) {
370            bindField(fieldInfoOpt.get());
371          }
372        } catch (BoundFieldException e) {
373          // keep going to try to collect as many errors as possible
374          binder.addError(e.message);
375        }
376      }
377      currentClassType =
378          currentClassType.getSupertype(currentClassType.getRawType().getSuperclass());
379    }
380  }
381}
382