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