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 17 package com.google.inject.testing.fieldbinder; 18 19 import com.google.common.base.Optional; 20 import com.google.common.base.Preconditions; 21 import com.google.inject.Binder; 22 import com.google.inject.BindingAnnotation; 23 import com.google.inject.Module; 24 import com.google.inject.Provider; 25 import com.google.inject.TypeLiteral; 26 import com.google.inject.binder.AnnotatedBindingBuilder; 27 import com.google.inject.binder.LinkedBindingBuilder; 28 import com.google.inject.internal.Annotations; 29 import com.google.inject.spi.Message; 30 31 import java.lang.annotation.Annotation; 32 import java.lang.reflect.Field; 33 import java.lang.reflect.ParameterizedType; 34 import 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 */ 98 public 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 BoundFieldModule(Object instance)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 */ of(Object instance)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 BoundFieldException(Message message)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 BoundFieldInfo( Field field, Bind bindAnnotation, TypeLiteral<?> fieldType)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 getBoundType()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 */ getNaturalFieldType()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 getValue()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 hasInject(Field field)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 */ getBoundFieldInfo( TypeLiteral<?> containingClassType, Field field)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 verifyBindingAnnotations( Field field, AnnotatedBindingBuilder<?> annotatedBinder)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 */ isTransparentProvider(Class<?> clazz)287 private static boolean isTransparentProvider(Class<?> clazz) { 288 return com.google.inject.Provider.class == clazz || javax.inject.Provider.class == clazz; 289 } 290 bindField(final BoundFieldInfo fieldInfo)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 getFieldValue(final BoundFieldInfo fieldInfo)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 throwBoundFieldException(Field field, String format, Object... args)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 configure(Binder binder)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