Home | History | Annotate | Download | only in inject
      1 /**
      2  * Copyright (C) 2006 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;
     18 
     19 import static com.google.inject.Asserts.asModuleChain;
     20 import static com.google.inject.Asserts.assertContains;
     21 import static com.google.inject.Asserts.getDeclaringSourcePart;
     22 import static java.lang.annotation.RetentionPolicy.RUNTIME;
     23 
     24 import com.google.common.collect.Iterables;
     25 import com.google.inject.matcher.Matchers;
     26 import com.google.inject.spi.ConvertedConstantBinding;
     27 import com.google.inject.spi.TypeConverter;
     28 import com.google.inject.spi.TypeConverterBinding;
     29 
     30 import junit.framework.AssertionFailedError;
     31 import junit.framework.TestCase;
     32 
     33 import java.lang.annotation.Retention;
     34 import java.util.Date;
     35 
     36 /**
     37  * @author crazybob (at) google.com (Bob Lee)
     38  */
     39 public class TypeConversionTest extends TestCase {
     40 
     41   @Retention(RUNTIME)
     42   @BindingAnnotation @interface NumericValue {}
     43 
     44   @Retention(RUNTIME)
     45   @BindingAnnotation @interface BooleanValue {}
     46 
     47   @Retention(RUNTIME)
     48   @BindingAnnotation @interface EnumValue {}
     49 
     50   @Retention(RUNTIME)
     51   @BindingAnnotation @interface ClassName {}
     52 
     53   public static class Foo {
     54     @Inject @BooleanValue Boolean booleanField;
     55     @Inject @BooleanValue boolean primitiveBooleanField;
     56     @Inject @NumericValue Byte byteField;
     57     @Inject @NumericValue byte primitiveByteField;
     58     @Inject @NumericValue Short shortField;
     59     @Inject @NumericValue short primitiveShortField;
     60     @Inject @NumericValue Integer integerField;
     61     @Inject @NumericValue int primitiveIntField;
     62     @Inject @NumericValue Long longField;
     63     @Inject @NumericValue long primitiveLongField;
     64     @Inject @NumericValue Float floatField;
     65     @Inject @NumericValue float primitiveFloatField;
     66     @Inject @NumericValue Double doubleField;
     67     @Inject @NumericValue double primitiveDoubleField;
     68     @Inject @EnumValue Bar enumField;
     69     @Inject @ClassName Class<?> classField;
     70   }
     71 
     72   public enum Bar {
     73     TEE, BAZ, BOB
     74   }
     75 
     76   public void testOneConstantInjection() throws CreationException {
     77     Injector injector = Guice.createInjector(new AbstractModule() {
     78       @Override protected void configure() {
     79         bindConstant().annotatedWith(NumericValue.class).to("5");
     80         bind(Simple.class);
     81       }
     82     });
     83 
     84     Simple simple = injector.getInstance(Simple.class);
     85     assertEquals(5, simple.i);
     86   }
     87 
     88   static class Simple {
     89     @Inject @NumericValue int i;
     90   }
     91 
     92   public void testConstantInjection() throws CreationException {
     93     Injector injector = Guice.createInjector(new AbstractModule() {
     94       @Override protected void configure() {
     95         bindConstant().annotatedWith(NumericValue.class).to("5");
     96         bindConstant().annotatedWith(BooleanValue.class).to("true");
     97         bindConstant().annotatedWith(EnumValue.class).to("TEE");
     98         bindConstant().annotatedWith(ClassName.class).to(Foo.class.getName());
     99       }
    100     });
    101 
    102     Foo foo = injector.getInstance(Foo.class);
    103 
    104     checkNumbers(
    105       foo.integerField,
    106       foo.primitiveIntField,
    107       foo.longField,
    108       foo.primitiveLongField,
    109       foo.byteField,
    110       foo.primitiveByteField,
    111       foo.shortField,
    112       foo.primitiveShortField,
    113       foo.floatField,
    114       foo.primitiveFloatField,
    115       foo.doubleField,
    116       foo.primitiveDoubleField
    117     );
    118 
    119     assertEquals(Bar.TEE, foo.enumField);
    120     assertEquals(Foo.class, foo.classField);
    121   }
    122 
    123   public void testConstantInjectionWithExplicitBindingsRequired() throws CreationException {
    124     Injector injector = Guice.createInjector(new AbstractModule() {
    125       @Override protected void configure() {
    126         binder().requireExplicitBindings();
    127         bind(Foo.class);
    128         bindConstant().annotatedWith(NumericValue.class).to("5");
    129         bindConstant().annotatedWith(BooleanValue.class).to("true");
    130         bindConstant().annotatedWith(EnumValue.class).to("TEE");
    131         bindConstant().annotatedWith(ClassName.class).to(Foo.class.getName());
    132       }
    133     });
    134 
    135     Foo foo = injector.getInstance(Foo.class);
    136 
    137     checkNumbers(
    138       foo.integerField,
    139       foo.primitiveIntField,
    140       foo.longField,
    141       foo.primitiveLongField,
    142       foo.byteField,
    143       foo.primitiveByteField,
    144       foo.shortField,
    145       foo.primitiveShortField,
    146       foo.floatField,
    147       foo.primitiveFloatField,
    148       foo.doubleField,
    149       foo.primitiveDoubleField
    150     );
    151 
    152     assertEquals(Bar.TEE, foo.enumField);
    153     assertEquals(Foo.class, foo.classField);
    154   }
    155 
    156   void checkNumbers(Number... ns) {
    157     for (Number n : ns) {
    158       assertEquals(5, n.intValue());
    159     }
    160   }
    161 
    162   static class OuterErrorModule extends AbstractModule {
    163     @Override protected void configure() {
    164       install(new InnerErrorModule());
    165     }
    166   }
    167 
    168   static class InnerErrorModule extends AbstractModule {
    169     @Override protected void configure() {
    170       bindConstant().annotatedWith(NumericValue.class).to("invalid");
    171     }
    172   }
    173 
    174   public void testInvalidInteger() throws CreationException {
    175     Injector injector = Guice.createInjector(new OuterErrorModule());
    176     try {
    177       injector.getInstance(InvalidInteger.class);
    178       fail();
    179     } catch (ConfigurationException expected) {
    180       assertContains(expected.getMessage(),
    181           "Error converting 'invalid' (bound at " + InnerErrorModule.class.getName()
    182               + getDeclaringSourcePart(getClass()),
    183           asModuleChain(OuterErrorModule.class, InnerErrorModule.class),
    184           "using TypeConverter<Integer> which matches identicalTo(class java.lang.Integer)"
    185               + " (bound at [unknown source]).",
    186           "Reason: java.lang.RuntimeException: For input string: \"invalid\"");
    187     }
    188   }
    189 
    190   public static class InvalidInteger {
    191     @Inject @NumericValue Integer integerField;
    192   }
    193 
    194   public void testInvalidCharacter() throws CreationException {
    195     Injector injector = Guice.createInjector(new AbstractModule() {
    196       @Override protected void configure() {
    197         bindConstant().annotatedWith(NumericValue.class).to("invalid");
    198       }
    199     });
    200 
    201     try {
    202       injector.getInstance(InvalidCharacter.class);
    203       fail();
    204     } catch (ConfigurationException expected) {
    205       assertContains(expected.getMessage(), "Error converting 'invalid'");
    206       assertContains(expected.getMessage(), "bound at " + getClass().getName());
    207       assertContains(expected.getMessage(), "to java.lang.Character");
    208     }
    209   }
    210 
    211   public static class InvalidCharacter {
    212     @Inject @NumericValue char foo;
    213   }
    214 
    215   public void testInvalidEnum() throws CreationException {
    216     Injector injector = Guice.createInjector(new AbstractModule() {
    217       @Override protected void configure() {
    218         bindConstant().annotatedWith(NumericValue.class).to("invalid");
    219       }
    220     });
    221 
    222     try {
    223       injector.getInstance(InvalidEnum.class);
    224       fail();
    225     } catch (ConfigurationException expected) {
    226       assertContains(expected.getMessage(), "Error converting 'invalid'");
    227       assertContains(expected.getMessage(), "bound at " + getClass().getName());
    228       assertContains(expected.getMessage(), "to " + Bar.class.getName());
    229     }
    230   }
    231 
    232   public static class InvalidEnum {
    233     @Inject @NumericValue Bar foo;
    234   }
    235 
    236   public void testToInstanceIsTreatedLikeConstant() throws CreationException {
    237     Injector injector = Guice.createInjector(new AbstractModule() {
    238       @Override protected void configure() {
    239         bind(String.class).toInstance("5");
    240         bind(LongHolder.class);
    241       }
    242     });
    243 
    244     assertEquals(5L, (long) injector.getInstance(LongHolder.class).foo);
    245   }
    246 
    247   static class LongHolder {
    248     @Inject Long foo;
    249   }
    250 
    251   public void testCustomTypeConversion() throws CreationException {
    252     final Date result = new Date();
    253 
    254     Injector injector = Guice.createInjector(new AbstractModule() {
    255       @Override protected void configure() {
    256         convertToTypes(Matchers.only(TypeLiteral.get(Date.class)) , mockTypeConverter(result));
    257         bindConstant().annotatedWith(NumericValue.class).to("Today");
    258         bind(DateHolder.class);
    259       }
    260     });
    261 
    262     assertSame(result, injector.getInstance(DateHolder.class).date);
    263 
    264     Binding<Date> binding = injector.getBinding(Key.get(Date.class, NumericValue.class));
    265     assertTrue(binding instanceof ConvertedConstantBinding<?>);
    266 
    267     TypeConverterBinding converterBinding = ((ConvertedConstantBinding<?>)binding).getTypeConverterBinding();
    268     assertEquals("CustomConverter", converterBinding.getTypeConverter().toString());
    269 
    270     assertTrue(injector.getTypeConverterBindings().contains(converterBinding));
    271   }
    272 
    273   static class InvalidCustomValueModule extends AbstractModule {
    274     @Override protected void configure() {
    275       convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), failingTypeConverter());
    276       bindConstant().annotatedWith(NumericValue.class).to("invalid");
    277       bind(DateHolder.class);
    278     }
    279   }
    280 
    281   public void testInvalidCustomValue() throws CreationException {
    282     Module module = new InvalidCustomValueModule();
    283     try {
    284       Guice.createInjector(module);
    285       fail();
    286     } catch (CreationException expected) {
    287       Throwable cause = Iterables.getOnlyElement(expected.getErrorMessages()).getCause();
    288       assertTrue(cause instanceof UnsupportedOperationException);
    289       assertContains(expected.getMessage(),
    290           "1) Error converting 'invalid' (bound at ", getClass().getName(),
    291           getDeclaringSourcePart(getClass()), "to java.util.Date",
    292           "using BrokenConverter which matches only(java.util.Date) ",
    293           "(bound at " + getClass().getName(), getDeclaringSourcePart(getClass()),
    294           "Reason: java.lang.UnsupportedOperationException: Cannot convert",
    295           "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:");
    296     }
    297   }
    298 
    299   static class OuterModule extends AbstractModule {
    300     private final Module converterModule;
    301     OuterModule(Module converterModule) {
    302       this.converterModule = converterModule;
    303     }
    304 
    305     @Override protected void configure() {
    306       install(new InnerModule(converterModule));
    307     }
    308   }
    309 
    310   static class InnerModule extends AbstractModule {
    311     private final Module converterModule;
    312     InnerModule(Module converterModule) {
    313       this.converterModule = converterModule;
    314     }
    315 
    316     @Override protected void configure() {
    317       install(converterModule);
    318       bindConstant().annotatedWith(NumericValue.class).to("foo");
    319       bind(DateHolder.class);
    320     }
    321   }
    322 
    323   class ConverterNullModule extends AbstractModule {
    324     @Override protected void configure() {
    325       convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(null));
    326     }
    327   }
    328 
    329   public void testNullCustomValue() {
    330     try {
    331       Guice.createInjector(new OuterModule(new ConverterNullModule()));
    332       fail();
    333     } catch (CreationException expected) {
    334       assertContains(expected.getMessage(),
    335           "1) Received null converting 'foo' (bound at ",
    336           getClass().getName(),
    337           getDeclaringSourcePart(getClass()),
    338           asModuleChain(OuterModule.class, InnerModule.class),
    339           "to java.util.Date",
    340           "using CustomConverter which matches only(java.util.Date) ",
    341           "(bound at " + getClass().getName(),
    342           getDeclaringSourcePart(getClass()),
    343           asModuleChain(OuterModule.class, InnerModule.class, ConverterNullModule.class),
    344           "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:",
    345           asModuleChain(OuterModule.class, InnerModule.class));
    346     }
    347   }
    348 
    349   class ConverterCustomModule extends AbstractModule {
    350     @Override protected void configure() {
    351       convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(-1));
    352     }
    353   }
    354 
    355   public void testCustomValueTypeMismatch() {
    356     try {
    357       Guice.createInjector(new OuterModule(new ConverterCustomModule()));
    358       fail();
    359     } catch (CreationException expected) {
    360       assertContains(expected.getMessage(),
    361           "1) Type mismatch converting 'foo' (bound at ",
    362           getClass().getName(),
    363           getDeclaringSourcePart(getClass()),
    364           asModuleChain(OuterModule.class, InnerModule.class),
    365           "to java.util.Date",
    366           "using CustomConverter which matches only(java.util.Date) ",
    367           "(bound at " + getClass().getName(),
    368           getDeclaringSourcePart(getClass()),
    369           asModuleChain(OuterModule.class, InnerModule.class, ConverterCustomModule.class),
    370           "Converter returned -1.",
    371           "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:",
    372           asModuleChain(OuterModule.class, InnerModule.class));
    373     }
    374   }
    375 
    376   public void testStringIsConvertedOnlyOnce() {
    377     final TypeConverter converter = new TypeConverter() {
    378       boolean converted = false;
    379       public Object convert(String value, TypeLiteral<?> toType) {
    380         if (converted) {
    381           throw new AssertionFailedError("converted multiple times!");
    382         }
    383         converted = true;
    384         return new Date();
    385       }
    386     };
    387 
    388     Injector injector = Guice.createInjector(new AbstractModule() {
    389       @Override protected void configure() {
    390         convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), converter);
    391         bindConstant().annotatedWith(NumericValue.class).to("unused");
    392       }
    393     });
    394 
    395     Date first = injector.getInstance(Key.get(Date.class, NumericValue.class));
    396     Date second = injector.getInstance(Key.get(Date.class, NumericValue.class));
    397     assertSame(first, second);
    398   }
    399 
    400   class OuterAmbiguousModule extends AbstractModule {
    401     @Override protected void configure() {
    402       install(new InnerAmbiguousModule());
    403     }
    404   }
    405 
    406   class InnerAmbiguousModule extends AbstractModule {
    407     @Override protected void configure() {
    408       install(new Ambiguous1Module());
    409       install(new Ambiguous2Module());
    410       bindConstant().annotatedWith(NumericValue.class).to("foo");
    411       bind(DateHolder.class);
    412     }
    413   }
    414 
    415   class Ambiguous1Module extends AbstractModule {
    416     @Override protected void configure() {
    417       convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(new Date()));
    418     }
    419   }
    420 
    421   class Ambiguous2Module extends AbstractModule {
    422     @Override protected void configure() {
    423       convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(new Date()));
    424     }
    425   }
    426 
    427   public void testAmbiguousTypeConversion() {
    428     try {
    429       Guice.createInjector(new OuterAmbiguousModule());
    430       fail();
    431     } catch (CreationException expected) {
    432       assertContains(expected.getMessage(),
    433           "1) Multiple converters can convert 'foo' (bound at ", getClass().getName(),
    434           getDeclaringSourcePart(getClass()),
    435           asModuleChain(OuterAmbiguousModule.class, InnerAmbiguousModule.class),
    436           "to java.util.Date:",
    437           "CustomConverter which matches only(java.util.Date) (bound at "
    438               + Ambiguous1Module.class.getName()
    439               + getDeclaringSourcePart(getClass()),
    440           asModuleChain(
    441               OuterAmbiguousModule.class, InnerAmbiguousModule.class, Ambiguous1Module.class),
    442           "and",
    443           "CustomConverter which matches only(java.util.Date) (bound at "
    444               + Ambiguous2Module.class.getName()
    445               + getDeclaringSourcePart(getClass()),
    446           asModuleChain(
    447               OuterAmbiguousModule.class, InnerAmbiguousModule.class, Ambiguous2Module.class),
    448           "Please adjust your type converter configuration to avoid overlapping matches.",
    449           "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:");
    450     }
    451   }
    452 
    453   TypeConverter mockTypeConverter(final Object result) {
    454     return new TypeConverter() {
    455       public Object convert(String value, TypeLiteral<?> toType) {
    456         return result;
    457       }
    458 
    459       @Override public String toString() {
    460         return "CustomConverter";
    461       }
    462     };
    463   }
    464 
    465   private static TypeConverter failingTypeConverter() {
    466     return new TypeConverter() {
    467       public Object convert(String value, TypeLiteral<?> toType) {
    468         throw new UnsupportedOperationException("Cannot convert");
    469       }
    470       @Override public String toString() {
    471         return "BrokenConverter";
    472       }
    473     };
    474   }
    475 
    476   static class DateHolder {
    477     @Inject @NumericValue Date date;
    478   }
    479 
    480   public void testCannotConvertUnannotatedBindings() {
    481     Injector injector = Guice.createInjector(new AbstractModule() {
    482       @Override protected void configure() {
    483         bind(String.class).toInstance("55");
    484       }
    485     });
    486 
    487     try {
    488       injector.getInstance(Integer.class);
    489       fail("Converted an unannotated String to an Integer");
    490     } catch (ConfigurationException expected) {
    491       Asserts.assertContains(expected.getMessage(),
    492           "Could not find a suitable constructor in java.lang.Integer.");
    493     }
    494   }
    495 }
    496