1/**
2 * Copyright (C) 2008 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;
18
19import static com.google.inject.Asserts.asModuleChain;
20import static com.google.inject.Asserts.assertContains;
21import static com.google.inject.Asserts.getDeclaringSourcePart;
22import static com.google.inject.name.Names.named;
23
24import com.google.common.collect.ImmutableSet;
25import com.google.inject.name.Named;
26import com.google.inject.name.Names;
27import com.google.inject.spi.Dependency;
28import com.google.inject.spi.ExposedBinding;
29import com.google.inject.spi.PrivateElements;
30import com.google.inject.util.Types;
31
32import junit.framework.TestCase;
33
34import java.util.ArrayList;
35import java.util.Collection;
36import java.util.List;
37
38/**
39 * @author jessewilson@google.com (Jesse Wilson)
40 */
41public class PrivateModuleTest extends TestCase {
42
43  public void testBasicUsage() {
44    Injector injector = Guice.createInjector(new AbstractModule() {
45      @Override protected void configure() {
46        bind(String.class).annotatedWith(named("a")).toInstance("public");
47
48        install(new PrivateModule() {
49          @Override public void configure() {
50            bind(String.class).annotatedWith(named("b")).toInstance("i");
51
52            bind(AB.class).annotatedWith(named("one")).to(AB.class);
53            expose(AB.class).annotatedWith(named("one"));
54          }
55        });
56
57        install(new PrivateModule() {
58          @Override public void configure() {
59            bind(String.class).annotatedWith(named("b")).toInstance("ii");
60
61            bind(AB.class).annotatedWith(named("two")).to(AB.class);
62            expose(AB.class).annotatedWith(named("two"));
63          }
64        });
65      }
66    });
67
68    AB ab1 = injector.getInstance(Key.get(AB.class, named("one")));
69    assertEquals("public", ab1.a);
70    assertEquals("i", ab1.b);
71
72    AB ab2 = injector.getInstance(Key.get(AB.class, named("two")));
73    assertEquals("public", ab2.a);
74    assertEquals("ii", ab2.b);
75  }
76
77  public void testWithoutPrivateModules() {
78    Injector injector = Guice.createInjector(new AbstractModule() {
79      @Override protected void configure() {
80        PrivateBinder bindA = binder().newPrivateBinder();
81        bindA.bind(String.class).annotatedWith(named("a")).toInstance("i");
82        bindA.expose(String.class).annotatedWith(named("a"));
83        bindA.bind(String.class).annotatedWith(named("c")).toInstance("private to A");
84
85        PrivateBinder bindB = binder().newPrivateBinder();
86        bindB.bind(String.class).annotatedWith(named("b")).toInstance("ii");
87        bindB.expose(String.class).annotatedWith(named("b"));
88        bindB.bind(String.class).annotatedWith(named("c")).toInstance("private to B");
89      }
90    });
91
92    assertEquals("i", injector.getInstance(Key.get(String.class, named("a"))));
93    assertEquals("ii", injector.getInstance(Key.get(String.class, named("b"))));
94  }
95
96  public void testMisplacedExposedAnnotation() {
97    try {
98      Guice.createInjector(new AbstractModule() {
99        @Override protected void configure() {}
100
101        @Provides @Exposed
102        String provideString() {
103          return "i";
104        }
105      });
106      fail();
107    } catch (CreationException expected) {
108      assertContains(expected.getMessage(), "Cannot expose java.lang.String on a standard binder. ",
109          "Exposed bindings are only applicable to private binders.",
110          " at " + PrivateModuleTest.class.getName(), "provideString(PrivateModuleTest.java:");
111    }
112  }
113
114  public void testMisplacedExposeStatement() {
115    try {
116      Guice.createInjector(new AbstractModule() {
117        @Override protected void configure() {
118          ((PrivateBinder) binder()).expose(String.class).annotatedWith(named("a"));
119        }
120      });
121      fail();
122    } catch (CreationException expected) {
123      assertContains(expected.getMessage(), "Cannot expose java.lang.String on a standard binder. ",
124          "Exposed bindings are only applicable to private binders.",
125          " at " + PrivateModuleTest.class.getName(), getDeclaringSourcePart(getClass()));
126    }
127  }
128
129  public void testPrivateModulesAndProvidesMethods() {
130    Injector injector = Guice.createInjector(new AbstractModule() {
131      @Override protected void configure() {
132        install(new PrivateModule() {
133          @Override public void configure() {
134            expose(String.class).annotatedWith(named("a"));
135          }
136
137          @Provides @Named("a") String providePublicA() {
138            return "i";
139          }
140
141          @Provides @Named("b") String providePrivateB() {
142            return "private";
143          }
144        });
145
146        install(new PrivateModule() {
147          @Override public void configure() {}
148
149          @Provides @Named("c") String providePrivateC() {
150            return "private";
151          }
152
153          @Provides @Exposed @Named("d") String providePublicD() {
154            return "ii";
155          }
156        });
157      }
158    });
159
160    assertEquals("i", injector.getInstance(Key.get(String.class, named("a"))));
161
162    try {
163      injector.getInstance(Key.get(String.class, named("b")));
164      fail();
165    } catch(ConfigurationException expected) {
166    }
167
168    try {
169      injector.getInstance(Key.get(String.class, named("c")));
170      fail();
171    } catch(ConfigurationException expected) {
172    }
173
174    assertEquals("ii", injector.getInstance(Key.get(String.class, named("d"))));
175  }
176
177  public void testCannotBindAKeyExportedByASibling() {
178    try {
179      Guice.createInjector(new AbstractModule() {
180        @Override protected void configure() {
181          install(new PrivateModule() {
182            @Override public void configure() {
183              bind(String.class).toInstance("public");
184              expose(String.class);
185            }
186          });
187
188          install(new PrivateModule() {
189            @Override public void configure() {
190              bind(String.class).toInstance("private");
191            }
192          });
193        }
194      });
195      fail();
196    } catch (CreationException expected) {
197      assertContains(expected.getMessage(),
198          "A binding to java.lang.String was already configured at ",
199          getClass().getName(), getDeclaringSourcePart(getClass()),
200          " at " + getClass().getName(), getDeclaringSourcePart(getClass()));
201    }
202  }
203
204  public void testExposeButNoBind() {
205    try {
206      Guice.createInjector(new AbstractModule() {
207        @Override protected void configure() {
208          bind(String.class).annotatedWith(named("a")).toInstance("a");
209          bind(String.class).annotatedWith(named("b")).toInstance("b");
210
211          install(new PrivateModule() {
212            @Override public void configure() {
213              expose(AB.class);
214            }
215          });
216        }
217      });
218      fail("AB was exposed but not bound");
219    } catch (CreationException expected) {
220      assertContains(expected.getMessage(),
221          "Could not expose() " + AB.class.getName() + ", it must be explicitly bound",
222          getDeclaringSourcePart(getClass()));
223    }
224  }
225
226  /**
227   * Ensure that when we've got errors in different private modules, Guice presents all errors
228   * in a unified message.
229   */
230  public void testMessagesFromPrivateModulesAreNicelyIntegrated() {
231    try {
232      Guice.createInjector(
233          new PrivateModule() {
234            @Override public void configure() {
235              bind(C.class);
236            }
237          },
238          new PrivateModule() {
239            @Override public void configure() {
240              bind(AB.class);
241            }
242          }
243      );
244      fail();
245    } catch (CreationException expected) {
246      assertContains(expected.getMessage(),
247          "1) No implementation for " + C.class.getName() + " was bound.",
248          "at " + getClass().getName(), getDeclaringSourcePart(getClass()),
249          "2) No implementation for " + String.class.getName(), "Named(value=a) was bound.",
250          "for field at " + AB.class.getName() + ".a(PrivateModuleTest.java:",
251          "3) No implementation for " + String.class.getName(), "Named(value=b) was bound.",
252          "for field at " + AB.class.getName() + ".b(PrivateModuleTest.java:",
253          "3 errors");
254    }
255  }
256
257  public void testNestedPrivateInjectors() {
258    Injector injector = Guice.createInjector(new PrivateModule() {
259      @Override public void configure() {
260        expose(String.class);
261
262        install(new PrivateModule() {
263          @Override public void configure() {
264            bind(String.class).toInstance("nested");
265            expose(String.class);
266          }
267        });
268      }
269    });
270
271    assertEquals("nested", injector.getInstance(String.class));
272  }
273
274  public void testInstallingRegularModulesFromPrivateModules() {
275    Injector injector = Guice.createInjector(new PrivateModule() {
276      @Override public void configure() {
277        expose(String.class);
278
279        install(new AbstractModule() {
280          @Override protected void configure() {
281            bind(String.class).toInstance("nested");
282          }
283        });
284      }
285    });
286
287    assertEquals("nested", injector.getInstance(String.class));
288  }
289
290  public void testNestedPrivateModulesWithSomeKeysUnexposed() {
291    Injector injector = Guice.createInjector(new PrivateModule() {
292      @Override public void configure() {
293        bind(String.class).annotatedWith(named("bound outer, exposed outer")).toInstance("boeo");
294        expose(String.class).annotatedWith(named("bound outer, exposed outer"));
295        bind(String.class).annotatedWith(named("bound outer, exposed none")).toInstance("boen");
296        expose(String.class).annotatedWith(named("bound inner, exposed both"));
297
298        install(new PrivateModule() {
299          @Override public void configure() {
300            bind(String.class).annotatedWith(named("bound inner, exposed both")).toInstance("bieb");
301            expose(String.class).annotatedWith(named("bound inner, exposed both"));
302            bind(String.class).annotatedWith(named("bound inner, exposed none")).toInstance("bien");
303          }
304        });
305      }
306    });
307
308    assertEquals("boeo",
309        injector.getInstance(Key.get(String.class, named("bound outer, exposed outer"))));
310    assertEquals("bieb",
311        injector.getInstance(Key.get(String.class, named("bound inner, exposed both"))));
312
313    try {
314      injector.getInstance(Key.get(String.class, named("bound outer, exposed none")));
315      fail();
316    } catch (ConfigurationException expected) {
317    }
318
319    try {
320      injector.getInstance(Key.get(String.class, named("bound inner, exposed none")));
321      fail();
322    } catch (ConfigurationException expected) {
323    }
324  }
325
326  public void testDependenciesBetweenPrivateAndPublic() {
327    Injector injector = Guice.createInjector(
328        new PrivateModule() {
329          @Override protected void configure() {}
330
331          @Provides @Exposed @Named("a") String provideA() {
332            return "A";
333          }
334
335          @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) {
336            return ab + "C";
337          }
338        },
339        new AbstractModule() {
340          @Override protected void configure() {}
341
342          @Provides @Named("ab") String provideAb(@Named("a") String a) {
343            return a + "B";
344          }
345
346          @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) {
347            return abc + "D";
348          }
349        }
350    );
351
352    assertEquals("ABCD", injector.getInstance(Key.get(String.class, named("abcd"))));
353  }
354
355  public void testDependenciesBetweenPrivateAndPublicWithPublicEagerSingleton() {
356    Injector injector = Guice.createInjector(
357        new PrivateModule() {
358          @Override protected void configure() {}
359
360          @Provides @Exposed @Named("a") String provideA() {
361            return "A";
362          }
363
364          @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) {
365            return ab + "C";
366          }
367        },
368        new AbstractModule() {
369          @Override protected void configure() {
370            bind(String.class).annotatedWith(named("abcde")).toProvider(new Provider<String>() {
371              @Inject @Named("abcd") String abcd;
372
373              public String get() {
374                return abcd + "E";
375              }
376            }).asEagerSingleton();
377          }
378
379          @Provides @Named("ab") String provideAb(@Named("a") String a) {
380            return a + "B";
381          }
382
383          @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) {
384            return abc + "D";
385          }
386        }
387    );
388
389    assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde"))));
390  }
391
392  public void testDependenciesBetweenPrivateAndPublicWithPrivateEagerSingleton() {
393    Injector injector = Guice.createInjector(
394        new AbstractModule() {
395          @Override protected void configure() {}
396
397          @Provides @Named("ab") String provideAb(@Named("a") String a) {
398            return a + "B";
399          }
400
401          @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) {
402            return abc + "D";
403          }
404        },
405        new PrivateModule() {
406          @Override protected void configure() {
407            bind(String.class).annotatedWith(named("abcde")).toProvider(new Provider<String>() {
408              @Inject @Named("abcd") String abcd;
409
410              public String get() {
411                return abcd + "E";
412              }
413            }).asEagerSingleton();
414            expose(String.class).annotatedWith(named("abcde"));
415          }
416
417          @Provides @Exposed @Named("a") String provideA() {
418            return "A";
419          }
420
421          @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) {
422            return ab + "C";
423          }
424        }
425    );
426
427    assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde"))));
428  }
429
430  static class AB {
431    @Inject @Named("a") String a;
432    @Inject @Named("b") String b;
433  }
434
435  interface C {}
436
437  public void testSpiAccess() {
438    Injector injector = Guice.createInjector(new PrivateModule() {
439          @Override public void configure() {
440            bind(String.class).annotatedWith(named("a")).toInstance("private");
441            bind(String.class).annotatedWith(named("b")).toInstance("exposed");
442            expose(String.class).annotatedWith(named("b"));
443          }
444        });
445
446    ExposedBinding<?> binding
447        = (ExposedBinding<?>) injector.getBinding(Key.get(String.class, Names.named("b")));
448    assertEquals(ImmutableSet.<Dependency<?>>of(Dependency.get(Key.get(Injector.class))),
449        binding.getDependencies());
450    PrivateElements privateElements = binding.getPrivateElements();
451    assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class, named("b"))),
452        privateElements.getExposedKeys());
453    assertContains(privateElements.getExposedSource(Key.get(String.class, named("b"))).toString(),
454        PrivateModuleTest.class.getName(), getDeclaringSourcePart(getClass()));
455    Injector privateInjector = privateElements.getInjector();
456    assertEquals("private", privateInjector.getInstance(Key.get(String.class, Names.named("a"))));
457  }
458
459  public void testParentBindsSomethingInPrivate() {
460    try {
461      Guice.createInjector(new FailingModule());
462      fail();
463    } catch(CreationException expected) {
464      assertEquals(1, expected.getErrorMessages().size());
465      assertContains(expected.toString(),
466          "Unable to create binding for java.util.List.",
467          "It was already configured on one or more child injectors or private modules",
468          "bound at " + FailingPrivateModule.class.getName() + ".configure(",
469          asModuleChain(FailingModule.class, ManyPrivateModules.class, FailingPrivateModule.class),
470          "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(",
471          asModuleChain(
472              FailingModule.class, ManyPrivateModules.class, SecondFailingPrivateModule.class),
473          "If it was in a PrivateModule, did you forget to expose the binding?",
474          "at " + FailingModule.class.getName() + ".configure(");
475    }
476  }
477
478  public void testParentBindingToPrivateLinkedJitBinding() {
479    Injector injector = Guice.createInjector(new ManyPrivateModules());
480    try {
481      injector.getBinding(Key.get(Types.providerOf(List.class)));
482      fail();
483    } catch(ConfigurationException expected) {
484      assertEquals(1, expected.getErrorMessages().size());
485      assertContains(expected.toString(),
486          "Unable to create binding for com.google.inject.Provider<java.util.List>.",
487          "It was already configured on one or more child injectors or private modules",
488          "bound at " + FailingPrivateModule.class.getName() + ".configure(",
489          asModuleChain(ManyPrivateModules.class, FailingPrivateModule.class),
490          "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(",
491          asModuleChain(ManyPrivateModules.class, SecondFailingPrivateModule.class),
492          "If it was in a PrivateModule, did you forget to expose the binding?",
493          "while locating com.google.inject.Provider<java.util.List>");
494    }
495  }
496
497  public void testParentBindingToPrivateJitBinding() {
498    Injector injector = Guice.createInjector(new ManyPrivateModules());
499    try {
500      injector.getBinding(PrivateFoo.class);
501      fail();
502    } catch(ConfigurationException expected) {
503      assertEquals(1, expected.getErrorMessages().size());
504      assertContains(expected.toString(),
505          "Unable to create binding for " + PrivateFoo.class.getName(),
506          "It was already configured on one or more child injectors or private modules",
507          "(bound by a just-in-time binding)",
508          "If it was in a PrivateModule, did you forget to expose the binding?",
509          "while locating " + PrivateFoo.class.getName());
510    }
511  }
512
513  private static class FailingModule extends AbstractModule {
514    @Override protected void configure() {
515      bind(Collection.class).to(List.class);
516      install(new ManyPrivateModules());
517    }
518  }
519
520  private static class ManyPrivateModules extends AbstractModule {
521    @Override protected void configure() {
522      // make sure duplicate sources are collapsed
523      install(new FailingPrivateModule());
524      install(new FailingPrivateModule());
525      // but additional sources are listed
526      install(new SecondFailingPrivateModule());
527    }
528  }
529
530  private static class FailingPrivateModule extends PrivateModule {
531    @Override protected void configure() {
532      bind(List.class).toInstance(new ArrayList());
533
534      // Add the Provider<List> binding, created just-in-time,
535      // to make sure our linked JIT bindings have the correct source.
536      getProvider(Key.get(Types.providerOf(List.class)));
537
538      // Request a JIT binding for PrivateFoo, which can only
539      // be created in the private module because it depends
540      // on List.
541      getProvider(PrivateFoo.class);
542    }
543  }
544
545  /** A second class, so we can see another name in the source list. */
546  private static class SecondFailingPrivateModule extends PrivateModule {
547    @Override protected void configure() {
548      bind(List.class).toInstance(new ArrayList());
549
550      // Add the Provider<List> binding, created just-in-time,
551      // to make sure our linked JIT bindings have the correct source.
552      getProvider(Key.get(Types.providerOf(List.class)));
553
554      // Request a JIT binding for PrivateFoo, which can only
555      // be created in the private module because it depends
556      // on List.
557      getProvider(PrivateFoo.class);
558    }
559  }
560
561  private static class PrivateFoo {
562    @Inject List list;
563  }
564}
565