commit 0bb5cf504bff78f49598c744f7e763b0f54896af Author: Jörg Prante Date: Thu Jan 7 19:22:16 2016 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e42bcc --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/data +/work +/logs +/.idea +/target +.DS_Store +*.iml +/.settings +/.classpath +/.project +/.gradle +/build \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..57000f7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +sudo: false +language: java +jdk: + - oraclejdk8 +cache: + directories: + - $HOME/.m2 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fada35 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ += Guice + +This is a xbib Guice, a build of Guice with the following differences to the +original [Google Guice Core Library](https://github.com/google/guice): + +- only external dependencies are +-- javax.inject:javax.inject:1 +-- javax.annotation:javax.annotation-api:1.2 +-- com.google.guava:guava:19.0 +- the extra AOP stuff was removed (aopalliance/cglib), no intercepting any more +- removed all logging, uses of java.util.logger.* stuff and logger default binding +- removed all java.io.Serializable dependent stuff +- reformatted source by IntelliJ and cleaned up source of @author and @since tags +- target of compilation is Java 8, no support for Java 6/7 +- introduced Gradle as build system +- junit tests made compatible to junit 4.12 + +All credits belong to the original authors, especially Bob Lee http://blog.crazybob.org/ + += License + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..71d0020 --- /dev/null +++ b/build.gradle @@ -0,0 +1,90 @@ + +def xbibGroup = 'org.xbib' +def xbibVersion = '4.0' + +group = xbibGroup +version = xbibVersion + +println "Current JVM: " + org.gradle.internal.jvm.Jvm.current() + +apply plugin: 'java' +apply plugin: 'maven' + +repositories { + mavenCentral() + mavenLocal() + jcenter() + maven { + url "http://xbib.org/repository" + } +} + +configurations { + wagon +} + +dependencies { + compile "javax.inject:javax.inject:1" + compile 'javax.annotation:javax.annotation-api:1.2' + compile "com.google.guava:guava:19.0" + testCompile "junit:junit:4.12" + testCompile "org.apache.logging.log4j:log4j-slf4j-impl:2.5" + testCompile "org.apache.logging.log4j:log4j-core:2.5" + testCompile "javax.inject:javax.inject-tck:1" + testCompile "com.google.guava:guava-testlib:19.0" + wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +[compileJava, compileTestJava]*.options.collect { options -> + options.encoding = 'UTF-8' + options.compilerArgs << '-Xlint:-serial,-path,-rawtypes,-unchecked' +} + +test { + exclude '*$*' + exclude '**/ErrorHandlingTest*' + exclude '**/OSGiContainerTest*' + exclude '**/ScopesTest*' + exclude '**/TypeConversionTest*' + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + from sourceSets.main.allSource + classifier 'sources' +} + +artifacts { + archives sourcesJar +} + +uploadArchives { + repositories { + if (project.hasProperty("xbibUsername")) { + mavenDeployer { + configuration = configurations.wagon + repository( + id: 'xbib.org', + url: uri('scpexe://xbib.org/repository'), + authentication: [userName: xbibUsername, privateKey: xbibPrivateKey] + ) + pom.project { + inceptionYear '2016' + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + } + } + } + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..722622c --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + org.xbib + guice + 4.0 + + Guice + + + + javax.inject + javax.inject + 1 + + + javax.annotation + javax.annotation-api + 1.2 + + + com.google.guava + guava + 19.0 + + + javax.inject + javax.inject-tck + 1 + test + + + com.google.guava + guava-testlib + 19.0 + test + + + + + + + maven-compiler-plugin + 3.3 + + UTF-8 + 1.8 + 1.8 + true + true + true + true + -Xlint:all,-serial,-path,-rawtypes,-unchecked + + + + maven-surefire-plugin + 2.19.1 + + + **/*$* + **/ErrorHandlingTest* + **/OSGiContainerTest* + **/ScopesTest* + **/TypeConversionTest* + + + + + maven-jar-plugin + 2.6 + + + LICENSE + NOTICE + + + + + maven-source-plugin + 2.4 + + + attach-sources + + jar + + + + + + maven-javadoc-plugin + 2.10.3 + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..cbf896b --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'guice' diff --git a/src/main/java/com/google/inject/AbstractModule.java b/src/main/java/com/google/inject/AbstractModule.java new file mode 100644 index 0000000..e00957e --- /dev/null +++ b/src/main/java/com/google/inject/AbstractModule.java @@ -0,0 +1,220 @@ +package com.google.inject; + +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.AnnotatedConstantBindingBuilder; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.matcher.Matcher; +import com.google.inject.spi.Message; +import com.google.inject.spi.ProvisionListener; +import com.google.inject.spi.TypeConverter; +import com.google.inject.spi.TypeListener; + +import java.lang.annotation.Annotation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * A support class for {@link Module}s which reduces repetition and results in + * a more readable configuration. Simply extend this class, implement {@link + * #configure()}, and call the inherited methods which mirror those found in + * {@link Binder}. For example: + * + *
+ * public class MyModule extends AbstractModule {
+ *   protected void configure() {
+ *     bind(Service.class).to(ServiceImpl.class).in(Singleton.class);
+ *     bind(CreditCardPaymentService.class);
+ *     bind(PaymentService.class).to(CreditCardPaymentService.class);
+ *     bindConstant().annotatedWith(Names.named("port")).to(8080);
+ *   }
+ * }
+ * 
+ * + */ +public abstract class AbstractModule implements Module { + + Binder binder; + + public final synchronized void configure(Binder builder) { + checkState(this.binder == null, "Re-entry is not allowed."); + + this.binder = checkNotNull(builder, "builder"); + try { + configure(); + } finally { + this.binder = null; + } + } + + /** + * Configures a {@link Binder} via the exposed methods. + */ + protected abstract void configure(); + + /** + * Gets direct access to the underlying {@code Binder}. + */ + protected Binder binder() { + checkState(binder != null, "The binder can only be used inside configure()"); + return binder; + } + + /** + * @see Binder#bindScope(Class, Scope) + */ + protected void bindScope(Class scopeAnnotation, + Scope scope) { + binder().bindScope(scopeAnnotation, scope); + } + + /** + * @see Binder#bind(Key) + */ + protected LinkedBindingBuilder bind(Key key) { + return binder().bind(key); + } + + /** + * @see Binder#bind(TypeLiteral) + */ + protected AnnotatedBindingBuilder bind(TypeLiteral typeLiteral) { + return binder().bind(typeLiteral); + } + + /** + * @see Binder#bind(Class) + */ + protected AnnotatedBindingBuilder bind(Class clazz) { + return binder().bind(clazz); + } + + /** + * @see Binder#bindConstant() + */ + protected AnnotatedConstantBindingBuilder bindConstant() { + return binder().bindConstant(); + } + + /** + * @see Binder#install(Module) + */ + protected void install(Module module) { + binder().install(module); + } + + /** + * @see Binder#addError(String, Object[]) + */ + protected void addError(String message, Object... arguments) { + binder().addError(message, arguments); + } + + /** + * @see Binder#addError(Throwable) + */ + protected void addError(Throwable t) { + binder().addError(t); + } + + /** + * @see Binder#addError(Message) + */ + protected void addError(Message message) { + binder().addError(message); + } + + /** + * @see Binder#requestInjection(Object) + */ + protected void requestInjection(Object instance) { + binder().requestInjection(instance); + } + + /** + * @see Binder#requestStaticInjection(Class[]) + */ + protected void requestStaticInjection(Class... types) { + binder().requestStaticInjection(types); + } + + /** + * Adds a dependency from this module to {@code key}. When the injector is + * created, Guice will report an error if {@code key} cannot be injected. + * Note that this requirement may be satisfied by implicit binding, such as + * a public no-arguments constructor. + */ + protected void requireBinding(Key key) { + binder().getProvider(key); + } + + /** + * Adds a dependency from this module to {@code type}. When the injector is + * created, Guice will report an error if {@code type} cannot be injected. + * Note that this requirement may be satisfied by implicit binding, such as + * a public no-arguments constructor. + */ + protected void requireBinding(Class type) { + binder().getProvider(type); + } + + /** + * @see Binder#getProvider(Key) + */ + protected Provider getProvider(Key key) { + return binder().getProvider(key); + } + + /** + * @see Binder#getProvider(Class) + */ + protected Provider getProvider(Class type) { + return binder().getProvider(type); + } + + /** + * @see Binder#convertToTypes + */ + protected void convertToTypes(Matcher> typeMatcher, + TypeConverter converter) { + binder().convertToTypes(typeMatcher, converter); + } + + /** + * @see Binder#currentStage() + */ + protected Stage currentStage() { + return binder().currentStage(); + } + + /** + * @see Binder#getMembersInjector(Class) + */ + protected MembersInjector getMembersInjector(Class type) { + return binder().getMembersInjector(type); + } + + /** + * @see Binder#getMembersInjector(TypeLiteral) + */ + protected MembersInjector getMembersInjector(TypeLiteral type) { + return binder().getMembersInjector(type); + } + + /** + * @see Binder#bindListener(com.google.inject.matcher.Matcher, + * com.google.inject.spi.TypeListener) + */ + protected void bindListener(Matcher> typeMatcher, + TypeListener listener) { + binder().bindListener(typeMatcher, listener); + } + + /** + * @see Binder#bindListener(Matcher, ProvisionListener...) + */ + protected void bindListener(Matcher> bindingMatcher, + ProvisionListener... listener) { + binder().bindListener(bindingMatcher, listener); + } +} diff --git a/src/main/java/com/google/inject/Binder.java b/src/main/java/com/google/inject/Binder.java new file mode 100644 index 0000000..f4242d9 --- /dev/null +++ b/src/main/java/com/google/inject/Binder.java @@ -0,0 +1,451 @@ +package com.google.inject; + +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.AnnotatedConstantBindingBuilder; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.matcher.Matcher; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.Message; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; +import com.google.inject.spi.ProvisionListener; +import com.google.inject.spi.TypeConverter; +import com.google.inject.spi.TypeListener; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; + +/** + * Collects configuration information (primarily bindings) which will be + * used to create an {@link Injector}. Guice provides this object to your + * application's {@link Module} implementors so they may each contribute + * their own bindings and other registrations. + * + *

The Guice Binding EDSL

+ * + * Guice uses an embedded domain-specific language, or EDSL, to help you + * create bindings simply and readably. This approach is great for overall + * usability, but it does come with a small cost: it is difficult to + * learn how to use the Binding EDSL by reading + * method-level javadocs. Instead, you should consult the series of + * examples below. To save space, these examples omit the opening + * {@code binder}, just as you will if your module extends + * {@link AbstractModule}. + * + *
+ *     bind(ServiceImpl.class);
+ * + * This statement does essentially nothing; it "binds the {@code ServiceImpl} + * class to itself" and does not change Guice's default behavior. You may still + * want to use this if you prefer your {@link Module} class to serve as an + * explicit manifest for the services it provides. Also, in rare cases, + * Guice may be unable to validate a binding at injector creation time unless it + * is given explicitly. + * + *
+ *     bind(Service.class).to(ServiceImpl.class);
+ * + * Specifies that a request for a {@code Service} instance with no binding + * annotations should be treated as if it were a request for a + * {@code ServiceImpl} instance. This overrides the function of any + * {@link ImplementedBy @ImplementedBy} or {@link ProvidedBy @ProvidedBy} + * annotations found on {@code Service}, since Guice will have already + * "moved on" to {@code ServiceImpl} before it reaches the point when it starts + * looking for these annotations. + * + *
+ *     bind(Service.class).toProvider(ServiceProvider.class);
+ * + * In this example, {@code ServiceProvider} must extend or implement + * {@code Provider}. This binding specifies that Guice should resolve + * an unannotated injection request for {@code Service} by first resolving an + * instance of {@code ServiceProvider} in the regular way, then calling + * {@link Provider#get get()} on the resulting Provider instance to obtain the + * {@code Service} instance. + * + *

The {@link Provider} you use here does not have to be a "factory"; that + * is, a provider which always creates each instance it provides. + * However, this is generally a good practice to follow. You can then use + * Guice's concept of {@link Scope scopes} to guide when creation should happen + * -- "letting Guice work for you". + * + *

+ *     bind(Service.class).annotatedWith(Red.class).to(ServiceImpl.class);
+ * + * Like the previous example, but only applies to injection requests that use + * the binding annotation {@code @Red}. If your module also includes bindings + * for particular values of the {@code @Red} annotation (see below), + * then this binding will serve as a "catch-all" for any values of {@code @Red} + * that have no exact match in the bindings. + * + *
+ *     bind(ServiceImpl.class).in(Singleton.class);
+ *     // or, alternatively
+ *     bind(ServiceImpl.class).in(Scopes.SINGLETON);
+ * + * Either of these statements places the {@code ServiceImpl} class into + * singleton scope. Guice will create only one instance of {@code ServiceImpl} + * and will reuse it for all injection requests of this type. Note that it is + * still possible to bind another instance of {@code ServiceImpl} if the second + * binding is qualified by an annotation as in the previous example. Guice is + * not overly concerned with preventing you from creating multiple + * instances of your "singletons", only with enabling your application to + * share only one instance if that's all you tell Guice you need. + * + *

Note: a scope specified in this way overrides any scope that + * was specified with an annotation on the {@code ServiceImpl} class. + * + *

Besides {@link Singleton}/{@link Scopes#SINGLETON}, there are + * servlet-specific scopes available in + * {@code com.google.inject.servlet.ServletScopes}, and your Modules can + * contribute their own custom scopes for use here as well. + * + *

+ *     bind(new TypeLiteral<PaymentService<CreditCard>>() {})
+ *         .to(CreditCardPaymentService.class);
+ * + * This admittedly odd construct is the way to bind a parameterized type. It + * tells Guice how to honor an injection request for an element of type + * {@code PaymentService}. The class + * {@code CreditCardPaymentService} must implement the + * {@code PaymentService} interface. Guice cannot currently bind or + * inject a generic type, such as {@code Set}; all type parameters must be + * fully specified. + * + *
+ *     bind(Service.class).toInstance(new ServiceImpl());
+ *     // or, alternatively
+ *     bind(Service.class).toInstance(SomeLegacyRegistry.getService());
+ * + * In this example, your module itself, not Guice, takes responsibility + * for obtaining a {@code ServiceImpl} instance, then asks Guice to always use + * this single instance to fulfill all {@code Service} injection requests. When + * the {@link Injector} is created, it will automatically perform field + * and method injection for this instance, but any injectable constructor on + * {@code ServiceImpl} is simply ignored. Note that using this approach results + * in "eager loading" behavior that you can't control. + * + *
+ *     bindConstant().annotatedWith(ServerHost.class).to(args[0]);
+ * + * Sets up a constant binding. Constant injections must always be annotated. + * When a constant binding's value is a string, it is eligile for conversion to + * all primitive types, to {@link Enum#valueOf(Class, String) all enums}, and to + * {@link Class#forName class literals}. Conversions for other types can be + * configured using {@link #convertToTypes(Matcher, TypeConverter) + * convertToTypes()}. + * + *
+ *   {@literal @}Color("red") Color red; // A member variable (field)
+ *    . . .
+ *     red = MyModule.class.getDeclaredField("red").getAnnotation(Color.class);
+ *     bind(Service.class).annotatedWith(red).to(RedService.class);
+ * + * If your binding annotation has parameters you can apply different bindings to + * different specific values of your annotation. Getting your hands on the + * right instance of the annotation is a bit of a pain -- one approach, shown + * above, is to apply a prototype annotation to a field in your module class, so + * that you can read this annotation instance and give it to Guice. + * + *
+ *     bind(Service.class)
+ *         .annotatedWith(Names.named("blue"))
+ *         .to(BlueService.class);
+ * + * Differentiating by names is a common enough use case that we provided a + * standard annotation, {@link com.google.inject.name.Named @Named}. Because of + * Guice's library support, binding by name is quite easier than in the + * arbitrary binding annotation case we just saw. However, remember that these + * names will live in a single flat namespace with all the other names used in + * your application. + * + *
+ *     Constructor loneCtor = getLoneCtorFromServiceImplViaReflection();
+ *     bind(ServiceImpl.class)
+ *         .toConstructor(loneCtor);
+ * + * In this example, we directly tell Guice which constructor to use in a concrete + * class implementation. It means that we do not need to place {@literal @}Inject + * on any of the constructors and that Guice treats the provided constructor as though + * it were annotated so. It is useful for cases where you cannot modify existing + * classes and is a bit simpler than using a {@link Provider}. + * + *

The above list of examples is far from exhaustive. If you can think of + * how the concepts of one example might coexist with the concepts from another, + * you can most likely weave the two together. If the two concepts make no + * sense with each other, you most likely won't be able to do it. In a few + * cases Guice will let something bogus slip by, and will then inform you of + * the problems at runtime, as soon as you try to create your Injector. + * + *

The other methods of Binder such as {@link #bindScope}, + * {@link #install}, {@link #requestStaticInjection}, + * {@link #addError} and {@link #currentStage} are not part of the Binding EDSL; + * you can learn how to use these in the usual way, from the method + * documentation. + */ +public interface Binder { + + /** + * Binds a scope to an annotation. + */ + void bindScope(Class annotationType, Scope scope); + + /** + * See the EDSL examples at {@link Binder}. + */ + LinkedBindingBuilder bind(Key key); + + /** + * See the EDSL examples at {@link Binder}. + */ + AnnotatedBindingBuilder bind(TypeLiteral typeLiteral); + + /** + * See the EDSL examples at {@link Binder}. + */ + AnnotatedBindingBuilder bind(Class type); + + /** + * See the EDSL examples at {@link Binder}. + */ + AnnotatedConstantBindingBuilder bindConstant(); + + /** + * Upon successful creation, the {@link Injector} will inject instance fields + * and methods of the given object. + * + * @param type of instance + * @param instance for which members will be injected + */ + void requestInjection(TypeLiteral type, T instance); + + /** + * Upon successful creation, the {@link Injector} will inject instance fields + * and methods of the given object. + * + * @param instance for which members will be injected + */ + void requestInjection(Object instance); + + /** + * Upon successful creation, the {@link Injector} will inject static fields + * and methods in the given classes. + * + * @param types for which static members will be injected + */ + void requestStaticInjection(Class... types); + + /** + * Uses the given module to configure more bindings. + */ + void install(Module module); + + /** + * Gets the current stage. + */ + Stage currentStage(); + + /** + * Records an error message which will be presented to the user at a later + * time. Unlike throwing an exception, this enable us to continue + * configuring the Injector and discover more errors. Uses {@link + * String#format(String, Object[])} to insert the arguments into the + * message. + */ + void addError(String message, Object... arguments); + + /** + * Records an exception, the full details of which will be logged, and the + * message of which will be presented to the user at a later + * time. If your Module calls something that you worry may fail, you should + * catch the exception and pass it into this. + */ + void addError(Throwable t); + + /** + * Records an error message to be presented to the user at a later time. + */ + void addError(Message message); + + /** + * Returns the provider used to obtain instances for the given injection key. + * The returned provider will not be valid until the {@link Injector} has been + * created. The provider will throw an {@code IllegalStateException} if you + * try to use it beforehand. + * + */ + Provider getProvider(Key key); + + /** + * Returns the provider used to obtain instances for the given injection key. + * The returned provider will be attached to the injection point and will + * follow the nullability specified in the dependency. + * Additionally, the returned provider will not be valid until the {@link Injector} + * has been created. The provider will throw an {@code IllegalStateException} if you + * try to use it beforehand. + */ + Provider getProvider(Dependency dependency); + + /** + * Returns the provider used to obtain instances for the given injection type. + * The returned provider will not be valid until the {@link Injector} has been + * created. The provider will throw an {@code IllegalStateException} if you + * try to use it beforehand. + */ + Provider getProvider(Class type); + + /** + * Returns the members injector used to inject dependencies into methods and fields on instances + * of the given type {@code T}. The returned members injector will not be valid until the main + * {@link Injector} has been created. The members injector will throw an {@code + * IllegalStateException} if you try to use it beforehand. + * + * @param typeLiteral type to get members injector for + */ + MembersInjector getMembersInjector(TypeLiteral typeLiteral); + + /** + * Returns the members injector used to inject dependencies into methods and fields on instances + * of the given type {@code T}. The returned members injector will not be valid until the main + * {@link Injector} has been created. The members injector will throw an {@code + * IllegalStateException} if you try to use it beforehand. + * + * @param type type to get members injector for + */ + MembersInjector getMembersInjector(Class type); + + /** + * Binds a type converter. The injector will use the given converter to + * convert string constants to matching types as needed. + * + * @param typeMatcher matches types the converter can handle + * @param converter converts values + */ + void convertToTypes(Matcher> typeMatcher, + TypeConverter converter); + + /** + * Registers a listener for injectable types. Guice will notify the listener when it encounters + * injectable types matched by the given type matcher. + * + * @param typeMatcher that matches injectable types the listener should be notified of + * @param listener for injectable types matched by typeMatcher + */ + void bindListener(Matcher> typeMatcher, + TypeListener listener); + + /** + * Registers listeners for provisioned objects. Guice will notify the + * listeners just before and after the object is provisioned. Provisioned + * objects that are also injectable (everything except objects provided + * through Providers) can also be notified through TypeListeners registered in + * {@link #bindListener}. + * + * @param bindingMatcher that matches bindings of provisioned objects the listener + * should be notified of + * @param listeners for provisioned objects matched by bindingMatcher + */ + void bindListener(Matcher> bindingMatcher, ProvisionListener... listeners); + + /** + * Returns a binder that uses {@code source} as the reference location for + * configuration errors. This is typically a {@link StackTraceElement} + * for {@code .java} source but it could any binding source, such as the + * path to a {@code .properties} file. + * + * @param source any object representing the source location and has a + * concise {@link Object#toString() toString()} value + * @return a binder that shares its configuration with this binder + */ + Binder withSource(Object source); + + /** + * Returns a binder that skips {@code classesToSkip} when identify the + * calling code. The caller's {@link StackTraceElement} is used to locate + * the source of configuration errors. + * + * @param classesToSkip library classes that create bindings on behalf of + * their clients. + * @return a binder that shares its configuration with this binder. + */ + Binder skipSources(Class... classesToSkip); + + /** + * Creates a new private child environment for bindings and other configuration. The returned + * binder can be used to add and configuration information in this environment. See {@link + * PrivateModule} for details. + * + * @return a binder that inherits configuration from this binder. Only exposed configuration on + * the returned binder will be visible to this binder. + */ + PrivateBinder newPrivateBinder(); + + /** + * Instructs the Injector that bindings must be listed in a Module in order to + * be injected. Classes that are not explicitly bound in a module cannot be + * injected. Bindings created through a linked binding + * (bind(Foo.class).to(FooImpl.class)) are allowed, but the + * implicit binding (FooImpl) cannot be directly injected unless + * it is also explicitly bound (bind(FooImpl.class)). + *

+ * Tools can still retrieve bindings for implicit bindings (bindings created + * through a linked binding) if explicit bindings are required, however + * {@link Binding#getProvider} will fail. + *

+ * By default, explicit bindings are not required. + *

+ * If a parent injector requires explicit bindings, then all child injectors + * (and private modules within that injector) also require explicit bindings. + * If a parent does not require explicit bindings, a child injector or private + * module may optionally declare itself as requiring explicit bindings. If it + * does, the behavior is limited only to that child or any grandchildren. No + * siblings of the child will require explicit bindings. + *

+ * In the absence of an explicit binding for the target, linked bindings in + * child injectors create a binding for the target in the parent. Since this + * behavior can be surprising, it causes an error instead if explicit bindings + * are required. To avoid this error, add an explicit binding for the target, + * either in the child or the parent. + */ + void requireExplicitBindings(); + + /** + * Prevents Guice from constructing a {@link Proxy} when a circular dependency + * is found. By default, circular proxies are not disabled. + *

+ * If a parent injector disables circular proxies, then all child injectors + * (and private modules within that injector) also disable circular proxies. + * If a parent does not disable circular proxies, a child injector or private + * module may optionally declare itself as disabling circular proxies. If it + * does, the behavior is limited only to that child or any grandchildren. No + * siblings of the child will disable circular proxies. + */ + void disableCircularProxies(); + + /** + * Requires that a {@literal @}{@link Inject} annotation exists on a constructor in order for + * Guice to consider it an eligible injectable class. By default, Guice will inject classes that + * have a no-args constructor if no {@literal @}{@link Inject} annotation exists on any + * constructor. + *

+ * If the class is bound using {@link LinkedBindingBuilder#toConstructor}, Guice will still inject + * that constructor regardless of annotations. + */ + void requireAtInjectOnConstructors(); + + /** + * Requires that Guice finds an exactly matching binding annotation. This disables the + * error-prone feature in Guice where it can substitute a binding for + * {@literal @}Named Foo when attempting to inject + * {@literal @}Named("foo") Foo. + */ + void requireExactBindingAnnotations(); + + /** + * Adds a scanner that will look in all installed modules for annotations the scanner can parse, + * and binds them like {@literal @}Provides methods. Scanners apply to all modules installed in + * the injector. Scanners installed in child injectors or private modules do not impact modules in + * siblings or parents, however scanners installed in parents do apply to all child injectors and + * private modules. + */ + void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner); +} diff --git a/src/main/java/com/google/inject/Binding.java b/src/main/java/com/google/inject/Binding.java new file mode 100644 index 0000000..8b5fd29 --- /dev/null +++ b/src/main/java/com/google/inject/Binding.java @@ -0,0 +1,71 @@ +package com.google.inject; + +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Element; + +/** + * A mapping from a key (type and optional annotation) to the strategy for getting instances of the + * type. This interface is part of the introspection API and is intended primarily for use by + * tools. + * + *

Bindings are created in several ways: + *

    + *
  • Explicitly in a module, via {@code bind()} and {@code bindConstant()} + * statements: + *
    + *     bind(Service.class).annotatedWith(Red.class).to(ServiceImpl.class);
    + *     bindConstant().annotatedWith(ServerHost.class).to(args[0]);
  • + *
  • Implicitly by the Injector by following a type's {@link ImplementedBy + * pointer} {@link ProvidedBy annotations} or by using its {@link Inject annotated} or + * default constructor.
  • + *
  • By converting a bound instance to a different type.
  • + *
  • For {@link Provider providers}, by delegating to the binding for the provided type.
  • + *
+ * + * + *

They exist on both modules and on injectors, and their behaviour is different for each: + *

    + *
  • Module bindings are incomplete and cannot be used to provide instances. + * This is because the applicable scopes and interceptors may not be known until an injector + * is created. From a tool's perspective, module bindings are like the injector's source + * code. They can be inspected or rewritten, but this analysis must be done statically.
  • + *
  • Injector bindings are complete and valid and can be used to provide + * instances. From a tools' perspective, injector bindings are like reflection for an + * injector. They have full runtime information, including the complete graph of injections + * necessary to satisfy a binding.
  • + *
+ * + * @param the bound type. The injected is always assignable to this type. + */ +public interface Binding extends Element { + + /** + * Returns the key for this binding. + */ + Key getKey(); + + /** + * Returns the scoped provider guice uses to fulfill requests for this + * binding. + * + * @throws UnsupportedOperationException when invoked on a {@link Binding} + * created via {@link com.google.inject.spi.Elements#getElements}. This + * method is only supported on {@link Binding}s returned from an injector. + */ + Provider getProvider(); + + /** + * Accepts a target visitor. Invokes the visitor method specific to this binding's target. + * + * @param visitor to call back on + */ + V acceptTargetVisitor(BindingTargetVisitor visitor); + + /** + * Accepts a scoping visitor. Invokes the visitor method specific to this binding's scoping. + * + * @param visitor to call back on + */ + V acceptScopingVisitor(BindingScopingVisitor visitor); +} diff --git a/src/main/java/com/google/inject/BindingAnnotation.java b/src/main/java/com/google/inject/BindingAnnotation.java new file mode 100644 index 0000000..8160d96 --- /dev/null +++ b/src/main/java/com/google/inject/BindingAnnotation.java @@ -0,0 +1,25 @@ +package com.google.inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates annotations which are used for binding. Only one such annotation + * may apply to a single injection point. You must also annotate binder + * annotations with {@code @Retention(RUNTIME)}. For example: + * + *
+ *   {@code @}Retention(RUNTIME)
+ *   {@code @}Target({ FIELD, PARAMETER, METHOD })
+ *   {@code @}BindingAnnotation
+ *   public {@code @}interface Transactional {}
+ * 
+ * + */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +public @interface BindingAnnotation { +} diff --git a/src/main/java/com/google/inject/ConfigurationException.java b/src/main/java/com/google/inject/ConfigurationException.java new file mode 100644 index 0000000..c5ce072 --- /dev/null +++ b/src/main/java/com/google/inject/ConfigurationException.java @@ -0,0 +1,64 @@ +package com.google.inject; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.internal.Errors; +import com.google.inject.spi.Message; + +import java.util.Collection; + +import static com.google.common.base.Preconditions.checkState; + +/** + * Thrown when a programming error such as a misplaced annotation, illegal binding, or unsupported + * scope is found. Clients should catch this exception, log it, and stop execution. + * + */ +@SuppressWarnings("serial") +public final class ConfigurationException extends RuntimeException { + + private final ImmutableSet messages; + private Object partialValue = null; + + /** + * Creates a ConfigurationException containing {@code messages}. + */ + public ConfigurationException(Iterable messages) { + this.messages = ImmutableSet.copyOf(messages); + initCause(Errors.getOnlyCause(this.messages)); + } + + /** + * Returns a copy of this configuration exception with the specified partial value. + */ + public ConfigurationException withPartialValue(Object partialValue) { + checkState(this.partialValue == null, + "Can't clobber existing partial value %s with %s", this.partialValue, partialValue); + ConfigurationException result = new ConfigurationException(messages); + result.partialValue = partialValue; + return result; + } + + /** + * Returns messages for the errors that caused this exception. + */ + public Collection getErrorMessages() { + return messages; + } + + /** + * Returns a value that was only partially computed due to this exception. The caller can use + * this while collecting additional configuration problems. + * + * @return the partial value, or {@code null} if none was set. The type of the partial value is + * specified by the throwing method. + */ + @SuppressWarnings("unchecked") // this is *extremely* unsafe. We trust the caller here. + public E getPartialValue() { + return (E) partialValue; + } + + @Override + public String getMessage() { + return Errors.format("Guice configuration errors", messages); + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/CreationException.java b/src/main/java/com/google/inject/CreationException.java new file mode 100644 index 0000000..d764818 --- /dev/null +++ b/src/main/java/com/google/inject/CreationException.java @@ -0,0 +1,40 @@ +package com.google.inject; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.internal.Errors; +import com.google.inject.spi.Message; + +import java.util.Collection; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Thrown when errors occur while creating a {@link Injector}. Includes a list of encountered + * errors. Clients should catch this exception, log it, and stop execution. + */ +public class CreationException extends RuntimeException { + + private static final long serialVersionUID = 0; + private final ImmutableSet messages; + + /** + * Creates a CreationException containing {@code messages}. + */ + public CreationException(Collection messages) { + this.messages = ImmutableSet.copyOf(messages); + checkArgument(!this.messages.isEmpty()); + initCause(Errors.getOnlyCause(this.messages)); + } + + /** + * Returns messages for the errors that caused this exception. + */ + public Collection getErrorMessages() { + return messages; + } + + @Override + public String getMessage() { + return Errors.format("Unable to create injector, see the following errors", messages); + } +} diff --git a/src/main/java/com/google/inject/Exposed.java b/src/main/java/com/google/inject/Exposed.java new file mode 100644 index 0000000..5292e24 --- /dev/null +++ b/src/main/java/com/google/inject/Exposed.java @@ -0,0 +1,19 @@ +package com.google.inject; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Acccompanies a {@literal @}{@link com.google.inject.Provides Provides} method annotation in a + * private module to indicate that the provided binding is exposed. + * + */ +@Target(ElementType.METHOD) +@Retention(RUNTIME) +@Documented +public @interface Exposed { +} diff --git a/src/main/java/com/google/inject/Guice.java b/src/main/java/com/google/inject/Guice.java new file mode 100644 index 0000000..c8c0259 --- /dev/null +++ b/src/main/java/com/google/inject/Guice.java @@ -0,0 +1,86 @@ +package com.google.inject; + +import com.google.inject.internal.InternalInjectorCreator; + +import java.util.Arrays; + +/** + * The entry point to the Guice framework. Creates {@link Injector}s from + * {@link Module}s. + * + *

Guice supports a model of development that draws clear boundaries between + * APIs, Implementations of these APIs, Modules which configure these + * implementations, and finally Applications which consist of a collection of + * Modules. It is the Application, which typically defines your {@code main()} + * method, that bootstraps the Guice Injector using the {@code Guice} class, as + * in this example: + *

+ *     public class FooApplication {
+ *       public static void main(String[] args) {
+ *         Injector injector = Guice.createInjector(
+ *             new ModuleA(),
+ *             new ModuleB(),
+ *             . . .
+ *             new FooApplicationFlagsModule(args)
+ *         );
+ *
+ *         // Now just bootstrap the application and you're done
+ *         FooStarter starter = injector.getInstance(FooStarter.class);
+ *         starter.runApplication();
+ *       }
+ *     }
+ * 
+ */ +public final class Guice { + + private Guice() { + } + + /** + * Creates an injector for the given set of modules. This is equivalent to + * calling {@link #createInjector(Stage, Module...)} with Stage.DEVELOPMENT. + * + * @throws CreationException if one or more errors occur during injector + * construction + */ + public static Injector createInjector(Module... modules) { + return createInjector(Arrays.asList(modules)); + } + + /** + * Creates an injector for the given set of modules. This is equivalent to + * calling {@link #createInjector(Stage, Iterable)} with Stage.DEVELOPMENT. + * + * @throws CreationException if one or more errors occur during injector + * creation + */ + public static Injector createInjector(Iterable modules) { + return createInjector(Stage.DEVELOPMENT, modules); + } + + /** + * Creates an injector for the given set of modules, in a given development + * stage. + * + * @throws CreationException if one or more errors occur during injector + * creation. + */ + public static Injector createInjector(Stage stage, Module... modules) { + return createInjector(stage, Arrays.asList(modules)); + } + + /** + * Creates an injector for the given set of modules, in a given development + * stage. + * + * @throws CreationException if one or more errors occur during injector + * construction + */ + public static Injector createInjector(Stage stage, + Iterable modules) { + return new InternalInjectorCreator() + .stage(stage) + .addModules(modules) + .build(); + } +} diff --git a/src/main/java/com/google/inject/ImplementedBy.java b/src/main/java/com/google/inject/ImplementedBy.java new file mode 100644 index 0000000..be636f4 --- /dev/null +++ b/src/main/java/com/google/inject/ImplementedBy.java @@ -0,0 +1,20 @@ +package com.google.inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * A pointer to the default implementation of a type. + */ +@Retention(RUNTIME) +@Target(TYPE) +public @interface ImplementedBy { + + /** + * The implementation type. + */ + Class value(); +} diff --git a/src/main/java/com/google/inject/Inject.java b/src/main/java/com/google/inject/Inject.java new file mode 100644 index 0000000..ff71d06 --- /dev/null +++ b/src/main/java/com/google/inject/Inject.java @@ -0,0 +1,51 @@ +package com.google.inject; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates members of your implementation class (constructors, methods + * and fields) into which the {@link Injector} should inject values. + * The Injector fulfills injection requests for: + * + *
    + *
  • Every instance it constructs. The class being constructed must have + * exactly one of its constructors marked with {@code @Inject} or must have a + * constructor taking no parameters. The Injector then proceeds to perform + * field and method injections. + * + *
  • Pre-constructed instances passed to {@link Injector#injectMembers}, + * {@link com.google.inject.binder.LinkedBindingBuilder#toInstance(Object)} and + * {@link com.google.inject.binder.LinkedBindingBuilder#toProvider(javax.inject.Provider)}. + * In this case all constructors are, of course, ignored. + * + *
  • Static fields and methods of classes which any {@link Module} has + * specifically requested static injection for, using + * {@link Binder#requestStaticInjection}. + *
+ * + * In all cases, a member can be injected regardless of its Java access + * specifier (private, default, protected, public). + */ +@Target({METHOD, CONSTRUCTOR, FIELD}) +@Retention(RUNTIME) +@Documented +public @interface Inject { + + /** + * If true, and the appropriate binding is not found, + * the Injector will skip injection of this method or field rather than + * produce an error. When applied to a field, any default value already + * assigned to the field will remain (guice will not actively null out the + * field). When applied to a method, the method will only be invoked if + * bindings for all parameters are found. When applied to a + * constructor, an error will result upon Injector creation. + */ + boolean optional() default false; +} diff --git a/src/main/java/com/google/inject/Injector.java b/src/main/java/com/google/inject/Injector.java new file mode 100644 index 0000000..b9df905 --- /dev/null +++ b/src/main/java/com/google/inject/Injector.java @@ -0,0 +1,228 @@ +package com.google.inject; + +import com.google.inject.spi.TypeConverterBinding; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Builds the graphs of objects that make up your application. The injector tracks the dependencies + * for each type and uses bindings to inject them. This is the core of Guice, although you rarely + * interact with it directly. This "behind-the-scenes" operation is what distinguishes dependency + * injection from its cousin, the service locator pattern. + * + *

Contains several default bindings: + * + *

    + *
  • This {@link Injector} instance itself + *
  • A {@code Provider} for each binding of type {@code T} + *
  • The {@link java.util.logging.Logger} for the class being injected + *
  • The {@link Stage} in which the Injector was created + *
+ * + * Injectors are created using the facade class {@link Guice}. + * + *

An injector can also {@link #injectMembers(Object) inject the dependencies} of + * already-constructed instances. This can be used to interoperate with objects created by other + * frameworks or services. + * + *

Injectors can be {@link #createChildInjector(Iterable) hierarchical}. Child injectors inherit + * the configuration of their parent injectors, but the converse does not hold. + * + *

The injector's {@link #getBindings() internal bindings} are available for introspection. This + * enables tools and extensions to operate on an injector reflectively. + */ +public interface Injector { + + /** + * Injects dependencies into the fields and methods of {@code instance}. Ignores the presence or + * absence of an injectable constructor. + * + *

Whenever Guice creates an instance, it performs this injection automatically (after first + * performing constructor injection), so if you're able to let Guice create all your objects for + * you, you'll never need to use this method. + * + * @param instance to inject members on + * @see Binder#getMembersInjector(Class) for a preferred alternative that supports checks before + * run time + */ + void injectMembers(Object instance); + + /** + * Returns the members injector used to inject dependencies into methods and fields on instances + * of the given type {@code T}. + * + * @param typeLiteral type to get members injector for + * @see Binder#getMembersInjector(TypeLiteral) for an alternative that offers up front error + * detection + */ + MembersInjector getMembersInjector(TypeLiteral typeLiteral); + + /** + * Returns the members injector used to inject dependencies into methods and fields on instances + * of the given type {@code T}. When feasible, use {@link Binder#getMembersInjector(TypeLiteral)} + * instead to get increased up front error detection. + * + * @param type type to get members injector for + * @see Binder#getMembersInjector(Class) for an alternative that offers up front error + * detection + */ + MembersInjector getMembersInjector(Class type); + + /** + * Returns this injector's explicit bindings. + * + *

The returned map does not include bindings inherited from a {@link #getParent() parent + * injector}, should one exist. The returned map is guaranteed to iterate (for example, with + * its {@link Map#entrySet()} iterator) in the order of insertion. In other words, the order in + * which bindings appear in user Modules. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + Map, Binding> getBindings(); + + /** + * Returns a snapshot of this injector's bindings, both explicit and + * just-in-time. The returned map is immutable; it contains only the bindings that were + * present when {@code getAllBindings()} was invoked. Subsequent calls may return a map with + * additional just-in-time bindings. + * + *

The returned map does not include bindings inherited from a {@link #getParent() parent + * injector}, should one exist. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + Map, Binding> getAllBindings(); + + /** + * Returns the binding for the given injection key. This will be an explicit bindings if the key + * was bound explicitly by a module, or an implicit binding otherwise. The implicit binding will + * be created if necessary. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + * + * @throws ConfigurationException if this injector cannot find or create the binding. + */ + Binding getBinding(Key key); + + /** + * Returns the binding for the given type. This will be an explicit bindings if the injection key + * was bound explicitly by a module, or an implicit binding otherwise. The implicit binding will + * be created if necessary. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + * + * @throws ConfigurationException if this injector cannot find or create the binding. + */ + Binding getBinding(Class type); + + /** + * Returns the binding if it already exists, or null if does not exist. Unlike + * {@link #getBinding(Key)}, this does not attempt to create just-in-time bindings + * for keys that aren't bound. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + Binding getExistingBinding(Key key); + + /** + * Returns all explicit bindings for {@code type}. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + List> findBindingsByType(TypeLiteral type); + + /** + * Returns the provider used to obtain instances for the given injection key. When feasible, avoid + * using this method, in favor of having Guice inject your dependencies ahead of time. + * + * @throws ConfigurationException if this injector cannot find or create the provider. + * @see Binder#getProvider(Key) for an alternative that offers up front error detection + */ + Provider getProvider(Key key); + + /** + * Returns the provider used to obtain instances for the given type. When feasible, avoid + * using this method, in favor of having Guice inject your dependencies ahead of time. + * + * @throws ConfigurationException if this injector cannot find or create the provider. + * @see Binder#getProvider(Class) for an alternative that offers up front error detection + */ + Provider getProvider(Class type); + + /** + * Returns the appropriate instance for the given injection key; equivalent to {@code + * getProvider(key).get()}. When feasible, avoid using this method, in favor of having Guice + * inject your dependencies ahead of time. + * + * @throws ConfigurationException if this injector cannot find or create the provider. + * @throws ProvisionException if there was a runtime failure while providing an instance. + */ + T getInstance(Key key); + + /** + * Returns the appropriate instance for the given injection type; equivalent to {@code + * getProvider(type).get()}. When feasible, avoid using this method, in favor of having Guice + * inject your dependencies ahead of time. + * + * @throws ConfigurationException if this injector cannot find or create the provider. + * @throws ProvisionException if there was a runtime failure while providing an instance. + */ + T getInstance(Class type); + + /** + * Returns this injector's parent, or {@code null} if this is a top-level injector. + */ + Injector getParent(); + + /** + * Returns a new injector that inherits all state from this injector. All bindings, scopes, + * interceptors and type converters are inherited -- they are visible to the child injector. + * Elements of the child injector are not visible to its parent. + * + *

Just-in-time bindings created for child injectors will be created in an ancestor injector + * whenever possible. This allows for scoped instances to be shared between injectors. Use + * explicit bindings to prevent bindings from being shared with the parent injector. Optional + * injections in just-in-time bindings (created in the parent injector) may be silently + * ignored if the optional dependencies are from the child injector. + * + *

No key may be bound by both an injector and one of its ancestors. This includes just-in-time + * bindings. The lone exception is the key for {@code Injector.class}, which is bound by each + * injector to itself. + */ + Injector createChildInjector(Iterable modules); + + /** + * Returns a new injector that inherits all state from this injector. All bindings, scopes, + * interceptors and type converters are inherited -- they are visible to the child injector. + * Elements of the child injector are not visible to its parent. + * + *

Just-in-time bindings created for child injectors will be created in an ancestor injector + * whenever possible. This allows for scoped instances to be shared between injectors. Use + * explicit bindings to prevent bindings from being shared with the parent injector. + * + *

No key may be bound by both an injector and one of its ancestors. This includes just-in-time + * bindings. The lone exception is the key for {@code Injector.class}, which is bound by each + * injector to itself. + */ + Injector createChildInjector(Module... modules); + + /** + * Returns a map containing all scopes in the injector. The maps keys are scoping annotations + * like {@code Singleton.class}, and the values are scope instances, such as {@code + * Scopes.SINGLETON}. The returned map is immutable. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + Map, Scope> getScopeBindings(); + + /** + * Returns a set containing all type converter bindings in the injector. The returned set is + * immutable. + * + *

This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + Set getTypeConverterBindings(); +} diff --git a/src/main/java/com/google/inject/Key.java b/src/main/java/com/google/inject/Key.java new file mode 100644 index 0000000..6b34a11 --- /dev/null +++ b/src/main/java/com/google/inject/Key.java @@ -0,0 +1,508 @@ +package com.google.inject; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.inject.internal.Annotations; +import com.google.inject.internal.MoreTypes; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.inject.internal.Annotations.generateAnnotation; +import static com.google.inject.internal.Annotations.isAllDefaultMethods; + +/** + * Binding key consisting of an injection type and an optional annotation. + * Matches the type and annotation at a point of injection. + * + *

For example, {@code Key.get(Service.class, Transactional.class)} will + * match: + * + *

+ *   {@literal @}Inject
+ *   public void setService({@literal @}Transactional Service service) {
+ *     ...
+ *   }
+ * 
+ * + *

{@code Key} supports generic types via subclassing just like {@link + * TypeLiteral}. + * + *

Keys do not differentiate between primitive types (int, char, etc.) and + * their corresponding wrapper types (Integer, Character, etc.). Primitive + * types will be replaced with their wrapper types when keys are created. + */ +public class Key { + + private final AnnotationStrategy annotationStrategy; + + private final TypeLiteral typeLiteral; + private final int hashCode; + private final Supplier toStringSupplier; + + /** + * Constructs a new key. Derives the type from this class's type parameter. + * + *

Clients create an empty anonymous subclass. Doing so embeds the type + * parameter in the anonymous class's type hierarchy so we can reconstitute it + * at runtime despite erasure. + * + *

Example usage for a binding of type {@code Foo} annotated with + * {@code @Bar}: + * + *

{@code new Key(Bar.class) {}}. + */ + @SuppressWarnings("unchecked") + protected Key(Class annotationType) { + this.annotationStrategy = strategyFor(annotationType); + this.typeLiteral = MoreTypes.canonicalizeForKey( + (TypeLiteral) TypeLiteral.fromSuperclassTypeParameter(getClass())); + this.hashCode = computeHashCode(); + this.toStringSupplier = createToStringSupplier(); + } + + /** + * Constructs a new key. Derives the type from this class's type parameter. + * + *

Clients create an empty anonymous subclass. Doing so embeds the type + * parameter in the anonymous class's type hierarchy so we can reconstitute it + * at runtime despite erasure. + * + *

Example usage for a binding of type {@code Foo} annotated with + * {@code @Bar}: + * + *

{@code new Key(new Bar()) {}}. + */ + @SuppressWarnings("unchecked") + protected Key(Annotation annotation) { + // no usages, not test-covered + this.annotationStrategy = strategyFor(annotation); + this.typeLiteral = MoreTypes.canonicalizeForKey( + (TypeLiteral) TypeLiteral.fromSuperclassTypeParameter(getClass())); + this.hashCode = computeHashCode(); + this.toStringSupplier = createToStringSupplier(); + } + + /** + * Constructs a new key. Derives the type from this class's type parameter. + * + *

Clients create an empty anonymous subclass. Doing so embeds the type + * parameter in the anonymous class's type hierarchy so we can reconstitute it + * at runtime despite erasure. + * + *

Example usage for a binding of type {@code Foo}: + * + *

{@code new Key() {}}. + */ + @SuppressWarnings("unchecked") + protected Key() { + this.annotationStrategy = NullAnnotationStrategy.INSTANCE; + this.typeLiteral = MoreTypes.canonicalizeForKey( + (TypeLiteral) TypeLiteral.fromSuperclassTypeParameter(getClass())); + this.hashCode = computeHashCode(); + this.toStringSupplier = createToStringSupplier(); + } + + /** + * Unsafe. Constructs a key from a manually specified type. + */ + @SuppressWarnings("unchecked") + private Key(Type type, AnnotationStrategy annotationStrategy) { + this.annotationStrategy = annotationStrategy; + this.typeLiteral = MoreTypes.canonicalizeForKey((TypeLiteral) TypeLiteral.get(type)); + this.hashCode = computeHashCode(); + this.toStringSupplier = createToStringSupplier(); + } + + /** + * Constructs a key from a manually specified type. + */ + private Key(TypeLiteral typeLiteral, AnnotationStrategy annotationStrategy) { + this.annotationStrategy = annotationStrategy; + this.typeLiteral = MoreTypes.canonicalizeForKey(typeLiteral); + this.hashCode = computeHashCode(); + this.toStringSupplier = createToStringSupplier(); + } + + /** + * Gets a key for an injection type and an annotation strategy. + */ + static Key get(Class type, + AnnotationStrategy annotationStrategy) { + return new Key(type, annotationStrategy); + } + + /** + * Gets a key for an injection type. + */ + public static Key get(Class type) { + return new Key(type, NullAnnotationStrategy.INSTANCE); + } + + /** + * Gets a key for an injection type and an annotation type. + */ + public static Key get(Class type, + Class annotationType) { + return new Key(type, strategyFor(annotationType)); + } + + /** + * Gets a key for an injection type and an annotation. + */ + public static Key get(Class type, Annotation annotation) { + return new Key(type, strategyFor(annotation)); + } + + /** + * Gets a key for an injection type. + */ + public static Key get(Type type) { + return new Key(type, NullAnnotationStrategy.INSTANCE); + } + + /** + * Gets a key for an injection type and an annotation type. + */ + public static Key get(Type type, + Class annotationType) { + return new Key(type, strategyFor(annotationType)); + } + + /** + * Gets a key for an injection type and an annotation. + */ + public static Key get(Type type, Annotation annotation) { + return new Key(type, strategyFor(annotation)); + } + + /** + * Gets a key for an injection type. + */ + public static Key get(TypeLiteral typeLiteral) { + return new Key(typeLiteral, NullAnnotationStrategy.INSTANCE); + } + + /** + * Gets a key for an injection type and an annotation type. + */ + public static Key get(TypeLiteral typeLiteral, + Class annotationType) { + return new Key(typeLiteral, strategyFor(annotationType)); + } + + /** + * Gets a key for an injection type and an annotation. + */ + public static Key get(TypeLiteral typeLiteral, + Annotation annotation) { + return new Key(typeLiteral, strategyFor(annotation)); + } + + /** + * Gets the strategy for an annotation. + */ + static AnnotationStrategy strategyFor(Annotation annotation) { + checkNotNull(annotation, "annotation"); + Class annotationType = annotation.annotationType(); + ensureRetainedAtRuntime(annotationType); + ensureIsBindingAnnotation(annotationType); + + if (Annotations.isMarker(annotationType)) { + return new AnnotationTypeStrategy(annotationType, annotation); + } + + return new AnnotationInstanceStrategy(Annotations.canonicalizeIfNamed(annotation)); + } + + /** + * Gets the strategy for an annotation type. + */ + static AnnotationStrategy strategyFor(Class annotationType) { + annotationType = Annotations.canonicalizeIfNamed(annotationType); + if (isAllDefaultMethods(annotationType)) { + return strategyFor(generateAnnotation(annotationType)); + } + + checkNotNull(annotationType, "annotation type"); + ensureRetainedAtRuntime(annotationType); + ensureIsBindingAnnotation(annotationType); + return new AnnotationTypeStrategy(annotationType, null); + + } + + private static void ensureRetainedAtRuntime( + Class annotationType) { + checkArgument(Annotations.isRetainedAtRuntime(annotationType), + "%s is not retained at runtime. Please annotate it with @Retention(RUNTIME).", + annotationType.getName()); + } + + private static void ensureIsBindingAnnotation(Class annotationType) { + checkArgument(Annotations.isBindingAnnotation(annotationType), + "%s is not a binding annotation. Please annotate it with @BindingAnnotation.", + annotationType.getName()); + } + + /** + * Computes the hash code for this key. + */ + private int computeHashCode() { + return typeLiteral.hashCode() * 31 + annotationStrategy.hashCode(); + } + + /** + * @return a {@link Supplier} which memoizes the value for lazy initialization. + */ + private Supplier createToStringSupplier() { + // The performance hit on access is acceptable since the intended use is for non-performance- + // critical applications. + return Suppliers.memoize(new Supplier() { + @Override + public String get() { + return "Key[type=" + typeLiteral + ", annotation=" + annotationStrategy + "]"; + } + }); + } + + /** + * Gets the key type. + */ + public final TypeLiteral getTypeLiteral() { + return typeLiteral; + } + + /** + * Gets the annotation type. + */ + public final Class getAnnotationType() { + return annotationStrategy.getAnnotationType(); + } + + /** + * Gets the annotation. + */ + public final Annotation getAnnotation() { + return annotationStrategy.getAnnotation(); + } + + boolean hasAnnotationType() { + return annotationStrategy.getAnnotationType() != null; + } + + String getAnnotationName() { + Annotation annotation = annotationStrategy.getAnnotation(); + if (annotation != null) { + return annotation.toString(); + } + + // not test-covered + return annotationStrategy.getAnnotationType().toString(); + } + + Class getRawType() { + return typeLiteral.getRawType(); + } + + /** + * Gets the key of this key's provider. + */ + Key> providerKey() { + return ofType(typeLiteral.providerType()); + } + + @Override + public final boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Key)) { + return false; + } + Key other = (Key) o; + return annotationStrategy.equals(other.annotationStrategy) + && typeLiteral.equals(other.typeLiteral); + } + + @Override + public final int hashCode() { + return this.hashCode; + } + + @Override + public final String toString() { + return toStringSupplier.get(); + } + + /** + * Returns a new key of the specified type with the same annotation as this + * key. + */ + public Key ofType(Class type) { + return new Key(type, annotationStrategy); + } + + /** + * Returns a new key of the specified type with the same annotation as this + * key. + */ + public Key ofType(Type type) { + return new Key(type, annotationStrategy); + } + + /** + * Returns a new key of the specified type with the same annotation as this + * key. + */ + public Key ofType(TypeLiteral type) { + return new Key(type, annotationStrategy); + } + + /** + * Returns true if this key has annotation attributes. + */ + public boolean hasAttributes() { + return annotationStrategy.hasAttributes(); + } + + /** + * Returns this key without annotation attributes, i.e. with only the + * annotation type. + */ + public Key withoutAttributes() { + return new Key(typeLiteral, annotationStrategy.withoutAttributes()); + } + + static enum NullAnnotationStrategy implements AnnotationStrategy { + INSTANCE; + + public boolean hasAttributes() { + return false; + } + + public AnnotationStrategy withoutAttributes() { + throw new UnsupportedOperationException("Key already has no attributes."); + } + + public Annotation getAnnotation() { + return null; + } + + public Class getAnnotationType() { + return null; + } + + @Override + public String toString() { + return "[none]"; + } + } + + interface AnnotationStrategy { + Annotation getAnnotation(); + + Class getAnnotationType(); + + boolean hasAttributes(); + + AnnotationStrategy withoutAttributes(); + } + + // this class not test-covered + static class AnnotationInstanceStrategy implements AnnotationStrategy { + + final Annotation annotation; + + AnnotationInstanceStrategy(Annotation annotation) { + this.annotation = checkNotNull(annotation, "annotation"); + } + + public boolean hasAttributes() { + return true; + } + + public AnnotationStrategy withoutAttributes() { + return new AnnotationTypeStrategy(getAnnotationType(), annotation); + } + + public Annotation getAnnotation() { + return annotation; + } + + public Class getAnnotationType() { + return annotation.annotationType(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AnnotationInstanceStrategy)) { + return false; + } + + AnnotationInstanceStrategy other = (AnnotationInstanceStrategy) o; + return annotation.equals(other.annotation); + } + + @Override + public int hashCode() { + return annotation.hashCode(); + } + + @Override + public String toString() { + return annotation.toString(); + } + } + + static class AnnotationTypeStrategy implements AnnotationStrategy { + + final Class annotationType; + + // Keep the instance around if we have it so the client can request it. + final Annotation annotation; + + AnnotationTypeStrategy(Class annotationType, + Annotation annotation) { + this.annotationType = checkNotNull(annotationType, "annotation type"); + this.annotation = annotation; + } + + public boolean hasAttributes() { + return false; + } + + public AnnotationStrategy withoutAttributes() { + throw new UnsupportedOperationException("Key already has no attributes."); + } + + public Annotation getAnnotation() { + return annotation; + } + + public Class getAnnotationType() { + return annotationType; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AnnotationTypeStrategy)) { + return false; + } + + AnnotationTypeStrategy other = (AnnotationTypeStrategy) o; + return annotationType.equals(other.annotationType); + } + + @Override + public int hashCode() { + return annotationType.hashCode(); + } + + @Override + public String toString() { + return "@" + annotationType.getName(); + } + } +} diff --git a/src/main/java/com/google/inject/MembersInjector.java b/src/main/java/com/google/inject/MembersInjector.java new file mode 100644 index 0000000..f9fa7b9 --- /dev/null +++ b/src/main/java/com/google/inject/MembersInjector.java @@ -0,0 +1,22 @@ +package com.google.inject; + +/** + * Injects dependencies into the fields and methods on instances of type {@code T}. Ignores the + * presence or absence of an injectable constructor. + * + * @param type to inject members of + */ +public interface MembersInjector { + + /** + * Injects dependencies into the fields and methods of {@code instance}. Ignores the presence or + * absence of an injectable constructor. + * + *

Whenever Guice creates an instance, it performs this injection automatically (after first + * performing constructor injection), so if you're able to let Guice create all your objects for + * you, you'll never need to use this method. + * + * @param instance to inject members on. May be {@code null}. + */ + void injectMembers(T instance); +} diff --git a/src/main/java/com/google/inject/Module.java b/src/main/java/com/google/inject/Module.java new file mode 100644 index 0000000..5a9634a --- /dev/null +++ b/src/main/java/com/google/inject/Module.java @@ -0,0 +1,27 @@ +package com.google.inject; + +/** + * A module contributes configuration information, typically interface + * bindings, which will be used to create an {@link Injector}. A Guice-based + * application is ultimately composed of little more than a set of + * {@code Module}s and some bootstrapping code. + * + *

Your Module classes can use a more streamlined syntax by extending + * {@link AbstractModule} rather than implementing this interface directly. + * + *

In addition to the bindings configured via {@link #configure}, bindings + * will be created for all methods annotated with {@literal @}{@link Provides}. + * Use scope and binding annotations on these methods to configure the + * bindings. + */ +public interface Module { + + /** + * Contributes bindings and other configurations for this module to {@code binder}. + * + *

Do not invoke this method directly to install submodules. Instead use + * {@link Binder#install(Module)}, which ensures that {@link Provides provider methods} are + * discovered. + */ + void configure(Binder binder); +} diff --git a/src/main/java/com/google/inject/PrivateBinder.java b/src/main/java/com/google/inject/PrivateBinder.java new file mode 100644 index 0000000..cddf1a1 --- /dev/null +++ b/src/main/java/com/google/inject/PrivateBinder.java @@ -0,0 +1,35 @@ +package com.google.inject; + +import com.google.inject.binder.AnnotatedElementBuilder; + +/** + * Returns a binder whose configuration information is hidden from its environment by default. See + * {@link com.google.inject.PrivateModule PrivateModule} for details. + * + */ +public interface PrivateBinder extends Binder { + + /** + * Makes the binding for {@code key} available to the enclosing environment + */ + void expose(Key key); + + /** + * Makes a binding for {@code type} available to the enclosing environment. Use {@link + * com.google.inject.binder.AnnotatedElementBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} + * with a + * binding annotation. + */ + AnnotatedElementBuilder expose(Class type); + + /** + * Makes a binding for {@code type} available to the enclosing environment. Use {@link + * AnnotatedElementBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a + * binding annotation. + */ + AnnotatedElementBuilder expose(TypeLiteral type); + + PrivateBinder withSource(Object source); + + PrivateBinder skipSources(Class... classesToSkip); +} diff --git a/src/main/java/com/google/inject/PrivateModule.java b/src/main/java/com/google/inject/PrivateModule.java new file mode 100644 index 0000000..f993439 --- /dev/null +++ b/src/main/java/com/google/inject/PrivateModule.java @@ -0,0 +1,279 @@ +package com.google.inject; + +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.AnnotatedConstantBindingBuilder; +import com.google.inject.binder.AnnotatedElementBuilder; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.matcher.Matcher; +import com.google.inject.spi.Message; +import com.google.inject.spi.ProvisionListener; +import com.google.inject.spi.TypeConverter; +import com.google.inject.spi.TypeListener; + +import java.lang.annotation.Annotation; + +import static com.google.common.base.Preconditions.checkState; + +/** + * A module whose configuration information is hidden from its environment by default. Only bindings + * that are explicitly exposed will be available to other modules and to the users of the injector. + * This module may expose the bindings it creates and the bindings of the modules it installs. + * + *

A private module can be nested within a regular module or within another private module using + * {@link Binder#install install()}. Its bindings live in a new environment that inherits bindings, + * type converters, scopes, and interceptors from the surrounding ("parent") environment. When you + * nest multiple private modules, the result is a tree of environments where the injector's + * environment is the root. + * + *

Guice EDSL bindings can be exposed with {@link #expose(Class) expose()}. {@literal @}{@link + * com.google.inject.Provides Provides} bindings can be exposed with the {@literal @}{@link + * Exposed} annotation: + * + *

+ * public class FooBarBazModule extends PrivateModule {
+ *   protected void configure() {
+ *     bind(Foo.class).to(RealFoo.class);
+ *     expose(Foo.class);
+ *
+ *     install(new TransactionalBarModule());
+ *     expose(Bar.class).annotatedWith(Transactional.class);
+ *
+ *     bind(SomeImplementationDetail.class);
+ *     install(new MoreImplementationDetailsModule());
+ *   }
+ *
+ *   {@literal @}Provides {@literal @}Exposed
+ *   public Baz provideBaz() {
+ *     return new SuperBaz();
+ *   }
+ * }
+ * 
+ * + *

Private modules are implemented using {@link Injector#createChildInjector(Module[]) parent + * injectors}. When it can satisfy their dependencies, just-in-time bindings will be created in the + * root environment. Such bindings are shared among all environments in the tree. + * + *

The scope of a binding is constrained to its environment. A singleton bound in a private + * module will be unique to its environment. But a binding for the same type in a different private + * module will yield a different instance. + * + *

A shared binding that injects the {@code Injector} gets the root injector, which only has + * access to bindings in the root environment. An explicit binding that injects the {@code Injector} + * gets access to all bindings in the child environment. + * + *

To promote a just-in-time binding to an explicit binding, bind it: + *

+ *   bind(FooImpl.class);
+ * 
+ * + */ +public abstract class PrivateModule implements Module { + + /** + * Like abstract module, the binder of the current private module + */ + private PrivateBinder binder; + + public final synchronized void configure(Binder binder) { + checkState(this.binder == null, "Re-entry is not allowed."); + + // Guice treats PrivateModules specially and passes in a PrivateBinder automatically. + this.binder = (PrivateBinder) binder.skipSources(PrivateModule.class); + try { + configure(); + } finally { + this.binder = null; + } + } + + /** + * Creates bindings and other configurations private to this module. Use {@link #expose(Class) + * expose()} to make the bindings in this module available externally. + */ + protected abstract void configure(); + + /** + * Makes the binding for {@code key} available to other modules and the injector. + */ + protected final void expose(Key key) { + binder().expose(key); + } + + /** + * Makes a binding for {@code type} available to other modules and the injector. Use {@link + * AnnotatedElementBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a + * binding annotation. + */ + protected final AnnotatedElementBuilder expose(Class type) { + return binder().expose(type); + } + + /** + * Makes a binding for {@code type} available to other modules and the injector. Use {@link + * AnnotatedElementBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a + * binding annotation. + */ + protected final AnnotatedElementBuilder expose(TypeLiteral type) { + return binder().expose(type); + } + + // everything below is copied from AbstractModule + + /** + * Returns the current binder. + */ + protected final PrivateBinder binder() { + checkState(binder != null, "The binder can only be used inside configure()"); + return binder; + } + + /** + * @see Binder#bindScope(Class, Scope) + */ + protected final void bindScope(Class scopeAnnotation, Scope scope) { + binder().bindScope(scopeAnnotation, scope); + } + + /** + * @see Binder#bind(Key) + */ + protected final LinkedBindingBuilder bind(Key key) { + return binder().bind(key); + } + + /** + * @see Binder#bind(TypeLiteral) + */ + protected final AnnotatedBindingBuilder bind(TypeLiteral typeLiteral) { + return binder().bind(typeLiteral); + } + + /** + * @see Binder#bind(Class) + */ + protected final AnnotatedBindingBuilder bind(Class clazz) { + return binder().bind(clazz); + } + + /** + * @see Binder#bindConstant() + */ + protected final AnnotatedConstantBindingBuilder bindConstant() { + return binder().bindConstant(); + } + + /** + * @see Binder#install(Module) + */ + protected final void install(Module module) { + binder().install(module); + } + + /** + * @see Binder#addError(String, Object[]) + */ + protected final void addError(String message, Object... arguments) { + binder().addError(message, arguments); + } + + /** + * @see Binder#addError(Throwable) + */ + protected final void addError(Throwable t) { + binder().addError(t); + } + + /** + * @see Binder#addError(Message) + */ + protected final void addError(Message message) { + binder().addError(message); + } + + /** + * @see Binder#requestInjection(Object) + */ + protected final void requestInjection(Object instance) { + binder().requestInjection(instance); + } + + /** + * @see Binder#requestStaticInjection(Class[]) + */ + protected final void requestStaticInjection(Class... types) { + binder().requestStaticInjection(types); + } + + /** + * Instructs Guice to require a binding to the given key. + */ + protected final void requireBinding(Key key) { + binder().getProvider(key); + } + + /** + * Instructs Guice to require a binding to the given type. + */ + protected final void requireBinding(Class type) { + binder().getProvider(type); + } + + /** + * @see Binder#getProvider(Key) + */ + protected final Provider getProvider(Key key) { + return binder().getProvider(key); + } + + /** + * @see Binder#getProvider(Class) + */ + protected final Provider getProvider(Class type) { + return binder().getProvider(type); + } + + /** + * @see Binder#convertToTypes(com.google.inject.matcher.Matcher, com.google.inject.spi.TypeConverter) + */ + protected final void convertToTypes(Matcher> typeMatcher, + TypeConverter converter) { + binder().convertToTypes(typeMatcher, converter); + } + + /** + * @see Binder#currentStage() + */ + protected final Stage currentStage() { + return binder().currentStage(); + } + + /** + * @see Binder#getMembersInjector(Class) + */ + protected MembersInjector getMembersInjector(Class type) { + return binder().getMembersInjector(type); + } + + /** + * @see Binder#getMembersInjector(TypeLiteral) + */ + protected MembersInjector getMembersInjector(TypeLiteral type) { + return binder().getMembersInjector(type); + } + + /** + * @see Binder#bindListener(com.google.inject.matcher.Matcher, com.google.inject.spi.TypeListener) + */ + protected void bindListener(Matcher> typeMatcher, + TypeListener listener) { + binder().bindListener(typeMatcher, listener); + } + + /** + * @see Binder#bindListener(Matcher, ProvisionListener...) + */ + protected void bindListener(Matcher> bindingMatcher, + ProvisionListener... listeners) { + binder().bindListener(bindingMatcher, listeners); + } +} diff --git a/src/main/java/com/google/inject/ProvidedBy.java b/src/main/java/com/google/inject/ProvidedBy.java new file mode 100644 index 0000000..29b1a32 --- /dev/null +++ b/src/main/java/com/google/inject/ProvidedBy.java @@ -0,0 +1,20 @@ +package com.google.inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * A pointer to the default provider type for a type. + */ +@Retention(RUNTIME) +@Target(TYPE) +public @interface ProvidedBy { + + /** + * The implementation type. + */ + Class> value(); +} diff --git a/src/main/java/com/google/inject/Provider.java b/src/main/java/com/google/inject/Provider.java new file mode 100644 index 0000000..806cad8 --- /dev/null +++ b/src/main/java/com/google/inject/Provider.java @@ -0,0 +1,36 @@ +package com.google.inject; + +/** + * An object capable of providing instances of type {@code T}. Providers are used in numerous ways + * by Guice: + * + *
    + *
  • When the default means for obtaining instances (an injectable or parameterless constructor) + * is insufficient for a particular binding, the module can specify a custom {@code Provider} + * instead, to control exactly how Guice creates or obtains instances for the binding. + * + *
  • An implementation class may always choose to have a {@code Provider} instance injected, + * rather than having a {@code T} injected directly. This may give you access to multiple + * instances, instances you wish to safely mutate and discard, instances which are out of scope + * (e.g. using a {@code @RequestScoped} object from within a {@code @SessionScoped} object), or + * instances that will be initialized lazily. + * + *
  • A custom {@link Scope} is implemented as a decorator of {@code Provider}, which decides + * when to delegate to the backing provider and when to provide the instance some other way. + * + *
  • The {@link Injector} offers access to the {@code Provider} it uses to fulfill requests + * for a given key, via the {@link Injector#getProvider} methods. + *
+ * + * @param the type of object this provides + */ +public interface Provider extends javax.inject.Provider { + + /** + * Provides an instance of {@code T}. Must never return {@code null}. + * + * @throws ProvisionException if an instance cannot be provided. Such exceptions include messages + * and throwables to describe why provision failed. + */ + T get(); +} diff --git a/src/main/java/com/google/inject/Provides.java b/src/main/java/com/google/inject/Provides.java new file mode 100644 index 0000000..57d0af4 --- /dev/null +++ b/src/main/java/com/google/inject/Provides.java @@ -0,0 +1,18 @@ +package com.google.inject; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates methods of a {@link Module} to create a provider method binding. The method's return + * type is bound to its returned value. Guice will pass dependencies to the method as parameters. + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface Provides { +} diff --git a/src/main/java/com/google/inject/ProvisionException.java b/src/main/java/com/google/inject/ProvisionException.java new file mode 100644 index 0000000..79fc547 --- /dev/null +++ b/src/main/java/com/google/inject/ProvisionException.java @@ -0,0 +1,49 @@ +package com.google.inject; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.internal.Errors; +import com.google.inject.spi.Message; + +import java.util.Collection; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Indicates that there was a runtime failure while providing an instance. + * + */ +public final class ProvisionException extends RuntimeException { + + private static final long serialVersionUID = 0; + private final ImmutableSet messages; + + /** + * Creates a ProvisionException containing {@code messages}. + */ + public ProvisionException(Iterable messages) { + this.messages = ImmutableSet.copyOf(messages); + checkArgument(!this.messages.isEmpty()); + initCause(Errors.getOnlyCause(this.messages)); + } + + public ProvisionException(String message, Throwable cause) { + super(cause); + this.messages = ImmutableSet.of(new Message(message, cause)); + } + + public ProvisionException(String message) { + this.messages = ImmutableSet.of(new Message(message)); + } + + /** + * Returns messages for the errors that caused this exception. + */ + public Collection getErrorMessages() { + return messages; + } + + @Override + public String getMessage() { + return Errors.format("Unable to provision, see the following errors", messages); + } +} diff --git a/src/main/java/com/google/inject/Scope.java b/src/main/java/com/google/inject/Scope.java new file mode 100644 index 0000000..aa01e79 --- /dev/null +++ b/src/main/java/com/google/inject/Scope.java @@ -0,0 +1,41 @@ +package com.google.inject; + +/** + * A scope is a level of visibility that instances provided by Guice may have. + * By default, an instance created by the {@link Injector} has no scope, + * meaning it has no state from the framework's perspective -- the + * {@code Injector} creates it, injects it once into the class that required it, + * and then immediately forgets it. Associating a scope with a particular + * binding allows the created instance to be "remembered" and possibly used + * again for other injections. + * + *

An example of a scope is {@link Scopes#SINGLETON}. + */ +public interface Scope { + + /** + * Scopes a provider. The returned provider returns objects from this scope. + * If an object does not exist in this scope, the provider can use the given + * unscoped provider to retrieve one. + * + *

Scope implementations are strongly encouraged to override + * {@link Object#toString} in the returned provider and include the backing + * provider's {@code toString()} output. + * + * @param key binding key + * @param unscoped locates an instance when one doesn't already exist in this + * scope. + * @return a new provider which only delegates to the given unscoped provider + * when an instance of the requested object doesn't already exist in this + * scope + */ + public Provider scope(Key key, Provider unscoped); + + /** + * A short but useful description of this scope. For comparison, the standard + * scopes that ship with guice use the descriptions + * {@code "Scopes.SINGLETON"}, {@code "ServletScopes.SESSION"} and + * {@code "ServletScopes.REQUEST"}. + */ + String toString(); +} diff --git a/src/main/java/com/google/inject/ScopeAnnotation.java b/src/main/java/com/google/inject/ScopeAnnotation.java new file mode 100644 index 0000000..265ecb4 --- /dev/null +++ b/src/main/java/com/google/inject/ScopeAnnotation.java @@ -0,0 +1,24 @@ +package com.google.inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates annotations which are used for scoping. Only one such annotation + * may apply to a single implementation class. You must also annotate scope + * annotations with {@code @Retention(RUNTIME)}. For example: + * + *

+ *   {@code @}Retention(RUNTIME)
+ *   {@code @}Target(TYPE, METHOD)
+ *   {@code @}ScopeAnnotation
+ *   public {@code @}interface SessionScoped {}
+ * 
+ */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +public @interface ScopeAnnotation { +} diff --git a/src/main/java/com/google/inject/Scopes.java b/src/main/java/com/google/inject/Scopes.java new file mode 100644 index 0000000..70f957f --- /dev/null +++ b/src/main/java/com/google/inject/Scopes.java @@ -0,0 +1,162 @@ +package com.google.inject; + +import com.google.inject.internal.CircularDependencyProxy; +import com.google.inject.internal.LinkedBindingImpl; +import com.google.inject.internal.SingletonScope; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.ExposedBinding; + +import java.lang.annotation.Annotation; + +/** + * Built-in scope implementations. + */ +public class Scopes { + + /** + * One instance per {@link Injector}. Also see {@code @}{@link Singleton}. + */ + public static final Scope SINGLETON = new SingletonScope(); + /** + * No scope; the same as not applying any scope at all. Each time the + * Injector obtains an instance of an object with "no scope", it injects this + * instance then immediately forgets it. When the next request for the same + * binding arrives it will need to obtain the instance over again. + * + *

This exists only in case a class has been annotated with a scope + * annotation such as {@link Singleton @Singleton}, and you need to override + * this to "no scope" in your binding. + */ + public static final Scope NO_SCOPE = new Scope() { + public Provider scope(Key key, Provider unscoped) { + return unscoped; + } + + @Override + public String toString() { + return "Scopes.NO_SCOPE"; + } + }; + private static final BindingScopingVisitor IS_SINGLETON_VISITOR + = new BindingScopingVisitor() { + public Boolean visitNoScoping() { + return false; + } + + public Boolean visitScopeAnnotation(Class scopeAnnotation) { + return scopeAnnotation == Singleton.class + || scopeAnnotation == javax.inject.Singleton.class; + } + + public Boolean visitScope(Scope scope) { + return scope == Scopes.SINGLETON; + } + + public Boolean visitEagerSingleton() { + return true; + } + }; + + private Scopes() { + } + + /** + * Returns true if {@code binding} is singleton-scoped. If the binding is a {@link + * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (ie. it + * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will + * also true if the target binding is singleton-scoped. + */ + public static boolean isSingleton(Binding binding) { + do { + boolean singleton = binding.acceptScopingVisitor(IS_SINGLETON_VISITOR); + if (singleton) { + return true; + } + + if (binding instanceof LinkedBindingImpl) { + LinkedBindingImpl linkedBinding = (LinkedBindingImpl) binding; + Injector injector = linkedBinding.getInjector(); + if (injector != null) { + binding = injector.getBinding(linkedBinding.getLinkedKey()); + continue; + } + } else if (binding instanceof ExposedBinding) { + ExposedBinding exposedBinding = (ExposedBinding) binding; + Injector injector = exposedBinding.getPrivateElements().getInjector(); + if (injector != null) { + binding = injector.getBinding(exposedBinding.getKey()); + continue; + } + } + + return false; + } while (true); + } + + /** + * Returns true if {@code binding} has the given scope. If the binding is a {@link + * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (ie. it + * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will + * also true if the target binding has the given scope. + * + * @param binding binding to check + * @param scope scope implementation instance + * @param scopeAnnotation scope annotation class + */ + public static boolean isScoped(Binding binding, final Scope scope, + final Class scopeAnnotation) { + do { + boolean matches = binding.acceptScopingVisitor(new BindingScopingVisitor() { + public Boolean visitNoScoping() { + return false; + } + + public Boolean visitScopeAnnotation(Class visitedAnnotation) { + return visitedAnnotation == scopeAnnotation; + } + + public Boolean visitScope(Scope visitedScope) { + return visitedScope == scope; + } + + public Boolean visitEagerSingleton() { + return false; + } + }); + + if (matches) { + return true; + } + + if (binding instanceof LinkedBindingImpl) { + LinkedBindingImpl linkedBinding = (LinkedBindingImpl) binding; + Injector injector = linkedBinding.getInjector(); + if (injector != null) { + binding = injector.getBinding(linkedBinding.getLinkedKey()); + continue; + } + } else if (binding instanceof ExposedBinding) { + ExposedBinding exposedBinding = (ExposedBinding) binding; + Injector injector = exposedBinding.getPrivateElements().getInjector(); + if (injector != null) { + binding = injector.getBinding(exposedBinding.getKey()); + continue; + } + } + + return false; + } while (true); + } + + /** + * Returns true if the object is a proxy for a circular dependency, + * constructed by Guice because it encountered a circular dependency. Scope + * implementations should be careful to not cache circular proxies, + * because the proxies are not intended for general purpose use. (They are + * designed just to fulfill the immediate injection, not all injections. + * Caching them can lead to IllegalArgumentExceptions or ClassCastExceptions.) + */ + public static boolean isCircularProxy(Object object) { + return object instanceof CircularDependencyProxy; + } +} diff --git a/src/main/java/com/google/inject/Singleton.java b/src/main/java/com/google/inject/Singleton.java new file mode 100644 index 0000000..d3bd675 --- /dev/null +++ b/src/main/java/com/google/inject/Singleton.java @@ -0,0 +1,17 @@ +package com.google.inject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Apply this to implementation classes when you want only one instance + * (per {@link Injector}) to be reused for all injections for that binding. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RUNTIME) +@ScopeAnnotation +public @interface Singleton { +} diff --git a/src/main/java/com/google/inject/Stage.java b/src/main/java/com/google/inject/Stage.java new file mode 100644 index 0000000..f468113 --- /dev/null +++ b/src/main/java/com/google/inject/Stage.java @@ -0,0 +1,26 @@ +package com.google.inject; + +/** + * The stage we're running in. + */ +public enum Stage { + + /** + * We're running in a tool (an IDE plugin for example). We need binding meta data but not a + * functioning Injector. Do not inject members of instances. Do not load eager singletons. Do as + * little as possible so our tools run nice and snappy. Injectors created in this stage cannot + * be used to satisfy injections. + */ + TOOL, + + /** + * We want fast startup times at the expense of runtime performance and some up front error + * checking. + */ + DEVELOPMENT, + + /** + * We want to catch errors as early as possible and take performance hits up front. + */ + PRODUCTION +} diff --git a/src/main/java/com/google/inject/TypeLiteral.java b/src/main/java/com/google/inject/TypeLiteral.java new file mode 100644 index 0000000..aaa5873 --- /dev/null +++ b/src/main/java/com/google/inject/TypeLiteral.java @@ -0,0 +1,321 @@ +package com.google.inject; + +import com.google.common.collect.ImmutableList; +import com.google.inject.internal.MoreTypes; +import com.google.inject.util.Types; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.inject.internal.MoreTypes.canonicalize; + +/** + * Represents a generic type {@code T}. Java doesn't yet provide a way to + * represent generic types, so this class does. Forces clients to create a + * subclass of this class which enables retrieval the type information even at + * runtime. + * + *

For example, to create a type literal for {@code List}, you can + * create an empty anonymous inner class: + * + *

+ * {@code TypeLiteral> list = new TypeLiteral>() {};} + * + *

Along with modeling generic types, this class can resolve type parameters. + * For example, to figure out what type {@code keySet()} returns on a {@code + * Map}, use this code:

   {@code
+ *
+ *   TypeLiteral> mapType
+ *       = new TypeLiteral>() {};
+ *   TypeLiteral keySetType
+ *       = mapType.getReturnType(Map.class.getMethod("keySet"));
+ *   System.out.println(keySetType); // prints "Set"}
+ * + */ +public class TypeLiteral { + + final Class rawType; + final Type type; + final int hashCode; + + /** + * Constructs a new type literal. Derives represented class from type + * parameter. + * + *

Clients create an empty anonymous subclass. Doing so embeds the type + * parameter in the anonymous class's type hierarchy so we can reconstitute it + * at runtime despite erasure. + */ + @SuppressWarnings("unchecked") + protected TypeLiteral() { + this.type = getSuperclassTypeParameter(getClass()); + this.rawType = (Class) MoreTypes.getRawType(type); + this.hashCode = type.hashCode(); + } + + /** + * Unsafe. Constructs a type literal manually. + */ + @SuppressWarnings("unchecked") + TypeLiteral(Type type) { + this.type = canonicalize(checkNotNull(type, "type")); + this.rawType = (Class) MoreTypes.getRawType(this.type); + this.hashCode = this.type.hashCode(); + } + + /** + * Returns the type from super class's type parameter in {@link MoreTypes#canonicalize(Type) + * canonical form}. + */ + static Type getSuperclassTypeParameter(Class subclass) { + Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing type parameter."); + } + ParameterizedType parameterized = (ParameterizedType) superclass; + return canonicalize(parameterized.getActualTypeArguments()[0]); + } + + /** + * Gets type literal from super class's type parameter. + */ + static TypeLiteral fromSuperclassTypeParameter(Class subclass) { + return new TypeLiteral(getSuperclassTypeParameter(subclass)); + } + + /** + * Gets type literal for the given {@code Type} instance. + */ + public static TypeLiteral get(Type type) { + return new TypeLiteral(type); + } + + /** + * Gets type literal for the given {@code Class} instance. + */ + public static TypeLiteral get(Class type) { + return new TypeLiteral(type); + } + + /** + * Returns the raw (non-generic) type for this type. + * + */ + public final Class getRawType() { + return rawType; + } + + /** + * Gets underlying {@code Type} instance. + */ + public final Type getType() { + return type; + } + + /** + * Gets the type of this type's provider. + */ + @SuppressWarnings("unchecked") + final TypeLiteral> providerType() { + // This cast is safe and wouldn't generate a warning if Type had a type + // parameter. + return (TypeLiteral>) get(Types.providerOf(getType())); + } + + @Override + public final int hashCode() { + return this.hashCode; + } + + @Override + public final boolean equals(Object o) { + return o instanceof TypeLiteral + && MoreTypes.equals(type, ((TypeLiteral) o).type); + } + + @Override + public final String toString() { + return MoreTypes.typeToString(type); + } + + /** + * Returns an immutable list of the resolved types. + */ + private List> resolveAll(Type[] types) { + TypeLiteral[] result = new TypeLiteral[types.length]; + for (int t = 0; t < types.length; t++) { + result[t] = resolve(types[t]); + } + return ImmutableList.copyOf(result); + } + + /** + * Resolves known type parameters in {@code toResolve} and returns the result. + */ + TypeLiteral resolve(Type toResolve) { + return TypeLiteral.get(resolveType(toResolve)); + } + + Type resolveType(Type toResolve) { + // this implementation is made a little more complicated in an attempt to avoid object-creation + while (true) { + if (toResolve instanceof TypeVariable) { + TypeVariable original = (TypeVariable) toResolve; + toResolve = MoreTypes.resolveTypeVariable(type, rawType, original); + if (toResolve == original) { + return toResolve; + } + + } else if (toResolve instanceof GenericArrayType) { + GenericArrayType original = (GenericArrayType) toResolve; + Type componentType = original.getGenericComponentType(); + Type newComponentType = resolveType(componentType); + return componentType == newComponentType + ? original + : Types.arrayOf(newComponentType); + + } else if (toResolve instanceof ParameterizedType) { + ParameterizedType original = (ParameterizedType) toResolve; + Type ownerType = original.getOwnerType(); + Type newOwnerType = resolveType(ownerType); + boolean changed = newOwnerType != ownerType; + + Type[] args = original.getActualTypeArguments(); + for (int t = 0, length = args.length; t < length; t++) { + Type resolvedTypeArgument = resolveType(args[t]); + if (resolvedTypeArgument != args[t]) { + if (!changed) { + args = args.clone(); + changed = true; + } + args[t] = resolvedTypeArgument; + } + } + + return changed + ? Types.newParameterizedTypeWithOwner(newOwnerType, original.getRawType(), args) + : original; + + } else if (toResolve instanceof WildcardType) { + WildcardType original = (WildcardType) toResolve; + Type[] originalLowerBound = original.getLowerBounds(); + Type[] originalUpperBound = original.getUpperBounds(); + + if (originalLowerBound.length == 1) { + Type lowerBound = resolveType(originalLowerBound[0]); + if (lowerBound != originalLowerBound[0]) { + return Types.supertypeOf(lowerBound); + } + } else if (originalUpperBound.length == 1) { + Type upperBound = resolveType(originalUpperBound[0]); + if (upperBound != originalUpperBound[0]) { + return Types.subtypeOf(upperBound); + } + } + return original; + + } else { + return toResolve; + } + } + } + + /** + * Returns the generic form of {@code supertype}. For example, if this is {@code + * ArrayList}, this returns {@code Iterable} given the input {@code + * Iterable.class}. + * + * @param supertype a superclass of, or interface implemented by, this. + */ + public TypeLiteral getSupertype(Class supertype) { + checkArgument(supertype.isAssignableFrom(rawType), + "%s is not a supertype of %s", supertype, this.type); + return resolve(MoreTypes.getGenericSupertype(type, rawType, supertype)); + } + + /** + * Returns the resolved generic type of {@code field}. + * + * @param field a field defined by this or any superclass. + */ + public TypeLiteral getFieldType(Field field) { + checkArgument(field.getDeclaringClass().isAssignableFrom(rawType), + "%s is not defined by a supertype of %s", field, type); + return resolve(field.getGenericType()); + } + + /** + * Returns the resolved generic parameter types of {@code methodOrConstructor}. + * + * @param methodOrConstructor a method or constructor defined by this or any supertype. + */ + public List> getParameterTypes(Member methodOrConstructor) { + Type[] genericParameterTypes; + + if (methodOrConstructor instanceof Method) { + Method method = (Method) methodOrConstructor; + checkArgument(method.getDeclaringClass().isAssignableFrom(rawType), + "%s is not defined by a supertype of %s", method, type); + genericParameterTypes = method.getGenericParameterTypes(); + + } else if (methodOrConstructor instanceof Constructor) { + Constructor constructor = (Constructor) methodOrConstructor; + checkArgument(constructor.getDeclaringClass().isAssignableFrom(rawType), + "%s does not construct a supertype of %s", constructor, type); + genericParameterTypes = constructor.getGenericParameterTypes(); + + } else { + throw new IllegalArgumentException("Not a method or a constructor: " + methodOrConstructor); + } + + return resolveAll(genericParameterTypes); + } + + /** + * Returns the resolved generic exception types thrown by {@code constructor}. + * + * @param methodOrConstructor a method or constructor defined by this or any supertype. + */ + public List> getExceptionTypes(Member methodOrConstructor) { + Type[] genericExceptionTypes; + + if (methodOrConstructor instanceof Method) { + Method method = (Method) methodOrConstructor; + checkArgument(method.getDeclaringClass().isAssignableFrom(rawType), + "%s is not defined by a supertype of %s", method, type); + genericExceptionTypes = method.getGenericExceptionTypes(); + + } else if (methodOrConstructor instanceof Constructor) { + Constructor constructor = (Constructor) methodOrConstructor; + checkArgument(constructor.getDeclaringClass().isAssignableFrom(rawType), + "%s does not construct a supertype of %s", constructor, type); + genericExceptionTypes = constructor.getGenericExceptionTypes(); + + } else { + throw new IllegalArgumentException("Not a method or a constructor: " + methodOrConstructor); + } + + return resolveAll(genericExceptionTypes); + } + + /** + * Returns the resolved generic return type of {@code method}. + * + * @param method a method defined by this or any supertype. + */ + public TypeLiteral getReturnType(Method method) { + checkArgument(method.getDeclaringClass().isAssignableFrom(rawType), + "%s is not defined by a supertype of %s", method, type); + return resolve(method.getGenericReturnType()); + } +} diff --git a/src/main/java/com/google/inject/binder/AnnotatedBindingBuilder.java b/src/main/java/com/google/inject/binder/AnnotatedBindingBuilder.java new file mode 100644 index 0000000..bfd9452 --- /dev/null +++ b/src/main/java/com/google/inject/binder/AnnotatedBindingBuilder.java @@ -0,0 +1,20 @@ +package com.google.inject.binder; + +import java.lang.annotation.Annotation; + +/** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ +public interface AnnotatedBindingBuilder extends LinkedBindingBuilder { + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + LinkedBindingBuilder annotatedWith( + Class annotationType); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + LinkedBindingBuilder annotatedWith(Annotation annotation); +} diff --git a/src/main/java/com/google/inject/binder/AnnotatedConstantBindingBuilder.java b/src/main/java/com/google/inject/binder/AnnotatedConstantBindingBuilder.java new file mode 100644 index 0000000..7132452 --- /dev/null +++ b/src/main/java/com/google/inject/binder/AnnotatedConstantBindingBuilder.java @@ -0,0 +1,20 @@ +package com.google.inject.binder; + +import java.lang.annotation.Annotation; + +/** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ +public interface AnnotatedConstantBindingBuilder { + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ConstantBindingBuilder annotatedWith( + Class annotationType); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ConstantBindingBuilder annotatedWith(Annotation annotation); +} diff --git a/src/main/java/com/google/inject/binder/AnnotatedElementBuilder.java b/src/main/java/com/google/inject/binder/AnnotatedElementBuilder.java new file mode 100644 index 0000000..85fbe62 --- /dev/null +++ b/src/main/java/com/google/inject/binder/AnnotatedElementBuilder.java @@ -0,0 +1,19 @@ +package com.google.inject.binder; + +import java.lang.annotation.Annotation; + +/** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ +public interface AnnotatedElementBuilder { + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + void annotatedWith(Class annotationType); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + void annotatedWith(Annotation annotation); +} diff --git a/src/main/java/com/google/inject/binder/ConstantBindingBuilder.java b/src/main/java/com/google/inject/binder/ConstantBindingBuilder.java new file mode 100644 index 0000000..8a43b2d --- /dev/null +++ b/src/main/java/com/google/inject/binder/ConstantBindingBuilder.java @@ -0,0 +1,62 @@ +package com.google.inject.binder; + +/** + * Binds to a constant value. + */ +public interface ConstantBindingBuilder { + + /** + * Binds constant to the given value. + */ + void to(String value); + + /** + * Binds constant to the given value. + */ + void to(int value); + + /** + * Binds constant to the given value. + */ + void to(long value); + + /** + * Binds constant to the given value. + */ + void to(boolean value); + + /** + * Binds constant to the given value. + */ + void to(double value); + + /** + * Binds constant to the given value. + */ + void to(float value); + + /** + * Binds constant to the given value. + */ + void to(short value); + + /** + * Binds constant to the given value. + */ + void to(char value); + + /** + * Binds constant to the given value. + */ + void to(byte value); + + /** + * Binds constant to the given value. + */ + void to(Class value); + + /** + * Binds constant to the given value. + */ + > void to(E value); +} diff --git a/src/main/java/com/google/inject/binder/LinkedBindingBuilder.java b/src/main/java/com/google/inject/binder/LinkedBindingBuilder.java new file mode 100644 index 0000000..a22e2c0 --- /dev/null +++ b/src/main/java/com/google/inject/binder/LinkedBindingBuilder.java @@ -0,0 +1,79 @@ +package com.google.inject.binder; + +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; + +import java.lang.reflect.Constructor; + +/** + * See the EDSL examples at {@link com.google.inject.Binder}. + * + */ +public interface LinkedBindingBuilder extends ScopedBindingBuilder { + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder to(Class implementation); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder to(TypeLiteral implementation); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder to(Key targetKey); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + * + * @see com.google.inject.Injector#injectMembers + */ + void toInstance(T instance); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + * + * @see com.google.inject.Injector#injectMembers + */ + ScopedBindingBuilder toProvider(Provider provider); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + * + * @see com.google.inject.Injector#injectMembers + */ + ScopedBindingBuilder toProvider(javax.inject.Provider provider); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder toProvider( + Class> providerType); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder toProvider( + TypeLiteral> providerType); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder toProvider( + Key> providerKey); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder toConstructor(Constructor constructor); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + ScopedBindingBuilder toConstructor( + Constructor constructor, TypeLiteral type); +} diff --git a/src/main/java/com/google/inject/binder/ScopedBindingBuilder.java b/src/main/java/com/google/inject/binder/ScopedBindingBuilder.java new file mode 100644 index 0000000..3b6df6c --- /dev/null +++ b/src/main/java/com/google/inject/binder/ScopedBindingBuilder.java @@ -0,0 +1,29 @@ +package com.google.inject.binder; + +import com.google.inject.Scope; + +import java.lang.annotation.Annotation; + +/** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ +public interface ScopedBindingBuilder { + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + void in(Class scopeAnnotation); + + /** + * See the EDSL examples at {@link com.google.inject.Binder}. + */ + void in(Scope scope); + + /** + * Instructs the {@link com.google.inject.Injector} to eagerly initialize this + * singleton-scoped binding upon creation. Useful for application + * initialization logic. See the EDSL examples at + * {@link com.google.inject.Binder}. + */ + void asEagerSingleton(); +} diff --git a/src/main/java/com/google/inject/internal/AbstractBindingBuilder.java b/src/main/java/com/google/inject/internal/AbstractBindingBuilder.java new file mode 100644 index 0000000..789a6df --- /dev/null +++ b/src/main/java/com/google/inject/internal/AbstractBindingBuilder.java @@ -0,0 +1,118 @@ +package com.google.inject.internal; + +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Scope; +import com.google.inject.spi.Element; +import com.google.inject.spi.InstanceBinding; + +import java.lang.annotation.Annotation; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Bind a value or constant. + * + */ +public abstract class AbstractBindingBuilder { + + public static final String IMPLEMENTATION_ALREADY_SET = "Implementation is set more than once."; + public static final String SINGLE_INSTANCE_AND_SCOPE + = "Setting the scope is not permitted when binding to a single instance."; + public static final String SCOPE_ALREADY_SET = "Scope is set more than once."; + public static final String BINDING_TO_NULL = "Binding to null instances is not allowed. " + + "Use toProvider(Providers.of(null)) if this is your intended behaviour."; + public static final String CONSTANT_VALUE_ALREADY_SET = "Constant value is set more than once."; + public static final String ANNOTATION_ALREADY_SPECIFIED + = "More than one annotation is specified for this binding."; + + protected static final Key NULL_KEY = Key.get(Void.class); + protected final Binder binder; + protected List elements; + protected int position; + private BindingImpl binding; + + public AbstractBindingBuilder(Binder binder, List elements, Object source, Key key) { + this.binder = binder; + this.elements = elements; + this.position = elements.size(); + this.binding = new UntargettedBindingImpl(source, key, Scoping.UNSCOPED); + elements.add(position, this.binding); + } + + protected BindingImpl getBinding() { + return binding; + } + + protected BindingImpl setBinding(BindingImpl binding) { + this.binding = binding; + elements.set(position, binding); + return binding; + } + + /** + * Sets the binding to a copy with the specified annotation on the bound key + */ + protected BindingImpl annotatedWithInternal(Class annotationType) { + checkNotNull(annotationType, "annotationType"); + checkNotAnnotated(); + return setBinding(binding.withKey( + Key.get(this.binding.getKey().getTypeLiteral(), annotationType))); + } + + /** + * Sets the binding to a copy with the specified annotation on the bound key + */ + protected BindingImpl annotatedWithInternal(Annotation annotation) { + checkNotNull(annotation, "annotation"); + checkNotAnnotated(); + return setBinding(binding.withKey( + Key.get(this.binding.getKey().getTypeLiteral(), annotation))); + } + + public void in(final Class scopeAnnotation) { + checkNotNull(scopeAnnotation, "scopeAnnotation"); + checkNotScoped(); + setBinding(getBinding().withScoping(Scoping.forAnnotation(scopeAnnotation))); + } + + public void in(final Scope scope) { + checkNotNull(scope, "scope"); + checkNotScoped(); + setBinding(getBinding().withScoping(Scoping.forInstance(scope))); + } + + public void asEagerSingleton() { + checkNotScoped(); + setBinding(getBinding().withScoping(Scoping.EAGER_SINGLETON)); + } + + protected boolean keyTypeIsSet() { + return !Void.class.equals(binding.getKey().getTypeLiteral().getType()); + } + + protected void checkNotTargetted() { + if (!(binding instanceof UntargettedBindingImpl)) { + binder.addError(IMPLEMENTATION_ALREADY_SET); + } + } + + protected void checkNotAnnotated() { + if (binding.getKey().getAnnotationType() != null) { + binder.addError(ANNOTATION_ALREADY_SPECIFIED); + } + } + + protected void checkNotScoped() { + // Scoping isn't allowed when we have only one instance. + if (binding instanceof InstanceBinding) { + binder.addError(SINGLE_INSTANCE_AND_SCOPE); + return; + } + + if (binding.getScoping().isExplicitlyScoped()) { + binder.addError(SCOPE_ALREADY_SET); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/AbstractBindingProcessor.java b/src/main/java/com/google/inject/internal/AbstractBindingProcessor.java new file mode 100644 index 0000000..c21b01c --- /dev/null +++ b/src/main/java/com/google/inject/internal/AbstractBindingProcessor.java @@ -0,0 +1,146 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.DefaultBindingTargetVisitor; + +import java.util.Set; + +/** + * Guarantees that processing of Binding elements happens in a sane way. + */ +abstract class AbstractBindingProcessor extends AbstractProcessor { + + // It's unfortunate that we have to maintain a blacklist of specific + // classes, but we can't easily block the whole package because of + // all our unit tests. + private static final Set> FORBIDDEN_TYPES = ImmutableSet.>of( + AbstractModule.class, + Binder.class, + Binding.class, + Injector.class, + Key.class, + MembersInjector.class, + Module.class, + Provider.class, + Scope.class, + Stage.class, + TypeLiteral.class); + + protected final ProcessedBindingData bindingData; + + AbstractBindingProcessor(Errors errors, ProcessedBindingData bindingData) { + super(errors); + this.bindingData = bindingData; + } + + protected UntargettedBindingImpl invalidBinding( + InjectorImpl injector, Key key, Object source) { + return new UntargettedBindingImpl(injector, key, source); + } + + protected void putBinding(BindingImpl binding) { + Key key = binding.getKey(); + + Class rawType = key.getTypeLiteral().getRawType(); + if (FORBIDDEN_TYPES.contains(rawType)) { + errors.cannotBindToGuiceType(rawType.getSimpleName()); + return; + } + + BindingImpl original = injector.getExistingBinding(key); + if (original != null) { + // If it failed because of an explicit duplicate binding... + if (injector.state.getExplicitBinding(key) != null) { + try { + if (!isOkayDuplicate(original, binding, injector.state)) { + errors.bindingAlreadySet(key, original.getSource()); + return; + } + } catch (Throwable t) { + errors.errorCheckingDuplicateBinding(key, original.getSource(), t); + return; + } + } else { + // Otherwise, it failed because of a duplicate JIT binding + // in the parent + errors.jitBindingAlreadySet(key); + return; + } + } + + // prevent the parent from creating a JIT binding for this key + injector.state.parent().blacklist(key, injector.state, binding.getSource()); + injector.state.putBinding(key, binding); + } + + /** + * We tolerate duplicate bindings if one exposes the other or if the two bindings + * are considered duplicates. + * + * @param original the binding in the parent injector (candidate for an exposing binding) + * @param binding the binding to check (candidate for the exposed binding) + */ + private boolean isOkayDuplicate(BindingImpl original, BindingImpl binding, State state) { + if (original instanceof ExposedBindingImpl) { + ExposedBindingImpl exposed = (ExposedBindingImpl) original; + InjectorImpl exposedFrom = (InjectorImpl) exposed.getPrivateElements().getInjector(); + return (exposedFrom == binding.getInjector()); + } else { + original = (BindingImpl) state.getExplicitBindingsThisLevel().get(binding.getKey()); + // If no original at this level, the original was on a parent, and we don't + // allow deduplication between parents & children. + return original != null && original.equals(binding); + } + } + + private void validateKey(Object source, Key key) { + Annotations.checkForMisplacedScopeAnnotations( + key.getTypeLiteral().getRawType(), source, errors); + } + + /** + * Processor for visiting bindings. Each overriden method that wants to + * actually process the binding should call prepareBinding first. + */ + abstract class Processor extends DefaultBindingTargetVisitor { + final Object source; + final Key key; + final Class rawType; + Scoping scoping; + + Processor(BindingImpl binding) { + source = binding.getSource(); + key = binding.getKey(); + rawType = key.getTypeLiteral().getRawType(); + scoping = binding.getScoping(); + } + + protected void prepareBinding() { + validateKey(source, key); + scoping = Scoping.makeInjectable(scoping, injector, errors); + } + + protected void scheduleInitialization(final BindingImpl binding) { + bindingData.addUninitializedBinding(new Runnable() { + public void run() { + try { + binding.getInjector().initializeBinding(binding, errors.withSource(source)); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + }); + } + } +} diff --git a/src/main/java/com/google/inject/internal/AbstractProcessor.java b/src/main/java/com/google/inject/internal/AbstractProcessor.java new file mode 100644 index 0000000..e36bae7 --- /dev/null +++ b/src/main/java/com/google/inject/internal/AbstractProcessor.java @@ -0,0 +1,54 @@ +package com.google.inject.internal; + +import com.google.inject.spi.DefaultElementVisitor; +import com.google.inject.spi.Element; + +import java.util.Iterator; +import java.util.List; + +/** + * Abstract base class for creating an injector from module elements. + * + *

Extending classes must return {@code true} from any overridden + * {@code visit*()} methods, in order for the element processor to remove the + * handled element. + * + */ +abstract class AbstractProcessor extends DefaultElementVisitor { + + protected Errors errors; + protected InjectorImpl injector; + + protected AbstractProcessor(Errors errors) { + this.errors = errors; + } + + public void process(Iterable isolatedInjectorBuilders) { + for (InjectorShell injectorShell : isolatedInjectorBuilders) { + process(injectorShell.getInjector(), injectorShell.getElements()); + } + } + + public void process(InjectorImpl injector, List elements) { + Errors errorsAnyElement = this.errors; + this.injector = injector; + try { + for (Iterator i = elements.iterator(); i.hasNext(); ) { + Element element = i.next(); + this.errors = errorsAnyElement.withSource(element.getSource()); + Boolean allDone = element.acceptVisitor(this); + if (allDone) { + i.remove(); + } + } + } finally { + this.errors = errorsAnyElement; + this.injector = null; + } + } + + @Override + protected Boolean visitOther(Element element) { + return false; + } +} diff --git a/src/main/java/com/google/inject/internal/Annotations.java b/src/main/java/com/google/inject/internal/Annotations.java new file mode 100644 index 0000000..0c9e27a --- /dev/null +++ b/src/main/java/com/google/inject/internal/Annotations.java @@ -0,0 +1,334 @@ +package com.google.inject.internal; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Joiner.MapJoiner; +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.BindingAnnotation; +import com.google.inject.Key; +import com.google.inject.ScopeAnnotation; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.util.Classes; +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import javax.inject.Qualifier; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +/** + * Annotation utilities. + * + */ +public class Annotations { + + private static final MapJoiner JOINER = Joiner.on(", ").withKeyValueSeparator("="); + private static final Function DEEP_TO_STRING_FN = new Function() { + @Override + public String apply(Object arg) { + String s = Arrays.deepToString(new Object[]{arg}); + return s.substring(1, s.length() - 1); // cut off brackets + } + }; + private static final LoadingCache, Annotation> cache = + CacheBuilder.newBuilder().weakKeys().build( + new CacheLoader, Annotation>() { + @Override + public Annotation load(Class input) { + return generateAnnotationImpl(input); + } + }); + private static final AnnotationChecker scopeChecker = new AnnotationChecker( + Arrays.asList(ScopeAnnotation.class, javax.inject.Scope.class)); + private static final AnnotationChecker bindingAnnotationChecker = new AnnotationChecker( + Arrays.asList(BindingAnnotation.class, Qualifier.class)); + + /** + * Returns {@code true} if the given annotation type has no attributes. + */ + public static boolean isMarker(Class annotationType) { + return annotationType.getDeclaredMethods().length == 0; + } + + public static boolean isAllDefaultMethods(Class annotationType) { + boolean hasMethods = false; + for (Method m : annotationType.getDeclaredMethods()) { + hasMethods = true; + if (m.getDefaultValue() == null) { + return false; + } + } + return hasMethods; + } + + /** + * Generates an Annotation for the annotation class. Requires that the annotation is all + * optionals. + */ + @SuppressWarnings("unchecked") + public static T generateAnnotation(Class annotationType) { + Preconditions.checkState( + isAllDefaultMethods(annotationType), "%s is not all default methods", annotationType); + return (T) cache.getUnchecked(annotationType); + } + + private static T generateAnnotationImpl(final Class annotationType) { + final Map members = resolveMembers(annotationType); + return annotationType.cast(Proxy.newProxyInstance( + annotationType.getClassLoader(), + new Class[]{annotationType}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Exception { + String name = method.getName(); + if (name.equals("annotationType")) { + return annotationType; + } else if (name.equals("toString")) { + return annotationToString(annotationType, members); + } else if (name.equals("hashCode")) { + return annotationHashCode(annotationType, members); + } else if (name.equals("equals")) { + return annotationEquals(annotationType, members, args[0]); + } else { + return members.get(name); + } + } + })); + } + + private static ImmutableMap resolveMembers( + Class annotationType) { + ImmutableMap.Builder result = ImmutableMap.builder(); + for (Method method : annotationType.getDeclaredMethods()) { + result.put(method.getName(), method.getDefaultValue()); + } + return result.build(); + } + + /** + * Implements {@link Annotation#equals}. + */ + private static boolean annotationEquals(Class type, + Map members, Object other) throws Exception { + if (!type.isInstance(other)) { + return false; + } + for (Method method : type.getDeclaredMethods()) { + String name = method.getName(); + if (!Arrays.deepEquals( + new Object[]{method.invoke(other)}, new Object[]{members.get(name)})) { + return false; + } + } + return true; + } + + /** + * Implements {@link Annotation#hashCode}. + */ + private static int annotationHashCode(Class type, + Map members) throws Exception { + int result = 0; + for (Method method : type.getDeclaredMethods()) { + String name = method.getName(); + Object value = members.get(name); + result += (127 * name.hashCode()) ^ (Arrays.deepHashCode(new Object[]{value}) - 31); + } + return result; + } + + /** + * Implements {@link Annotation#toString}. + */ + private static String annotationToString(Class type, + Map members) throws Exception { + StringBuilder sb = new StringBuilder().append("@").append(type.getName()).append("("); + JOINER.appendTo(sb, Maps.transformValues(members, DEEP_TO_STRING_FN)); + return sb.append(")").toString(); + } + + /** + * Returns true if the given annotation is retained at runtime. + */ + public static boolean isRetainedAtRuntime(Class annotationType) { + Retention retention = annotationType.getAnnotation(Retention.class); + return retention != null && retention.value() == RetentionPolicy.RUNTIME; + } + + /** + * Returns the scope annotation on {@code type}, or null if none is specified. + */ + public static Class findScopeAnnotation( + Errors errors, Class implementation) { + return findScopeAnnotation(errors, implementation.getAnnotations()); + } + + /** + * Returns the scoping annotation, or null if there isn't one. + */ + public static Class findScopeAnnotation(Errors errors, Annotation[] annotations) { + Class found = null; + + for (Annotation annotation : annotations) { + Class annotationType = annotation.annotationType(); + if (isScopeAnnotation(annotationType)) { + if (found != null) { + errors.duplicateScopeAnnotations(found, annotationType); + } else { + found = annotationType; + } + } + } + + return found; + } + + static boolean containsComponentAnnotation(Annotation[] annotations) { + for (Annotation annotation : annotations) { + // TODO(user): Should we scope this down to dagger.Component? + if (annotation.annotationType().getSimpleName().equals("Component")) { + return true; + } + } + + return false; + } + + public static boolean isScopeAnnotation(Class annotationType) { + return scopeChecker.hasAnnotations(annotationType); + } + + /** + * Adds an error if there is a misplaced annotations on {@code type}. Scoping + * annotations are not allowed on abstract classes or interfaces. + */ + public static void checkForMisplacedScopeAnnotations( + Class type, Object source, Errors errors) { + if (Classes.isConcrete(type)) { + return; + } + + Class scopeAnnotation = findScopeAnnotation(errors, type); + if (scopeAnnotation != null + // We let Dagger Components through to aid migrations. + && !containsComponentAnnotation(type.getAnnotations())) { + errors.withSource(type).scopeAnnotationOnAbstractType(scopeAnnotation, type, source); + } + } + + /** + * Gets a key for the given type, member and annotations. + */ + public static Key getKey(TypeLiteral type, Member member, Annotation[] annotations, + Errors errors) throws ErrorsException { + int numErrorsBefore = errors.size(); + Annotation found = findBindingAnnotation(errors, member, annotations); + errors.throwIfNewErrors(numErrorsBefore); + return found == null ? Key.get(type) : Key.get(type, found); + } + + /** + * Returns the binding annotation on {@code member}, or null if there isn't one. + */ + public static Annotation findBindingAnnotation( + Errors errors, Member member, Annotation[] annotations) { + Annotation found = null; + + for (Annotation annotation : annotations) { + Class annotationType = annotation.annotationType(); + if (isBindingAnnotation(annotationType)) { + if (found != null) { + errors.duplicateBindingAnnotations(member, found.annotationType(), annotationType); + } else { + found = annotation; + } + } + } + + return found; + } + + /** + * Returns true if annotations of the specified type are binding annotations. + */ + public static boolean isBindingAnnotation(Class annotationType) { + return bindingAnnotationChecker.hasAnnotations(annotationType); + } + + /** + * If the annotation is an instance of {@code javax.inject.Named}, canonicalizes to + * com.google.guice.name.Named. Returns the given annotation otherwise. + */ + public static Annotation canonicalizeIfNamed(Annotation annotation) { + if (annotation instanceof javax.inject.Named) { + return Names.named(((javax.inject.Named) annotation).value()); + } else { + return annotation; + } + } + + /** + * If the annotation is the class {@code javax.inject.Named}, canonicalizes to + * com.google.guice.name.Named. Returns the given annotation class otherwise. + */ + public static Class canonicalizeIfNamed( + Class annotationType) { + if (annotationType == javax.inject.Named.class) { + return Named.class; + } else { + return annotationType; + } + } + + /** + * Checks for the presence of annotations. Caches results because Android doesn't. + */ + static class AnnotationChecker { + private final Collection> annotationTypes; + + /** + * Returns true if the given class has one of the desired annotations. + */ + private CacheLoader, Boolean> hasAnnotations = + new CacheLoader, Boolean>() { + public Boolean load(Class annotationType) { + for (Annotation annotation : annotationType.getAnnotations()) { + if (annotationTypes.contains(annotation.annotationType())) { + return true; + } + } + return false; + } + }; + + final LoadingCache, Boolean> cache = CacheBuilder.newBuilder().weakKeys() + .build(hasAnnotations); + + /** + * Constructs a new checker that looks for annotations of the given types. + */ + AnnotationChecker(Collection> annotationTypes) { + this.annotationTypes = annotationTypes; + } + + /** + * Returns true if the given type has one of the desired annotations. + */ + boolean hasAnnotations(Class annotated) { + return cache.getUnchecked(annotated); + } + } +} diff --git a/src/main/java/com/google/inject/internal/BindingBuilder.java b/src/main/java/com/google/inject/internal/BindingBuilder.java new file mode 100644 index 0000000..aefe976 --- /dev/null +++ b/src/main/java/com/google/inject/internal/BindingBuilder.java @@ -0,0 +1,167 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.ConfigurationException; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.ScopedBindingBuilder; +import com.google.inject.spi.Element; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.Message; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Bind a non-constant key. + */ +public class BindingBuilder extends AbstractBindingBuilder + implements AnnotatedBindingBuilder { + + public BindingBuilder(Binder binder, List elements, Object source, Key key) { + super(binder, elements, source, key); + } + + public BindingBuilder annotatedWith(Class annotationType) { + annotatedWithInternal(annotationType); + return this; + } + + public BindingBuilder annotatedWith(Annotation annotation) { + annotatedWithInternal(annotation); + return this; + } + + public BindingBuilder to(Class implementation) { + return to(Key.get(implementation)); + } + + public BindingBuilder to(TypeLiteral implementation) { + return to(Key.get(implementation)); + } + + public BindingBuilder to(Key linkedKey) { + checkNotNull(linkedKey, "linkedKey"); + checkNotTargetted(); + BindingImpl base = getBinding(); + setBinding(new LinkedBindingImpl( + base.getSource(), base.getKey(), base.getScoping(), linkedKey)); + return this; + } + + public void toInstance(T instance) { + checkNotTargetted(); + + // lookup the injection points, adding any errors to the binder's errors list + Set injectionPoints; + if (instance != null) { + try { + injectionPoints = InjectionPoint.forInstanceMethodsAndFields(instance.getClass()); + } catch (ConfigurationException e) { + copyErrorsToBinder(e); + injectionPoints = e.getPartialValue(); + } + } else { + binder.addError(BINDING_TO_NULL); + injectionPoints = ImmutableSet.of(); + } + + BindingImpl base = getBinding(); + setBinding(new InstanceBindingImpl( + base.getSource(), base.getKey(), Scoping.EAGER_SINGLETON, injectionPoints, instance)); + } + + @SuppressWarnings("unchecked") + public BindingBuilder toProvider(Provider provider) { + return toProvider((javax.inject.Provider) provider); + } + + public BindingBuilder toProvider(javax.inject.Provider provider) { + checkNotNull(provider, "provider"); + checkNotTargetted(); + + // lookup the injection points, adding any errors to the binder's errors list + Set injectionPoints; + try { + injectionPoints = InjectionPoint.forInstanceMethodsAndFields(provider.getClass()); + } catch (ConfigurationException e) { + copyErrorsToBinder(e); + injectionPoints = e.getPartialValue(); + } + + BindingImpl base = getBinding(); + setBinding(new ProviderInstanceBindingImpl( + base.getSource(), base.getKey(), base.getScoping(), injectionPoints, provider)); + return this; + } + + public BindingBuilder toProvider( + Class> providerType) { + return toProvider(Key.get(providerType)); + } + + public BindingBuilder toProvider( + TypeLiteral> providerType) { + return toProvider(Key.get(providerType)); + } + + public BindingBuilder toProvider( + Key> providerKey) { + checkNotNull(providerKey, "providerKey"); + checkNotTargetted(); + + BindingImpl base = getBinding(); + setBinding(new LinkedProviderBindingImpl( + base.getSource(), base.getKey(), base.getScoping(), providerKey)); + return this; + } + + public ScopedBindingBuilder toConstructor(Constructor constructor) { + return toConstructor(constructor, TypeLiteral.get(constructor.getDeclaringClass())); + } + + public ScopedBindingBuilder toConstructor(Constructor constructor, + TypeLiteral type) { + checkNotNull(constructor, "constructor"); + checkNotNull(type, "type"); + checkNotTargetted(); + + BindingImpl base = getBinding(); + + Set injectionPoints; + try { + injectionPoints = InjectionPoint.forInstanceMethodsAndFields(type); + } catch (ConfigurationException e) { + copyErrorsToBinder(e); + injectionPoints = e.getPartialValue(); + } + + try { + InjectionPoint constructorPoint = InjectionPoint.forConstructor(constructor, type); + setBinding(new ConstructorBindingImpl(base.getKey(), base.getSource(), base.getScoping(), + constructorPoint, injectionPoints)); + } catch (ConfigurationException e) { + copyErrorsToBinder(e); + } + + return this; + } + + @Override + public String toString() { + return "BindingBuilder<" + getBinding().getKey().getTypeLiteral() + ">"; + } + + private void copyErrorsToBinder(ConfigurationException e) { + for (Message message : e.getErrorMessages()) { + binder.addError(message); + } + } +} diff --git a/src/main/java/com/google/inject/internal/BindingImpl.java b/src/main/java/com/google/inject/internal/BindingImpl.java new file mode 100644 index 0000000..6a9ed1b --- /dev/null +++ b/src/main/java/com/google/inject/internal/BindingImpl.java @@ -0,0 +1,100 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.ElementVisitor; +import com.google.inject.spi.InstanceBinding; + +public abstract class BindingImpl implements Binding { + + private final InjectorImpl injector; + private final Key key; + private final Object source; + private final Scoping scoping; + private final InternalFactory internalFactory; + private volatile Provider provider; + + public BindingImpl(InjectorImpl injector, Key key, Object source, + InternalFactory internalFactory, Scoping scoping) { + this.injector = injector; + this.key = key; + this.source = source; + this.internalFactory = internalFactory; + this.scoping = scoping; + } + + protected BindingImpl(Object source, Key key, Scoping scoping) { + this.internalFactory = null; + this.injector = null; + this.source = source; + this.key = key; + this.scoping = scoping; + } + + public Key getKey() { + return key; + } + + public Object getSource() { + return source; + } + + public Provider getProvider() { + if (provider == null) { + if (injector == null) { + throw new UnsupportedOperationException("getProvider() not supported for module bindings"); + } + + provider = injector.getProvider(key); + } + return provider; + } + + public InternalFactory getInternalFactory() { + return internalFactory; + } + + public Scoping getScoping() { + return scoping; + } + + /** + * Is this a constant binding? This returns true for constant bindings as + * well as toInstance() bindings. + */ + public boolean isConstant() { + return this instanceof InstanceBinding; + } + + public V acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public V acceptScopingVisitor(BindingScopingVisitor visitor) { + return scoping.acceptVisitor(visitor); + } + + protected BindingImpl withScoping(Scoping scoping) { + throw new AssertionError(); + } + + protected BindingImpl withKey(Key key) { + throw new AssertionError(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(Binding.class) + .add("key", key) + .add("scope", scoping) + .add("source", source) + .toString(); + } + + public InjectorImpl getInjector() { + return injector; + } +} diff --git a/src/main/java/com/google/inject/internal/BindingProcessor.java b/src/main/java/com/google/inject/internal/BindingProcessor.java new file mode 100644 index 0000000..2d4c067 --- /dev/null +++ b/src/main/java/com/google/inject/internal/BindingProcessor.java @@ -0,0 +1,180 @@ +package com.google.inject.internal; + +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.spi.ConstructorBinding; +import com.google.inject.spi.ConvertedConstantBinding; +import com.google.inject.spi.ExposedBinding; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.spi.PrivateElements; +import com.google.inject.spi.ProviderBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderKeyBinding; +import com.google.inject.spi.UntargettedBinding; + +import java.util.Set; + +/** + * Handles {@link Binder#bind} and {@link Binder#bindConstant} elements. + */ +final class BindingProcessor extends AbstractBindingProcessor { + + private final Initializer initializer; + + BindingProcessor(Errors errors, Initializer initializer, ProcessedBindingData bindingData) { + super(errors, bindingData); + this.initializer = initializer; + } + + @Override + public Boolean visit(Binding command) { + Class rawType = command.getKey().getTypeLiteral().getRawType(); + if (Void.class.equals(rawType)) { + if (command instanceof ProviderInstanceBinding + && ((ProviderInstanceBinding) command).getUserSuppliedProvider() + instanceof ProviderMethod) { + errors.voidProviderMethod(); + } else { + errors.missingConstantValues(); + } + return true; + } + + if (rawType == Provider.class) { + errors.bindingToProvider(); + return true; + } + + return command.acceptTargetVisitor(new Processor((BindingImpl) command) { + @Override + public Boolean visit(ConstructorBinding binding) { + prepareBinding(); + try { + ConstructorBindingImpl onInjector = ConstructorBindingImpl.create(injector, key, + binding.getConstructor(), source, scoping, errors, false, false); + scheduleInitialization(onInjector); + putBinding(onInjector); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + putBinding(invalidBinding(injector, key, source)); + } + return true; + } + + @Override + public Boolean visit(InstanceBinding binding) { + prepareBinding(); + Set injectionPoints = binding.getInjectionPoints(); + T instance = binding.getInstance(); + @SuppressWarnings("unchecked") // safe to cast to binding because + // the processor was constructed w/ it + Initializable ref = initializer.requestInjection( + injector, instance, (Binding) binding, source, injectionPoints); + ConstantFactory factory = new ConstantFactory(ref); + InternalFactory scopedFactory + = Scoping.scope(key, injector, factory, source, scoping); + putBinding(new InstanceBindingImpl(injector, key, source, scopedFactory, injectionPoints, + instance)); + return true; + } + + @Override + public Boolean visit(ProviderInstanceBinding binding) { + prepareBinding(); + javax.inject.Provider provider = binding.getUserSuppliedProvider(); + Set injectionPoints = binding.getInjectionPoints(); + Initializable> initializable = + initializer.>requestInjection( + injector, provider, null, source, injectionPoints); + // always visited with Binding + @SuppressWarnings("unchecked") + InternalFactory factory = new InternalFactoryToInitializableAdapter( + initializable, source, + injector.provisionListenerStore.get((ProviderInstanceBinding) binding)); + InternalFactory scopedFactory + = Scoping.scope(key, injector, factory, source, scoping); + putBinding(new ProviderInstanceBindingImpl(injector, key, source, scopedFactory, scoping, + provider, injectionPoints)); + return true; + } + + @Override + public Boolean visit(ProviderKeyBinding binding) { + prepareBinding(); + Key> providerKey = binding.getProviderKey(); + // always visited with Binding + @SuppressWarnings("unchecked") + BoundProviderFactory boundProviderFactory = new BoundProviderFactory( + injector, providerKey, source, + injector.provisionListenerStore.get((ProviderKeyBinding) binding)); + bindingData.addCreationListener(boundProviderFactory); + InternalFactory scopedFactory = Scoping.scope( + key, injector, (InternalFactory) boundProviderFactory, source, scoping); + putBinding(new LinkedProviderBindingImpl( + injector, key, source, scopedFactory, scoping, providerKey)); + return true; + } + + @Override + public Boolean visit(LinkedKeyBinding binding) { + prepareBinding(); + Key linkedKey = binding.getLinkedKey(); + if (key.equals(linkedKey)) { + errors.recursiveBinding(); + } + + FactoryProxy factory = new FactoryProxy(injector, key, linkedKey, source); + bindingData.addCreationListener(factory); + InternalFactory scopedFactory + = Scoping.scope(key, injector, factory, source, scoping); + putBinding( + new LinkedBindingImpl(injector, key, source, scopedFactory, scoping, linkedKey)); + return true; + } + + @Override + public Boolean visit(UntargettedBinding untargetted) { + return false; + } + + @Override + public Boolean visit(ExposedBinding binding) { + throw new IllegalArgumentException("Cannot apply a non-module element"); + } + + @Override + public Boolean visit(ConvertedConstantBinding binding) { + throw new IllegalArgumentException("Cannot apply a non-module element"); + } + + @Override + public Boolean visit(ProviderBinding binding) { + throw new IllegalArgumentException("Cannot apply a non-module element"); + } + + @Override + protected Boolean visitOther(Binding binding) { + throw new IllegalStateException("BindingProcessor should override all visitations"); + } + }); + } + + @Override + public Boolean visit(PrivateElements privateElements) { + for (Key key : privateElements.getExposedKeys()) { + bindExposed(privateElements, key); + } + return false; // leave the private elements for the PrivateElementsProcessor to handle + } + + private void bindExposed(PrivateElements privateElements, Key key) { + ExposedKeyFactory exposedKeyFactory = new ExposedKeyFactory(key, privateElements); + bindingData.addCreationListener(exposedKeyFactory); + putBinding(new ExposedBindingImpl( + injector, privateElements.getExposedSource(key), key, exposedKeyFactory, privateElements)); + } +} diff --git a/src/main/java/com/google/inject/internal/BoundProviderFactory.java b/src/main/java/com/google/inject/internal/BoundProviderFactory.java new file mode 100644 index 0000000..528f8d4 --- /dev/null +++ b/src/main/java/com/google/inject/internal/BoundProviderFactory.java @@ -0,0 +1,66 @@ +package com.google.inject.internal; + +import com.google.inject.Key; +import com.google.inject.internal.InjectorImpl.JitLimitation; +import com.google.inject.spi.Dependency; + +import javax.inject.Provider; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Delegates to a custom factory which is also bound in the injector. + */ +final class BoundProviderFactory extends ProviderInternalFactory implements CreationListener { + + final Key> providerKey; + private final ProvisionListenerStackCallback provisionCallback; + private final InjectorImpl injector; + private InternalFactory> providerFactory; + + BoundProviderFactory( + InjectorImpl injector, + Key> providerKey, + Object source, + ProvisionListenerStackCallback provisionCallback) { + super(source); + this.provisionCallback = checkNotNull(provisionCallback, "provisionCallback"); + this.injector = injector; + this.providerKey = providerKey; + } + + public void notify(Errors errors) { + try { + providerFactory = injector.getInternalFactory(providerKey, errors.withSource(source), JitLimitation.NEW_OR_EXISTING_JIT); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + context.pushState(providerKey, source); + try { + errors = errors.withSource(providerKey); + javax.inject.Provider provider = providerFactory.get(errors, context, dependency, true); + return circularGet(provider, errors, context, dependency, provisionCallback); + } finally { + context.popState(); + } + } + + @Override + protected T provision(Provider provider, Errors errors, Dependency dependency, + ConstructionContext constructionContext) throws ErrorsException { + try { + return super.provision(provider, errors, dependency, constructionContext); + } catch (RuntimeException userException) { + throw errors.errorInProvider(userException).toException(); + } + } + + @Override + public String toString() { + return providerKey.toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/CircularDependencyProxy.java b/src/main/java/com/google/inject/internal/CircularDependencyProxy.java new file mode 100644 index 0000000..46aeda1 --- /dev/null +++ b/src/main/java/com/google/inject/internal/CircularDependencyProxy.java @@ -0,0 +1,5 @@ +package com.google.inject.internal; + +public interface CircularDependencyProxy { + // marker interface +} diff --git a/src/main/java/com/google/inject/internal/ConstantBindingBuilderImpl.java b/src/main/java/com/google/inject/internal/ConstantBindingBuilderImpl.java new file mode 100644 index 0000000..22a13dd --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstantBindingBuilderImpl.java @@ -0,0 +1,115 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.binder.AnnotatedConstantBindingBuilder; +import com.google.inject.binder.ConstantBindingBuilder; +import com.google.inject.spi.Element; +import com.google.inject.spi.InjectionPoint; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Bind a constant. + * + */ +public final class ConstantBindingBuilderImpl + extends AbstractBindingBuilder + implements AnnotatedConstantBindingBuilder, ConstantBindingBuilder { + + @SuppressWarnings("unchecked") // constant bindings start out with T unknown + public ConstantBindingBuilderImpl(Binder binder, List elements, Object source) { + super(binder, elements, source, (Key) NULL_KEY); + } + + public ConstantBindingBuilder annotatedWith(Class annotationType) { + annotatedWithInternal(annotationType); + return this; + } + + public ConstantBindingBuilder annotatedWith(Annotation annotation) { + annotatedWithInternal(annotation); + return this; + } + + public void to(final String value) { + toConstant(String.class, value); + } + + public void to(final int value) { + toConstant(Integer.class, value); + } + + public void to(final long value) { + toConstant(Long.class, value); + } + + public void to(final boolean value) { + toConstant(Boolean.class, value); + } + + public void to(final double value) { + toConstant(Double.class, value); + } + + public void to(final float value) { + toConstant(Float.class, value); + } + + public void to(final short value) { + toConstant(Short.class, value); + } + + public void to(final char value) { + toConstant(Character.class, value); + } + + public void to(final byte value) { + toConstant(Byte.class, value); + } + + public void to(final Class value) { + toConstant(Class.class, value); + } + + public > void to(final E value) { + toConstant(value.getDeclaringClass(), value); + } + + private void toConstant(Class type, Object instance) { + // this type will define T, so these assignments are safe + @SuppressWarnings("unchecked") + Class typeAsClassT = (Class) type; + @SuppressWarnings("unchecked") + T instanceAsT = (T) instance; + + if (keyTypeIsSet()) { + binder.addError(CONSTANT_VALUE_ALREADY_SET); + return; + } + + BindingImpl base = getBinding(); + Key key; + if (base.getKey().getAnnotation() != null) { + key = Key.get(typeAsClassT, base.getKey().getAnnotation()); + } else if (base.getKey().getAnnotationType() != null) { + key = Key.get(typeAsClassT, base.getKey().getAnnotationType()); + } else { + key = Key.get(typeAsClassT); + } + + if (instanceAsT == null) { + binder.addError(BINDING_TO_NULL); + } + + setBinding(new InstanceBindingImpl( + base.getSource(), key, base.getScoping(), ImmutableSet.of(), instanceAsT)); + } + + @Override + public String toString() { + return "ConstantBindingBuilder"; + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/ConstantFactory.java b/src/main/java/com/google/inject/internal/ConstantFactory.java new file mode 100644 index 0000000..2c18513 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstantFactory.java @@ -0,0 +1,24 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.inject.spi.Dependency; + +final class ConstantFactory implements InternalFactory { + + private final Initializable initializable; + + public ConstantFactory(Initializable initializable) { + this.initializable = initializable; + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + return initializable.get(errors); + } + + public String toString() { + return MoreObjects.toStringHelper(ConstantFactory.class) + .add("value", initializable) + .toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/ConstructionContext.java b/src/main/java/com/google/inject/internal/ConstructionContext.java new file mode 100644 index 0000000..a2dbbc8 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstructionContext.java @@ -0,0 +1,75 @@ +package com.google.inject.internal; + +import com.google.inject.internal.InjectorImpl.InjectorOptions; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +/** + * Context of a dependency construction. Used to manage circular references. + */ +final class ConstructionContext { + + T currentReference; + boolean constructing; + + List> invocationHandlers; + + public T getCurrentReference() { + return currentReference; + } + + public void setCurrentReference(T currentReference) { + this.currentReference = currentReference; + } + + public void removeCurrentReference() { + this.currentReference = null; + } + + public boolean isConstructing() { + return constructing; + } + + public void startConstruction() { + this.constructing = true; + } + + public void finishConstruction() { + this.constructing = false; + invocationHandlers = null; + } + + public Object createProxy(Errors errors, InjectorOptions injectorOptions, + Class expectedType) throws ErrorsException { + if (injectorOptions.disableCircularProxies) { + throw errors.circularProxiesDisabled(expectedType).toException(); + } + if (!expectedType.isInterface()) { + throw errors.cannotSatisfyCircularDependency(expectedType).toException(); + } + + if (invocationHandlers == null) { + invocationHandlers = new ArrayList>(); + } + + DelegatingInvocationHandler invocationHandler = new DelegatingInvocationHandler(); + invocationHandlers.add(invocationHandler); + + ClassLoader classLoader = expectedType.getClass().getClassLoader() != null ? + expectedType.getClass().getClassLoader() : ClassLoader.getSystemClassLoader(); + return expectedType.cast(Proxy.newProxyInstance(classLoader, + new Class[]{expectedType, CircularDependencyProxy.class}, invocationHandler)); + } + + public void setProxyDelegates(T delegate) { + if (invocationHandlers != null) { + for (DelegatingInvocationHandler handler : invocationHandlers) { + handler.setDelegate(delegate); + } + // initialization of each handler can happen no more than once + invocationHandlers = null; + } + } +} diff --git a/src/main/java/com/google/inject/internal/ConstructionProxy.java b/src/main/java/com/google/inject/internal/ConstructionProxy.java new file mode 100644 index 0000000..d4c0f09 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstructionProxy.java @@ -0,0 +1,30 @@ +package com.google.inject.internal; + +import com.google.inject.spi.InjectionPoint; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Proxies calls to a {@link java.lang.reflect.Constructor} for a class + * {@code T}. + */ +interface ConstructionProxy { + + /** + * Constructs an instance of {@code T} for the given arguments. + */ + T newInstance(Object... arguments) throws InvocationTargetException; + + /** + * Returns the injection point for this constructor. + */ + InjectionPoint getInjectionPoint(); + + /** + * Returns the injected constructor. If the injected constructor is synthetic (such as generated + * code for method interception), the natural constructor is returned. + */ + Constructor getConstructor(); + +} diff --git a/src/main/java/com/google/inject/internal/ConstructionProxyFactory.java b/src/main/java/com/google/inject/internal/ConstructionProxyFactory.java new file mode 100644 index 0000000..5151326 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstructionProxyFactory.java @@ -0,0 +1,12 @@ +package com.google.inject.internal; + +/** + * Creates {@link ConstructionProxy} instances. + */ +interface ConstructionProxyFactory { + + /** + * Gets a construction proxy for the given constructor. + */ + ConstructionProxy create() throws ErrorsException; +} diff --git a/src/main/java/com/google/inject/internal/ConstructorBindingImpl.java b/src/main/java/com/google/inject/internal/ConstructorBindingImpl.java new file mode 100644 index 0000000..d52f486 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstructorBindingImpl.java @@ -0,0 +1,258 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.util.Classes; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.ConstructorBinding; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.InjectionPoint; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.inject.internal.Annotations.findScopeAnnotation; + +final class ConstructorBindingImpl extends BindingImpl + implements ConstructorBinding, DelayedInitialize { + + private final Factory factory; + private final InjectionPoint constructorInjectionPoint; + + private ConstructorBindingImpl(InjectorImpl injector, Key key, Object source, + InternalFactory scopedFactory, Scoping scoping, Factory factory, + InjectionPoint constructorInjectionPoint) { + super(injector, key, source, scopedFactory, scoping); + this.factory = factory; + this.constructorInjectionPoint = constructorInjectionPoint; + } + + public ConstructorBindingImpl(Key key, Object source, Scoping scoping, + InjectionPoint constructorInjectionPoint, Set injectionPoints) { + super(source, key, scoping); + this.factory = new Factory(false, key); + ConstructionProxy constructionProxy + = new DefaultConstructionProxyFactory(constructorInjectionPoint).create(); + this.constructorInjectionPoint = constructorInjectionPoint; + factory.constructorInjector = new ConstructorInjector( + injectionPoints, constructionProxy, null, null); + } + + /** + * @param constructorInjector the constructor to use, or {@code null} to use the default. + * @param failIfNotLinked true if this ConstructorBindingImpl's InternalFactory should + * only succeed if retrieved from a linked binding + */ + static ConstructorBindingImpl create(InjectorImpl injector, Key key, + InjectionPoint constructorInjector, Object source, Scoping scoping, Errors errors, + boolean failIfNotLinked, boolean failIfNotExplicit) + throws ErrorsException { + int numErrors = errors.size(); + + @SuppressWarnings("unchecked") // constructorBinding guarantees type is consistent + Class rawType = constructorInjector == null + ? key.getTypeLiteral().getRawType() + : (Class) constructorInjector.getDeclaringType().getRawType(); + + // We can't inject abstract classes. + if (Modifier.isAbstract(rawType.getModifiers())) { + errors.missingImplementation(key); + } + + // Error: Inner class. + if (Classes.isInnerClass(rawType)) { + errors.cannotInjectInnerClass(rawType); + } + + errors.throwIfNewErrors(numErrors); + + // Find a constructor annotated @Inject + if (constructorInjector == null) { + try { + constructorInjector = InjectionPoint.forConstructorOf(key.getTypeLiteral()); + if (failIfNotExplicit && !hasAtInject((Constructor) constructorInjector.getMember())) { + errors.atInjectRequired(rawType); + } + } catch (ConfigurationException e) { + throw errors.merge(e.getErrorMessages()).toException(); + } + } + + // if no scope is specified, look for a scoping annotation on the concrete class + if (!scoping.isExplicitlyScoped()) { + Class annotatedType = constructorInjector.getMember().getDeclaringClass(); + Class scopeAnnotation = findScopeAnnotation(errors, annotatedType); + if (scopeAnnotation != null) { + scoping = Scoping.makeInjectable(Scoping.forAnnotation(scopeAnnotation), + injector, errors.withSource(rawType)); + } + } + + errors.throwIfNewErrors(numErrors); + + Factory factoryFactory = new Factory(failIfNotLinked, key); + InternalFactory scopedFactory + = Scoping.scope(key, injector, factoryFactory, source, scoping); + + return new ConstructorBindingImpl( + injector, key, source, scopedFactory, scoping, factoryFactory, constructorInjector); + } + + /** + * Returns true if the inject annotation is on the constructor. + */ + private static boolean hasAtInject(Constructor cxtor) { + return cxtor.isAnnotationPresent(Inject.class) + || cxtor.isAnnotationPresent(javax.inject.Inject.class); + } + + @SuppressWarnings("unchecked") // the result type always agrees with the ConstructorInjector type + public void initialize(InjectorImpl injector, Errors errors) throws ErrorsException { + factory.constructorInjector = + (ConstructorInjector) injector.constructors.get(constructorInjectionPoint, errors); + factory.provisionCallback = + injector.provisionListenerStore.get(this); + } + + /** + * True if this binding has been initialized and is ready for use. + */ + boolean isInitialized() { + return factory.constructorInjector != null; + } + + /** + * Returns an injection point that can be used to clean up the constructor store. + */ + InjectionPoint getInternalConstructor() { + if (factory.constructorInjector != null) { + return factory.constructorInjector.getConstructionProxy().getInjectionPoint(); + } else { + return constructorInjectionPoint; + } + } + + /** + * Returns a set of dependencies that can be iterated over to clean up stray JIT bindings. + */ + Set> getInternalDependencies() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + if (factory.constructorInjector == null) { + builder.add(constructorInjectionPoint); + // If the below throws, it's OK -- we just ignore those dependencies, because no one + // could have used them anyway. + try { + builder.addAll(InjectionPoint.forInstanceMethodsAndFields(constructorInjectionPoint.getDeclaringType())); + } catch (ConfigurationException ignored) { + } + } else { + builder.add(getConstructor()) + .addAll(getInjectableMembers()); + } + + return Dependency.forInjectionPoints(builder.build()); + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + checkState(factory.constructorInjector != null, "not initialized"); + return visitor.visit(this); + } + + public InjectionPoint getConstructor() { + checkState(factory.constructorInjector != null, "Binding is not ready"); + return factory.constructorInjector.getConstructionProxy().getInjectionPoint(); + } + + public Set getInjectableMembers() { + checkState(factory.constructorInjector != null, "Binding is not ready"); + return factory.constructorInjector.getInjectableMembers(); + } + + public Set> getDependencies() { + return Dependency.forInjectionPoints(new ImmutableSet.Builder() + .add(getConstructor()) + .addAll(getInjectableMembers()) + .build()); + } + + @Override + protected BindingImpl withScoping(Scoping scoping) { + return new ConstructorBindingImpl( + null, getKey(), getSource(), factory, scoping, factory, constructorInjectionPoint); + } + + @Override + protected BindingImpl withKey(Key key) { + return new ConstructorBindingImpl( + null, key, getSource(), factory, getScoping(), factory, constructorInjectionPoint); + } + + @SuppressWarnings("unchecked") // the raw constructor member and declaring type always agree + public void applyTo(Binder binder) { + InjectionPoint constructor = getConstructor(); + getScoping().applyTo(binder.withSource(getSource()).bind(getKey()).toConstructor( + (Constructor) getConstructor().getMember(), (TypeLiteral) constructor.getDeclaringType())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ConstructorBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .add("scope", getScoping()) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ConstructorBindingImpl) { + ConstructorBindingImpl o = (ConstructorBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(constructorInjectionPoint, o.constructorInjectionPoint); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping(), constructorInjectionPoint); + } + + private static class Factory implements InternalFactory { + private final boolean failIfNotLinked; + private final Key key; + private ConstructorInjector constructorInjector; + private ProvisionListenerStackCallback provisionCallback; + + Factory(boolean failIfNotLinked, Key key) { + this.failIfNotLinked = failIfNotLinked; + this.key = key; + } + + @SuppressWarnings("unchecked") + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + checkState(constructorInjector != null, "Constructor not ready"); + + if (failIfNotLinked && !linked) { + throw errors.jitDisabled(key).toException(); + } + + // This may not actually be safe because it could return a super type of T (if that's all the + // client needs), but it should be OK in practice thanks to the wonders of erasure. + return (T) constructorInjector.construct(errors, context, + dependency.getKey().getTypeLiteral().getRawType(), provisionCallback); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ConstructorInjector.java b/src/main/java/com/google/inject/internal/ConstructorInjector.java new file mode 100644 index 0000000..127f296 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstructorInjector.java @@ -0,0 +1,112 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.internal.ProvisionListenerStackCallback.ProvisionCallback; +import com.google.inject.spi.InjectionPoint; + +import java.lang.reflect.InvocationTargetException; +import java.util.Set; + +/** + * Creates instances using an injectable constructor. After construction, all injectable fields and + * methods are injected. + */ +final class ConstructorInjector { + + private final ImmutableSet injectableMembers; + private final SingleParameterInjector[] parameterInjectors; + private final ConstructionProxy constructionProxy; + private final MembersInjectorImpl membersInjector; + + ConstructorInjector(Set injectableMembers, + ConstructionProxy constructionProxy, + SingleParameterInjector[] parameterInjectors, + MembersInjectorImpl membersInjector) { + this.injectableMembers = ImmutableSet.copyOf(injectableMembers); + this.constructionProxy = constructionProxy; + this.parameterInjectors = parameterInjectors; + this.membersInjector = membersInjector; + } + + public ImmutableSet getInjectableMembers() { + return injectableMembers; + } + + ConstructionProxy getConstructionProxy() { + return constructionProxy; + } + + /** + * Construct an instance. Returns {@code Object} instead of {@code T} because + * it may return a proxy. + */ + Object construct(final Errors errors, final InternalContext context, + Class expectedType, + ProvisionListenerStackCallback provisionCallback) + throws ErrorsException { + final ConstructionContext constructionContext = context.getConstructionContext(this); + + // We have a circular reference between constructors. Return a proxy. + if (constructionContext.isConstructing()) { + // TODO (crazybob): if we can't proxy this object, can we proxy the other object? + return constructionContext.createProxy( + errors, context.getInjectorOptions(), expectedType); + } + + // If we're re-entering this factory while injecting fields or methods, + // return the same instance. This prevents infinite loops. + T t = constructionContext.getCurrentReference(); + if (t != null) { + return t; + } + + constructionContext.startConstruction(); + try { + // Optimization: Don't go through the callback stack if we have no listeners. + if (!provisionCallback.hasListeners()) { + return provision(errors, context, constructionContext); + } else { + return provisionCallback.provision(errors, context, new ProvisionCallback() { + public T call() throws ErrorsException { + return provision(errors, context, constructionContext); + } + }); + } + } finally { + constructionContext.finishConstruction(); + } + } + + /** + * Provisions a new T. + */ + private T provision(Errors errors, InternalContext context, + ConstructionContext constructionContext) throws ErrorsException { + try { + T t; + try { + Object[] parameters = SingleParameterInjector.getAll(errors, context, parameterInjectors); + t = constructionProxy.newInstance(parameters); + constructionContext.setProxyDelegates(t); + } finally { + constructionContext.finishConstruction(); + } + + // Store reference. If an injector re-enters this factory, they'll get the same reference. + constructionContext.setCurrentReference(t); + + membersInjector.injectMembers(t, errors, context, false); + membersInjector.notifyListeners(t, errors); + + return t; + } catch (InvocationTargetException userException) { + Throwable cause = userException.getCause() != null + ? userException.getCause() + : userException; + throw errors.withSource(constructionProxy.getInjectionPoint()) + .errorInjectingConstructor(cause).toException(); + } finally { + constructionContext.removeCurrentReference(); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ConstructorInjectorStore.java b/src/main/java/com/google/inject/internal/ConstructorInjectorStore.java new file mode 100644 index 0000000..7060654 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ConstructorInjectorStore.java @@ -0,0 +1,63 @@ +package com.google.inject.internal; + +import com.google.inject.spi.InjectionPoint; + +/** + * Constructor injectors by type. + */ +final class ConstructorInjectorStore { + private final InjectorImpl injector; + + private final FailableCache> cache + = new FailableCache>() { + @Override + protected ConstructorInjector create(InjectionPoint constructorInjector, Errors errors) + throws ErrorsException { + return createConstructor(constructorInjector, errors); + } + }; + + ConstructorInjectorStore(InjectorImpl injector) { + this.injector = injector; + } + + /** + * Returns a new complete constructor injector with injection listeners registered. + */ + public ConstructorInjector get(InjectionPoint constructorInjector, Errors errors) + throws ErrorsException { + return cache.get(constructorInjector, errors); + } + + /** + * Purges an injection point from the cache. Use this only if the cache is not actually valid and + * needs to be purged. (See issue 319 and + * ImplicitBindingTest#testCircularJitBindingsLeaveNoResidue and + * #testInstancesRequestingProvidersForThemselvesWithChildInjectors for examples of when this is + * necessary.) + * + * Returns true if the injector for that point was stored in the cache, false otherwise. + */ + boolean remove(InjectionPoint ip) { + return cache.remove(ip); + } + + private ConstructorInjector createConstructor(InjectionPoint injectionPoint, Errors errors) + throws ErrorsException { + int numErrorsBefore = errors.size(); + + SingleParameterInjector[] constructorParameterInjectors + = injector.getParametersInjectors(injectionPoint.getDependencies(), errors); + + @SuppressWarnings("unchecked") // the injector type agrees with the injection point type + MembersInjectorImpl membersInjector = (MembersInjectorImpl) injector.membersInjectorStore + .get(injectionPoint.getDeclaringType(), errors); + + ConstructionProxyFactory factory = new DefaultConstructionProxyFactory(injectionPoint); + + errors.throwIfNewErrors(numErrorsBefore); + + return new ConstructorInjector(membersInjector.getInjectionPoints(), factory.create(), + constructorParameterInjectors, membersInjector); + } +} diff --git a/src/main/java/com/google/inject/internal/ContextualCallable.java b/src/main/java/com/google/inject/internal/ContextualCallable.java new file mode 100644 index 0000000..d67b8b7 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ContextualCallable.java @@ -0,0 +1,5 @@ +package com.google.inject.internal; + +interface ContextualCallable { + T call(InternalContext context) throws ErrorsException; +} diff --git a/src/main/java/com/google/inject/internal/CreationListener.java b/src/main/java/com/google/inject/internal/CreationListener.java new file mode 100644 index 0000000..104a2d8 --- /dev/null +++ b/src/main/java/com/google/inject/internal/CreationListener.java @@ -0,0 +1,12 @@ +package com.google.inject.internal; + +/** + * Something that is notified upon creation. + */ +interface CreationListener { + + /** + * Notifies that creation should happen. + */ + void notify(Errors errors); +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/CycleDetectingLock.java b/src/main/java/com/google/inject/internal/CycleDetectingLock.java new file mode 100644 index 0000000..ef3a605 --- /dev/null +++ b/src/main/java/com/google/inject/internal/CycleDetectingLock.java @@ -0,0 +1,308 @@ +package com.google.inject.internal; + +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Simplified version of {@link Lock} that is special due to how it handles deadlocks detection. + * + *

Is an inherent part of {@link SingletonScope}, moved into a upper level class due + * to its size and complexity. + * + * @param Lock identification provided by the client, is returned unmodified to the client + * when lock cycle is detected to identify it. Only toString() needs to be implemented. + * Lock references this object internally, + * for the purposes of Garbage Collection you should not use heavy IDs. + * Lock is referenced by a lock factory as long as it's owned by a thread. + */ +interface CycleDetectingLock { + + /** + * Takes a lock in a blocking fashion in case no potential deadlocks are detected. + * If the lock was successfully owned, returns an empty map indicating no detected potential + * deadlocks. + * + * Otherwise, a map indicating threads involved in a potential deadlock are returned. + * Map is ordered by dependency cycle and lists locks for each thread that are part of + * the loop in order. Returned map is created atomically. + * + * In case no cycle is detected performance is O(threads creating singletons), + * in case cycle is detected performance is O(singleton locks). + */ + ListMultimap lockOrDetectPotentialLocksCycle(); + + /** + * Unlocks previously locked lock. + */ + void unlock(); + + /** + * Wraps locks so they would never cause a deadlock. On each + * {@link CycleDetectingLock#lockOrDetectPotentialLocksCycle} we check for dependency cycles + * within locks created by the same factory. Either we detect a cycle and return it + * or take it atomically. + * + *

Important to note that we do not prevent deadlocks in the client code. As an example: + * Thread A takes lock L and creates singleton class CA depending on the singleton class CB. + * Meanwhile thread B is creating class CB and is waiting on the lock L. Issue happens + * due to client code creating interdependent classes and using locks, where + * no guarantees on the creation order from Guice are provided. + * + *

Instances of these locks are not intended to be exposed outside of {@link SingletonScope}. + */ + class CycleDetectingLockFactory { + + /** + * Lists locks that thread owns. + * Used only to populate locks in a potential cycle when it is detected. + * + * Key: thread id + * Value: stack of locks that were owned. + * + * Element is added inside {@link #lockOrDetectPotentialLocksCycle()} after {@link Lock#lock} + * is called. Element is removed inside {@link #unlock()} synchronously with + * {@link Lock#unlock()} call. + * + * Same lock can only be present several times for the same thread as locks are + * reentrant. Lock can not be owned by several different threads as the same time. + * + * Guarded by {@code this}. + */ + private final Multimap locksOwnedByThread = + LinkedHashMultimap.create(); + /** + * Specifies lock that thread is currently waiting on to own it. + * Used only for purposes of locks cycle detection. + * + * Key: thread id + * Value: lock that is being waited on + * + * Element is added inside {@link #lockOrDetectPotentialLocksCycle()} before {@link Lock#lock} + * is called. Element is removed inside {@link #lockOrDetectPotentialLocksCycle()} after + * {@link Lock#lock} and synchronously with adding it to {@link #locksOwnedByThread}. + * + * Same lock can be added for several threads in case all of them are trying to + * take it. + * + * Guarded by {@code this}. + */ + private Map lockThreadIsWaitingOn = Maps.newHashMap(); + + /** + * Creates new lock within this factory context. We can guarantee that locks created by + * the same factory would not deadlock. + * + * @param newLockId lock id that would be used to report lock cycles if detected + */ + CycleDetectingLock create(ID newLockId) { + return new ReentrantCycleDetectingLock(newLockId, new ReentrantLock()); + } + + /** + * The implementation for {@link CycleDetectingLock}. + */ + class ReentrantCycleDetectingLock implements CycleDetectingLock { + + /** + * Underlying lock used for actual waiting when no potential deadlocks are detected. + */ + private final Lock lockImplementation; + /** + * User id for this lock. + */ + private final ID userLockId; + /** + * Thread id for the thread that owned this lock. Nullable. + * Guarded by {@code CycleDetectingLockFactory.this}. + */ + private Long lockOwnerThreadId = null; + /** + * Number of times that thread owned this lock. + * Guarded by {@code CycleDetectingLockFactory.this}. + */ + private int lockReentranceCount = 0; + + ReentrantCycleDetectingLock(ID userLockId, Lock lockImplementation) { + this.userLockId = Preconditions.checkNotNull(userLockId, "userLockId"); + this.lockImplementation = Preconditions.checkNotNull( + lockImplementation, "lockImplementation"); + } + + @Override + public ListMultimap lockOrDetectPotentialLocksCycle() { + final long currentThreadId = Thread.currentThread().getId(); + synchronized (CycleDetectingLockFactory.this) { + checkState(); + ListMultimap locksInCycle = detectPotentialLocksCycle(); + if (!locksInCycle.isEmpty()) { + // potential deadlock is found, we don't try to take this lock + return locksInCycle; + } + + lockThreadIsWaitingOn.put(currentThreadId, this); + } + + // this may be blocking, but we don't expect it to cause a deadlock + lockImplementation.lock(); + + synchronized (CycleDetectingLockFactory.this) { + // current thread is no longer waiting on this lock + lockThreadIsWaitingOn.remove(currentThreadId); + checkState(); + + // mark it as owned by us + lockOwnerThreadId = currentThreadId; + lockReentranceCount++; + // add this lock to the list of locks owned by a current thread + locksOwnedByThread.put(currentThreadId, this); + } + // no deadlock is found, locking successful + return ImmutableListMultimap.of(); + } + + @Override + public void unlock() { + final long currentThreadId = Thread.currentThread().getId(); + synchronized (CycleDetectingLockFactory.this) { + checkState(); + Preconditions.checkState(lockOwnerThreadId != null, + "Thread is trying to unlock a lock that is not locked"); + Preconditions.checkState(lockOwnerThreadId == currentThreadId, + "Thread is trying to unlock a lock owned by another thread"); + + // releasing underlying lock + lockImplementation.unlock(); + + // be sure to release the lock synchronously with updating internal state + lockReentranceCount--; + if (lockReentranceCount == 0) { + // we no longer own this lock + lockOwnerThreadId = null; + Preconditions.checkState(locksOwnedByThread.remove(currentThreadId, this), + "Internal error: Can not find this lock in locks owned by a current thread"); + if (locksOwnedByThread.get(currentThreadId).isEmpty()) { + // clearing memory + locksOwnedByThread.removeAll(currentThreadId); + } + } + } + } + + /** + * Check consistency of an internal state. + */ + void checkState() throws IllegalStateException { + final long currentThreadId = Thread.currentThread().getId(); + Preconditions.checkState(!lockThreadIsWaitingOn.containsKey(currentThreadId), + "Internal error: Thread should not be in a waiting thread on a lock now"); + if (lockOwnerThreadId != null) { + // check state of a locked lock + Preconditions.checkState(lockReentranceCount >= 0, + "Internal error: Lock ownership and reentrance count internal states do not match"); + Preconditions.checkState(locksOwnedByThread.get(lockOwnerThreadId).contains(this), + "Internal error: Set of locks owned by a current thread and lock " + + "ownership status do not match"); + } else { + // check state of a non locked lock + Preconditions.checkState(lockReentranceCount == 0, + "Internal error: Reentrance count of a non locked lock is expect to be zero"); + Preconditions.checkState(!locksOwnedByThread.values().contains(this), + "Internal error: Non locked lock should not be owned by any thread"); + } + } + + /** + * Algorithm to detect a potential lock cycle. + * + * For lock's thread owner check which lock is it trying to take. + * Repeat recursively. When current thread is found a potential cycle is detected. + * + * @see CycleDetectingLock#lockOrDetectPotentialLocksCycle() + */ + private ListMultimap detectPotentialLocksCycle() { + final long currentThreadId = Thread.currentThread().getId(); + if (lockOwnerThreadId == null || lockOwnerThreadId == currentThreadId) { + // if nobody owns this lock, lock cycle is impossible + // if a current thread owns this lock, we let Guice to handle it + return ImmutableListMultimap.of(); + } + + ListMultimap potentialLocksCycle = Multimaps.newListMultimap( + new LinkedHashMap>(), + new Supplier>() { + @Override + public List get() { + return Lists.newArrayList(); + } + }); + // lock that is a part of a potential locks cycle, starts with current lock + ReentrantCycleDetectingLock lockOwnerWaitingOn = this; + // try to find a dependency path between lock's owner thread and a current thread + while (lockOwnerWaitingOn != null && lockOwnerWaitingOn.lockOwnerThreadId != null) { + Long threadOwnerThreadWaits = lockOwnerWaitingOn.lockOwnerThreadId; + // in case locks cycle exists lock we're waiting for is part of it + potentialLocksCycle.putAll(threadOwnerThreadWaits, + getAllLockIdsAfter(threadOwnerThreadWaits, lockOwnerWaitingOn)); + + if (threadOwnerThreadWaits == currentThreadId) { + // owner thread depends on current thread, cycle detected + return potentialLocksCycle; + } + // going for the next thread we wait on indirectly + lockOwnerWaitingOn = lockThreadIsWaitingOn.get(threadOwnerThreadWaits); + } + // no dependency path from an owner thread to a current thread + return ImmutableListMultimap.of(); + } + + /** + * Return locks owned by a thread after a lock specified, inclusive. + */ + private List getAllLockIdsAfter(long threadId, ReentrantCycleDetectingLock lock) { + List ids = Lists.newArrayList(); + boolean found = false; + Collection ownedLocks = locksOwnedByThread.get(threadId); + Preconditions.checkNotNull(ownedLocks, + "Internal error: No locks were found taken by a thread"); + for (ReentrantCycleDetectingLock ownedLock : ownedLocks) { + if (ownedLock == lock) { + found = true; + } + if (found) { + ids.add(ownedLock.userLockId); + } + } + Preconditions.checkState(found, "Internal error: We can not find locks that " + + "created a cycle that we detected"); + return ids; + } + + @Override + public String toString() { + // copy is made to prevent a data race + // no synchronization is used, potentially stale data, should be good enough + Long localLockOwnerThreadId = this.lockOwnerThreadId; + if (localLockOwnerThreadId != null) { + return String.format("CycleDetectingLock[%s][locked by %s]", + userLockId, localLockOwnerThreadId); + } else { + return String.format("CycleDetectingLock[%s][unlocked]", userLockId); + } + } + } + } +} diff --git a/src/main/java/com/google/inject/internal/DefaultConstructionProxyFactory.java b/src/main/java/com/google/inject/internal/DefaultConstructionProxyFactory.java new file mode 100644 index 0000000..12f19cd --- /dev/null +++ b/src/main/java/com/google/inject/internal/DefaultConstructionProxyFactory.java @@ -0,0 +1,57 @@ +package com.google.inject.internal; + +import com.google.inject.spi.InjectionPoint; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; + +/** + * Produces construction proxies that invoke the class constructor. + */ +final class DefaultConstructionProxyFactory implements ConstructionProxyFactory { + + private final InjectionPoint injectionPoint; + + /** + * @param injectionPoint an injection point whose member is a constructor of {@code T}. + */ + DefaultConstructionProxyFactory(InjectionPoint injectionPoint) { + this.injectionPoint = injectionPoint; + } + + public ConstructionProxy create() { + @SuppressWarnings("unchecked") // the injection point is for a constructor of T + final Constructor constructor = (Constructor) injectionPoint.getMember(); + + // Use FastConstructor if the constructor is public. + if (Modifier.isPublic(constructor.getModifiers())) { + Class classToConstruct = constructor.getDeclaringClass(); + if (!Modifier.isPublic(classToConstruct.getModifiers())) { + constructor.setAccessible(true); + } + } else { + constructor.setAccessible(true); + } + + return new ConstructionProxy() { + public T newInstance(Object... arguments) throws InvocationTargetException { + try { + return constructor.newInstance(arguments); + } catch (InstantiationException e) { + throw new AssertionError(e); // shouldn't happen, we know this is a concrete type + } catch (IllegalAccessException e) { + throw new AssertionError(e); // a security manager is blocking us, we're hosed + } + } + + public InjectionPoint getInjectionPoint() { + return injectionPoint; + } + + public Constructor getConstructor() { + return constructor; + } + }; + } +} diff --git a/src/main/java/com/google/inject/internal/DeferredLookups.java b/src/main/java/com/google/inject/internal/DeferredLookups.java new file mode 100644 index 0000000..ee3c021 --- /dev/null +++ b/src/main/java/com/google/inject/internal/DeferredLookups.java @@ -0,0 +1,45 @@ +package com.google.inject.internal; + +import com.google.common.collect.Lists; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Element; +import com.google.inject.spi.MembersInjectorLookup; +import com.google.inject.spi.ProviderLookup; + +import java.util.List; + +/** + * Returns providers and members injectors that haven't yet been initialized. As a part of injector + * creation it's necessary to {@link #initialize initialize} these lookups. + */ +final class DeferredLookups implements Lookups { + private final InjectorImpl injector; + private final List lookups = Lists.newArrayList(); + + DeferredLookups(InjectorImpl injector) { + this.injector = injector; + } + + /** + * Initialize the specified lookups, either immediately or when the injector is created. + */ + void initialize(Errors errors) { + injector.lookups = injector; + new LookupProcessor(errors).process(injector, lookups); + } + + public Provider getProvider(Key key) { + ProviderLookup lookup = new ProviderLookup(key, key); + lookups.add(lookup); + return lookup.getProvider(); + } + + public MembersInjector getMembersInjector(TypeLiteral type) { + MembersInjectorLookup lookup = new MembersInjectorLookup(type, type); + lookups.add(lookup); + return lookup.getMembersInjector(); + } +} diff --git a/src/main/java/com/google/inject/internal/DelayedInitialize.java b/src/main/java/com/google/inject/internal/DelayedInitialize.java new file mode 100644 index 0000000..3fb3f7b --- /dev/null +++ b/src/main/java/com/google/inject/internal/DelayedInitialize.java @@ -0,0 +1,15 @@ +package com.google.inject.internal; + +/** + * Something that needs some delayed initialization, typically + * a binding or internal factory that needs to be created & put + * into the bindings map & then initialized later. + */ +interface DelayedInitialize { + + /** + * Initializes this binding, throwing any errors if necessary. + */ + void initialize(InjectorImpl injector, Errors errors) throws ErrorsException; + +} diff --git a/src/main/java/com/google/inject/internal/DelegatingInvocationHandler.java b/src/main/java/com/google/inject/internal/DelegatingInvocationHandler.java new file mode 100644 index 0000000..9edd669 --- /dev/null +++ b/src/main/java/com/google/inject/internal/DelegatingInvocationHandler.java @@ -0,0 +1,44 @@ +package com.google.inject.internal; + +import com.google.common.base.Preconditions; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class DelegatingInvocationHandler implements InvocationHandler { + + private volatile boolean initialized; + + private T delegate; + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + try { + // checking volatile field for synchronization + Preconditions.checkState(initialized, + "This is a proxy used to support" + + " circular references. The object we're" + + " proxying is not constructed yet. Please wait until after" + + " injection has completed to use this object."); + Preconditions.checkNotNull(delegate, + "This is a proxy used to support" + + " circular references. The object we're " + + " proxying is initialized to null." + + " No methods can be called."); + + return method.invoke(delegate, args); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + + void setDelegate(T delegate) { + this.delegate = delegate; + initialized = true; + } +} diff --git a/src/main/java/com/google/inject/internal/EncounterImpl.java b/src/main/java/com/google/inject/internal/EncounterImpl.java new file mode 100644 index 0000000..9a2824a --- /dev/null +++ b/src/main/java/com/google/inject/internal/EncounterImpl.java @@ -0,0 +1,98 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.Message; +import com.google.inject.spi.TypeEncounter; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +final class EncounterImpl implements TypeEncounter { + + private final Errors errors; + private final Lookups lookups; + private List> membersInjectors; // lazy + private List> injectionListeners; // lazy + private boolean valid = true; + + EncounterImpl(Errors errors, Lookups lookups) { + this.errors = errors; + this.lookups = lookups; + } + + void invalidate() { + valid = false; + } + + ImmutableSet> getMembersInjectors() { + return membersInjectors == null + ? ImmutableSet.>of() + : ImmutableSet.copyOf(membersInjectors); + } + + ImmutableSet> getInjectionListeners() { + return injectionListeners == null + ? ImmutableSet.>of() + : ImmutableSet.copyOf(injectionListeners); + } + + public void register(MembersInjector membersInjector) { + checkState(valid, "Encounters may not be used after hear() returns."); + + if (membersInjectors == null) { + membersInjectors = Lists.newArrayList(); + } + + membersInjectors.add(membersInjector); + } + + public void register(InjectionListener injectionListener) { + checkState(valid, "Encounters may not be used after hear() returns."); + + if (injectionListeners == null) { + injectionListeners = Lists.newArrayList(); + } + + injectionListeners.add(injectionListener); + } + + public void addError(String message, Object... arguments) { + checkState(valid, "Encounters may not be used after hear() returns."); + errors.addMessage(message, arguments); + } + + public void addError(Throwable t) { + checkState(valid, "Encounters may not be used after hear() returns."); + errors.errorInUserCode(t, "An exception was caught and reported. Message: %s", t.getMessage()); + } + + public void addError(Message message) { + checkState(valid, "Encounters may not be used after hear() returns."); + errors.addMessage(message); + } + + public Provider getProvider(Key key) { + checkState(valid, "Encounters may not be used after hear() returns."); + return lookups.getProvider(key); + } + + public Provider getProvider(Class type) { + return getProvider(Key.get(type)); + } + + public MembersInjector getMembersInjector(TypeLiteral typeLiteral) { + checkState(valid, "Encounters may not be used after hear() returns."); + return lookups.getMembersInjector(typeLiteral); + } + + public MembersInjector getMembersInjector(Class type) { + return getMembersInjector(TypeLiteral.get(type)); + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/Errors.java b/src/main/java/com/google/inject/internal/Errors.java new file mode 100644 index 0000000..0a61fc6 --- /dev/null +++ b/src/main/java/com/google/inject/internal/Errors.java @@ -0,0 +1,817 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.inject.ConfigurationException; +import com.google.inject.CreationException; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.Scope; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.util.Classes; +import com.google.inject.internal.util.SourceProvider; +import com.google.inject.internal.util.StackTraceElements; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.ElementSource; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.Message; +import com.google.inject.spi.ScopeBinding; +import com.google.inject.spi.TypeConverterBinding; +import com.google.inject.spi.TypeListenerBinding; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.Formatter; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A collection of error messages. If this type is passed as a method parameter, the method is + * considered to have executed successfully only if new errors were not added to this collection. + * + *

Errors can be chained to provide additional context. To add context, call {@link #withSource} + * to create a new Errors instance that contains additional context. All messages added to the + * returned instance will contain full context. + * + *

To avoid messages with redundant context, {@link #withSource} should be added sparingly. A + * good rule of thumb is to assume a method's caller has already specified enough context to + * identify that method. When calling a method that's defined in a different context, call that + * method with an errors object that includes its context. + * + */ +public final class Errors { + + private static final Set> warnedDependencies = + Collections.newSetFromMap(new ConcurrentHashMap, Boolean>()); + private static final String CONSTRUCTOR_RULES = + "Classes must have either one (and only one) constructor " + + "annotated with @Inject or a zero-argument constructor that is not private."; + private static final Collection> converters = ImmutableList.of( + new Converter(Class.class) { + @Override + public String toString(Class c) { + return c.getName(); + } + }, + new Converter(Member.class) { + @Override + public String toString(Member member) { + return Classes.toString(member); + } + }, + new Converter(Key.class) { + @Override + public String toString(Key key) { + if (key.getAnnotationType() != null) { + return key.getTypeLiteral() + " annotated with " + + (key.getAnnotation() != null ? key.getAnnotation() : key.getAnnotationType()); + } else { + return key.getTypeLiteral().toString(); + } + } + }); + /** + * The root errors object. Used to access the list of error messages. + */ + private final Errors root; + /** + * The parent errors object. Used to obtain the chain of source objects. + */ + private final Errors parent; + /** + * The leaf source for errors added here. + */ + private final Object source; + /** + * null unless (root == this) and error messages exist. Never an empty list. + */ + private List errors; // lazy, use getErrorsForAdd() + + public Errors() { + this.root = this; + this.parent = null; + this.source = SourceProvider.UNKNOWN_SOURCE; + } + + public Errors(Object source) { + this.root = this; + this.parent = null; + this.source = source; + } + + private Errors(Errors parent, Object source) { + this.root = parent.root; + this.parent = parent; + this.source = source; + } + + public static Collection getMessagesFromThrowable(Throwable throwable) { + if (throwable instanceof ProvisionException) { + return ((ProvisionException) throwable).getErrorMessages(); + } else if (throwable instanceof ConfigurationException) { + return ((ConfigurationException) throwable).getErrorMessages(); + } else if (throwable instanceof CreationException) { + return ((CreationException) throwable).getErrorMessages(); + } else { + return ImmutableSet.of(); + } + } + + public static String format(String messageFormat, Object... arguments) { + for (int i = 0; i < arguments.length; i++) { + arguments[i] = Errors.convert(arguments[i]); + } + return String.format(messageFormat, arguments); + } + + /** + * Returns the formatted message for an exception with the specified messages. + */ + public static String format(String heading, Collection errorMessages) { + Formatter fmt = new Formatter().format(heading).format(":%n%n"); + int index = 1; + boolean displayCauses = getOnlyCause(errorMessages) == null; + + for (Message errorMessage : errorMessages) { + fmt.format("%s) %s%n", index++, errorMessage.getMessage()); + + List dependencies = errorMessage.getSources(); + for (int i = dependencies.size() - 1; i >= 0; i--) { + Object source = dependencies.get(i); + formatSource(fmt, source); + } + + Throwable cause = errorMessage.getCause(); + if (displayCauses && cause != null) { + StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + fmt.format("Caused by: %s", writer.getBuffer()); + } + + fmt.format("%n"); + } + + if (errorMessages.size() == 1) { + fmt.format("1 error"); + } else { + fmt.format("%s errors", errorMessages.size()); + } + + return fmt.toString(); + } + + /** + * Returns the cause throwable if there is exactly one cause in {@code messages}. If there are + * zero or multiple messages with causes, null is returned. + */ + public static Throwable getOnlyCause(Collection messages) { + Throwable onlyCause = null; + for (Message message : messages) { + Throwable messageCause = message.getCause(); + if (messageCause == null) { + continue; + } + + if (onlyCause != null) { + return null; + } + + onlyCause = messageCause; + } + + return onlyCause; + } + + public static Object convert(Object o) { + ElementSource source = null; + if (o instanceof ElementSource) { + source = (ElementSource) o; + o = source.getDeclaringSource(); + } + return convert(o, source); + } + + public static Object convert(Object o, ElementSource source) { + for (Converter converter : converters) { + if (converter.appliesTo(o)) { + return appendModules(converter.convert(o), source); + } + } + return appendModules(o, source); + } + + private static Object appendModules(Object source, ElementSource elementSource) { + String modules = moduleSourceString(elementSource); + if (modules.length() == 0) { + return source; + } else { + return source + modules; + } + } + + private static String moduleSourceString(ElementSource elementSource) { + // if we only have one module (or don't know what they are), then don't bother + // reporting it, because the source already is going to report exactly that module. + if (elementSource == null) { + return ""; + } + List modules = Lists.newArrayList(elementSource.getModuleClassNames()); + // Insert any original element sources w/ module info into the path. + while (elementSource.getOriginalElementSource() != null) { + elementSource = elementSource.getOriginalElementSource(); + modules.addAll(0, elementSource.getModuleClassNames()); + } + if (modules.size() <= 1) { + return ""; + } + + // Ideally we'd do: + // return Joiner.on(" -> ") + // .appendTo(new StringBuilder(" (via modules: "), Lists.reverse(modules)) + // .append(")").toString(); + // ... but for some reason we can't find Lists.reverse, so do it the boring way. + StringBuilder builder = new StringBuilder(" (via modules: "); + for (int i = modules.size() - 1; i >= 0; i--) { + builder.append(modules.get(i)); + if (i != 0) { + builder.append(" -> "); + } + } + builder.append(")"); + return builder.toString(); + } + + public static void formatSource(Formatter formatter, Object source) { + ElementSource elementSource = null; + if (source instanceof ElementSource) { + elementSource = (ElementSource) source; + source = elementSource.getDeclaringSource(); + } + formatSource(formatter, source, elementSource); + } + + public static void formatSource(Formatter formatter, Object source, ElementSource elementSource) { + String modules = moduleSourceString(elementSource); + if (source instanceof Dependency) { + Dependency dependency = (Dependency) source; + InjectionPoint injectionPoint = dependency.getInjectionPoint(); + if (injectionPoint != null) { + formatInjectionPoint(formatter, dependency, injectionPoint, elementSource); + } else { + formatSource(formatter, dependency.getKey(), elementSource); + } + + } else if (source instanceof InjectionPoint) { + formatInjectionPoint(formatter, null, (InjectionPoint) source, elementSource); + + } else if (source instanceof Class) { + formatter.format(" at %s%s%n", StackTraceElements.forType((Class) source), modules); + + } else if (source instanceof Member) { + formatter.format(" at %s%s%n", StackTraceElements.forMember((Member) source), modules); + + } else if (source instanceof TypeLiteral) { + formatter.format(" while locating %s%s%n", source, modules); + + } else if (source instanceof Key) { + Key key = (Key) source; + formatter.format(" while locating %s%n", convert(key, elementSource)); + + } else if (source instanceof Thread) { + formatter.format(" in thread %s%n", source); + + } else { + formatter.format(" at %s%s%n", source, modules); + } + } + + public static void formatInjectionPoint(Formatter formatter, Dependency dependency, + InjectionPoint injectionPoint, ElementSource elementSource) { + Member member = injectionPoint.getMember(); + Class memberType = Classes.memberType(member); + + if (memberType == Field.class) { + dependency = injectionPoint.getDependencies().get(0); + formatter.format(" while locating %s%n", convert(dependency.getKey(), elementSource)); + formatter.format(" for field at %s%n", StackTraceElements.forMember(member)); + + } else if (dependency != null) { + formatter.format(" while locating %s%n", convert(dependency.getKey(), elementSource)); + formatter.format(" for parameter %s at %s%n", + dependency.getParameterIndex(), StackTraceElements.forMember(member)); + + } else { + formatSource(formatter, injectionPoint.getMember()); + } + } + + /** + * Returns an instance that uses {@code source} as a reference point for newly added errors. + */ + public Errors withSource(Object source) { + return source == this.source || source == SourceProvider.UNKNOWN_SOURCE + ? this + : new Errors(this, source); + } + + /** + * We use a fairly generic error message here. The motivation is to share the + * same message for both bind time errors: + *
Guice.createInjector(new AbstractModule() {
+     *   public void configure() {
+     *     bind(Runnable.class);
+     *   }
+     * }
+ * ...and at provide-time errors: + *
Guice.createInjector().getInstance(Runnable.class);
+ * Otherwise we need to know who's calling when resolving a just-in-time + * binding, which makes things unnecessarily complex. + */ + public Errors missingImplementation(Key key) { + return addMessage("No implementation for %s was bound.", key); + } + + public Errors jitDisabled(Key key) { + return addMessage("Explicit bindings are required and %s is not explicitly bound.", key); + } + + public Errors jitDisabledInParent(Key key) { + return addMessage( + "Explicit bindings are required and %s would be bound in a parent injector.%n" + + "Please add an explicit binding for it, either in the child or the parent.", + key); + } + + public Errors atInjectRequired(Class clazz) { + return addMessage( + "Explicit @Inject annotations are required on constructors," + + " but %s has no constructors annotated with @Inject.", + clazz); + } + + public Errors converterReturnedNull(String stringValue, Object source, + TypeLiteral type, TypeConverterBinding typeConverterBinding) { + return addMessage("Received null converting '%s' (bound at %s) to %s%n" + + " using %s.", + stringValue, convert(source), type, typeConverterBinding); + } + + public Errors conversionTypeError(String stringValue, Object source, TypeLiteral type, + TypeConverterBinding typeConverterBinding, Object converted) { + return addMessage("Type mismatch converting '%s' (bound at %s) to %s%n" + + " using %s.%n" + + " Converter returned %s.", + stringValue, convert(source), type, typeConverterBinding, converted); + } + + public Errors conversionError(String stringValue, Object source, + TypeLiteral type, TypeConverterBinding typeConverterBinding, RuntimeException cause) { + return errorInUserCode(cause, "Error converting '%s' (bound at %s) to %s%n" + + " using %s.%n" + + " Reason: %s", + stringValue, convert(source), type, typeConverterBinding, cause); + } + + public Errors ambiguousTypeConversion(String stringValue, Object source, TypeLiteral type, + TypeConverterBinding a, TypeConverterBinding b) { + return addMessage("Multiple converters can convert '%s' (bound at %s) to %s:%n" + + " %s and%n" + + " %s.%n" + + " Please adjust your type converter configuration to avoid overlapping matches.", + stringValue, convert(source), type, a, b); + } + + public Errors bindingToProvider() { + return addMessage("Binding to Provider is not allowed."); + } + + public Errors subtypeNotProvided(Class> providerType, + Class type) { + return addMessage("%s doesn't provide instances of %s.", providerType, type); + } + + public Errors notASubtype(Class implementationType, Class type) { + return addMessage("%s doesn't extend %s.", implementationType, type); + } + + public Errors recursiveImplementationType() { + return addMessage("@ImplementedBy points to the same class it annotates."); + } + + public Errors recursiveProviderType() { + return addMessage("@ProvidedBy points to the same class it annotates."); + } + + public Errors missingRuntimeRetention(Class annotation) { + return addMessage(format("Please annotate %s with @Retention(RUNTIME).", annotation)); + } + + public Errors missingScopeAnnotation(Class annotation) { + return addMessage(format("Please annotate %s with @ScopeAnnotation.", annotation)); + } + + public Errors optionalConstructor(Constructor constructor) { + return addMessage("%s is annotated @Inject(optional=true), " + + "but constructors cannot be optional.", constructor); + } + + public Errors cannotBindToGuiceType(String simpleName) { + return addMessage("Binding to core guice framework type is not allowed: %s.", simpleName); + } + + public Errors scopeNotFound(Class scopeAnnotation) { + return addMessage("No scope is bound to %s.", scopeAnnotation); + } + + public Errors scopeAnnotationOnAbstractType( + Class scopeAnnotation, Class type, Object source) { + return addMessage("%s is annotated with %s, but scope annotations are not supported " + + "for abstract types.%n Bound at %s.", type, scopeAnnotation, convert(source)); + } + + public Errors misplacedBindingAnnotation(Member member, Annotation bindingAnnotation) { + return addMessage("%s is annotated with %s, but binding annotations should be applied " + + "to its parameters instead.", member, bindingAnnotation); + } + + public Errors missingConstructor(Class implementation) { + return addMessage("Could not find a suitable constructor in %s. " + CONSTRUCTOR_RULES, + implementation); + } + + public Errors tooManyConstructors(Class implementation) { + return addMessage("%s has more than one constructor annotated with @Inject. " + + CONSTRUCTOR_RULES, implementation); + } + + public Errors constructorNotDefinedByType(Constructor constructor, TypeLiteral type) { + return addMessage("%s does not define %s", type, constructor); + } + + public Errors duplicateScopes(ScopeBinding existing, + Class annotationType, Scope scope) { + return addMessage("Scope %s is already bound to %s at %s.%n Cannot bind %s.", + existing.getScope(), annotationType, existing.getSource(), scope); + } + + public Errors voidProviderMethod() { + return addMessage("Provider methods must return a value. Do not return void."); + } + + public Errors missingConstantValues() { + return addMessage("Missing constant value. Please call to(...)."); + } + + public Errors cannotInjectInnerClass(Class type) { + return addMessage("Injecting into inner classes is not supported. " + + "Please use a 'static' class (top-level or nested) instead of %s.", type); + } + + public Errors duplicateBindingAnnotations(Member member, + Class a, Class b) { + return addMessage("%s has more than one annotation annotated with @BindingAnnotation: " + + "%s and %s", member, a, b); + } + + public Errors staticInjectionOnInterface(Class clazz) { + return addMessage("%s is an interface, but interfaces have no static injection points.", clazz); + } + + public Errors cannotInjectFinalField(Field field) { + return addMessage("Injected field %s cannot be final.", field); + } + + public Errors cannotInjectAbstractMethod(Method method) { + return addMessage("Injected method %s cannot be abstract.", method); + } + + public Errors cannotInjectNonVoidMethod(Method method) { + return addMessage("Injected method %s must return void.", method); + } + + public Errors cannotInjectMethodWithTypeParameters(Method method) { + return addMessage("Injected method %s cannot declare type parameters of its own.", method); + } + + public Errors duplicateScopeAnnotations( + Class a, Class b) { + return addMessage("More than one scope annotation was found: %s and %s.", a, b); + } + + public Errors recursiveBinding() { + return addMessage("Binding points to itself."); + } + + public Errors bindingAlreadySet(Key key, Object source) { + return addMessage("A binding to %s was already configured at %s.", key, convert(source)); + } + + public Errors jitBindingAlreadySet(Key key) { + return addMessage("A just-in-time binding to %s was already configured on a parent injector.", key); + } + + public Errors childBindingAlreadySet(Key key, Set sources) { + Formatter allSources = new Formatter(); + for (Object source : sources) { + if (source == null) { + allSources.format("%n (bound by a just-in-time binding)"); + } else { + allSources.format("%n bound at %s", source); + } + } + Errors errors = addMessage( + "Unable to create binding for %s." + + " It was already configured on one or more child injectors or private modules" + + "%s%n" + + " If it was in a PrivateModule, did you forget to expose the binding?", + key, allSources.out()); + return errors; + } + + public Errors errorCheckingDuplicateBinding(Key key, Object source, Throwable t) { + return addMessage( + "A binding to %s was already configured at %s and an error was thrown " + + "while checking duplicate bindings. Error: %s", + key, convert(source), t); + } + + public Errors errorInjectingMethod(Throwable cause) { + return errorInUserCode(cause, "Error injecting method, %s", cause); + } + + public Errors errorNotifyingTypeListener(TypeListenerBinding listener, + TypeLiteral type, Throwable cause) { + return errorInUserCode(cause, + "Error notifying TypeListener %s (bound at %s) of %s.%n" + + " Reason: %s", + listener.getListener(), convert(listener.getSource()), type, cause); + } + + public Errors errorInjectingConstructor(Throwable cause) { + return errorInUserCode(cause, "Error injecting constructor, %s", cause); + } + + public Errors errorInProvider(RuntimeException runtimeException) { + Throwable unwrapped = unwrap(runtimeException); + return errorInUserCode(unwrapped, "Error in custom provider, %s", unwrapped); + } + + public Errors errorInUserInjector( + MembersInjector listener, TypeLiteral type, RuntimeException cause) { + return errorInUserCode(cause, "Error injecting %s using %s.%n" + + " Reason: %s", type, listener, cause); + } + + public Errors errorNotifyingInjectionListener( + InjectionListener listener, TypeLiteral type, RuntimeException cause) { + return errorInUserCode(cause, "Error notifying InjectionListener %s of %s.%n" + + " Reason: %s", listener, type, cause); + } + + public Errors exposedButNotBound(Key key) { + return addMessage("Could not expose() %s, it must be explicitly bound.", key); + } + + public Errors keyNotFullySpecified(TypeLiteral typeLiteral) { + return addMessage("%s cannot be used as a key; It is not fully specified.", typeLiteral); + } + + public Errors errorEnhancingClass(Class clazz, Throwable cause) { + return errorInUserCode(cause, "Unable to method intercept: %s", clazz); + } + + public Errors errorInUserCode(Throwable cause, String messageFormat, Object... arguments) { + Collection messages = getMessagesFromThrowable(cause); + + if (!messages.isEmpty()) { + return merge(messages); + } else { + return addMessage(cause, messageFormat, arguments); + } + } + + private Throwable unwrap(RuntimeException runtimeException) { + if (runtimeException instanceof Exceptions.UnhandledCheckedUserException) { + return runtimeException.getCause(); + } else { + return runtimeException; + } + } + + public Errors cannotInjectRawProvider() { + return addMessage("Cannot inject a Provider that has no type parameter"); + } + + public Errors cannotInjectRawMembersInjector() { + return addMessage("Cannot inject a MembersInjector that has no type parameter"); + } + + public Errors cannotInjectTypeLiteralOf(Type unsupportedType) { + return addMessage("Cannot inject a TypeLiteral of %s", unsupportedType); + } + + public Errors cannotInjectRawTypeLiteral() { + return addMessage("Cannot inject a TypeLiteral that has no type parameter"); + } + + public Errors cannotSatisfyCircularDependency(Class expectedType) { + return addMessage( + "Tried proxying %s to support a circular dependency, but it is not an interface.", + expectedType); + } + + public Errors circularProxiesDisabled(Class expectedType) { + return addMessage( + "Tried proxying %s to support a circular dependency, but circular proxies are disabled.", + expectedType); + } + + public void throwCreationExceptionIfErrorsExist() { + if (!hasErrors()) { + return; + } + + throw new CreationException(getMessages()); + } + + public void throwConfigurationExceptionIfErrorsExist() { + if (!hasErrors()) { + return; + } + + throw new ConfigurationException(getMessages()); + } + + public void throwProvisionExceptionIfErrorsExist() { + if (!hasErrors()) { + return; + } + + throw new ProvisionException(getMessages()); + } + + private Message merge(Message message) { + List sources = Lists.newArrayList(); + sources.addAll(getSources()); + sources.addAll(message.getSources()); + return new Message(sources, message.getMessage(), message.getCause()); + } + + public Errors merge(Collection messages) { + for (Message message : messages) { + addMessage(merge(message)); + } + return this; + } + + public Errors merge(Errors moreErrors) { + if (moreErrors.root == root || moreErrors.root.errors == null) { + return this; + } + + merge(moreErrors.root.errors); + return this; + } + + public List getSources() { + List sources = Lists.newArrayList(); + for (Errors e = this; e != null; e = e.parent) { + if (e.source != SourceProvider.UNKNOWN_SOURCE) { + sources.add(0, e.source); + } + } + return sources; + } + + public void throwIfNewErrors(int expectedSize) throws ErrorsException { + if (size() == expectedSize) { + return; + } + + throw toException(); + } + + public ErrorsException toException() { + return new ErrorsException(this); + } + + public boolean hasErrors() { + return root.errors != null; + } + + public Errors addMessage(String messageFormat, Object... arguments) { + return addMessage(null, messageFormat, arguments); + } + + private Errors addMessage(Throwable cause, String messageFormat, Object... arguments) { + String message = format(messageFormat, arguments); + addMessage(new Message(getSources(), message, cause)); + return this; + } + + public Errors addMessage(Message message) { + if (root.errors == null) { + root.errors = Lists.newArrayList(); + } + root.errors.add(message); + return this; + } + + public List getMessages() { + if (root.errors == null) { + return ImmutableList.of(); + } + + return new Ordering() { + @Override + public int compare(Message a, Message b) { + return a.getSource().compareTo(b.getSource()); + } + }.sortedCopy(root.errors); + } + + /** + * Returns {@code value} if it is non-null allowed to be null. Otherwise a message is added and + * an {@code ErrorsException} is thrown. + */ + public T checkForNull(T value, Object source, Dependency dependency) + throws ErrorsException { + if (value != null || dependency.isNullable()) { + return value; + } + + // Hack to allow null parameters to @Provides methods, for backwards compatibility. + if (dependency.getInjectionPoint().getMember() instanceof Method) { + Method annotated = (Method) dependency.getInjectionPoint().getMember(); + if (annotated.isAnnotationPresent(Provides.class)) { + switch (InternalFlags.getNullableProvidesOption()) { + case ERROR: + break; // break out & let the below exception happen + case IGNORE: + return value; // user doesn't care about injecting nulls to non-@Nullables. + case WARN: + // Warn only once, otherwise we spam logs too much. + if (!warnedDependencies.add(dependency)) { + return value; + } + /*logger.log(Level.WARNING, + "Guice injected null into parameter {0} of {1} (a {2}), please mark it @Nullable." + + " Use -Dguice_check_nullable_provides_params=ERROR to turn this into an" + + " error.", + new Object[]{ + dependency.getParameterIndex(), + convert(dependency.getInjectionPoint().getMember()), + convert(dependency.getKey())});*/ + return null; // log & exit. + } + } + } + + int parameterIndex = dependency.getParameterIndex(); + String parameterName = (parameterIndex != -1) + ? "parameter " + parameterIndex + " of " + : ""; + addMessage("null returned by binding at %s%n but %s%s is not @Nullable", + source, parameterName, dependency.getInjectionPoint().getMember()); + + throw toException(); + } + + public int size() { + return root.errors == null ? 0 : root.errors.size(); + } + + private static abstract class Converter { + + final Class type; + + Converter(Class type) { + this.type = type; + } + + boolean appliesTo(Object o) { + return o != null && type.isAssignableFrom(o.getClass()); + } + + String convert(Object o) { + return toString(type.cast(o)); + } + + abstract String toString(T t); + } +} diff --git a/src/main/java/com/google/inject/internal/ErrorsException.java b/src/main/java/com/google/inject/internal/ErrorsException.java new file mode 100644 index 0000000..b2718cb --- /dev/null +++ b/src/main/java/com/google/inject/internal/ErrorsException.java @@ -0,0 +1,20 @@ +package com.google.inject.internal; + +/** + * Indicates that a result could not be returned while preparing or resolving a binding. The caller + * should {@link Errors#merge(Errors) merge} the errors from this exception with their existing + * errors. + */ +@SuppressWarnings("serial") +public class ErrorsException extends Exception { + + private final Errors errors; + + public ErrorsException(Errors errors) { + this.errors = errors; + } + + public Errors getErrors() { + return errors; + } +} diff --git a/src/main/java/com/google/inject/internal/Exceptions.java b/src/main/java/com/google/inject/internal/Exceptions.java new file mode 100644 index 0000000..e3502ae --- /dev/null +++ b/src/main/java/com/google/inject/internal/Exceptions.java @@ -0,0 +1,46 @@ +package com.google.inject.internal; + +/** + * Rethrows user-code exceptions in wrapped exceptions so that Errors can target the correct + * exception. + */ +class Exceptions { + + /** + * Rethrows the exception (or it's cause, if it has one) directly if possible. + * If it was a checked exception, this wraps the exception in a stack trace + * with no frames, so that the exception is shown immediately with no frames + * above it. + */ + public static RuntimeException rethrowCause(Throwable throwable) { + Throwable cause = throwable; + if (cause.getCause() != null) { + cause = cause.getCause(); + } + return rethrow(cause); + } + + /** + * Rethrows the exception. + */ + public static RuntimeException rethrow(Throwable throwable) { + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else if (throwable instanceof Error) { + throw (Error) throwable; + } else { + throw new UnhandledCheckedUserException(throwable); + } + } + + /** + * A marker exception class that we look for in order to unwrap the exception + * into the user exception, to provide a cleaner stack trace. + */ + @SuppressWarnings("serial") + static class UnhandledCheckedUserException extends RuntimeException { + public UnhandledCheckedUserException(Throwable cause) { + super(cause); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ExposedBindingImpl.java b/src/main/java/com/google/inject/internal/ExposedBindingImpl.java new file mode 100644 index 0000000..93d96dc --- /dev/null +++ b/src/main/java/com/google/inject/internal/ExposedBindingImpl.java @@ -0,0 +1,52 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.ExposedBinding; +import com.google.inject.spi.PrivateElements; + +import java.util.Set; + +public final class ExposedBindingImpl extends BindingImpl implements ExposedBinding { + + private final PrivateElements privateElements; + + public ExposedBindingImpl(InjectorImpl injector, Object source, Key key, + InternalFactory factory, PrivateElements privateElements) { + super(injector, key, source, factory, Scoping.UNSCOPED); + this.privateElements = privateElements; + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + return visitor.visit(this); + } + + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(Key.get(Injector.class))); + } + + public PrivateElements getPrivateElements() { + return privateElements; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ExposedBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .add("privateElements", privateElements) + .toString(); + } + + public void applyTo(Binder binder) { + throw new UnsupportedOperationException("This element represents a synthetic binding."); + } + + // Purposely does not override equals/hashcode, because exposed bindings are only equal to + // themselves right now -- that is, there cannot be "duplicate" exposed bindings. +} diff --git a/src/main/java/com/google/inject/internal/ExposedKeyFactory.java b/src/main/java/com/google/inject/internal/ExposedKeyFactory.java new file mode 100644 index 0000000..d44ed70 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ExposedKeyFactory.java @@ -0,0 +1,40 @@ +package com.google.inject.internal; + +import com.google.inject.Key; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.PrivateElements; + +/** + * This factory exists in a parent injector. When invoked, it retrieves its value from a child + * injector. + */ +final class ExposedKeyFactory implements InternalFactory, CreationListener { + private final Key key; + private final PrivateElements privateElements; + private BindingImpl delegate; + + ExposedKeyFactory(Key key, PrivateElements privateElements) { + this.key = key; + this.privateElements = privateElements; + } + + public void notify(Errors errors) { + InjectorImpl privateInjector = (InjectorImpl) privateElements.getInjector(); + BindingImpl explicitBinding = privateInjector.state.getExplicitBinding(key); + + // validate that the child injector has its own factory. If the getInternalFactory() returns + // this, then that child injector doesn't have a factory (and getExplicitBinding has returned + // its parent's binding instead + if (explicitBinding.getInternalFactory() == this) { + errors.withSource(explicitBinding.getSource()).exposedButNotBound(key); + return; + } + + this.delegate = explicitBinding; + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + return delegate.getInternalFactory().get(errors, context, dependency, linked); + } +} diff --git a/src/main/java/com/google/inject/internal/ExposureBuilder.java b/src/main/java/com/google/inject/internal/ExposureBuilder.java new file mode 100644 index 0000000..612c141 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ExposureBuilder.java @@ -0,0 +1,54 @@ +package com.google.inject.internal; + +import com.google.common.base.Preconditions; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.binder.AnnotatedElementBuilder; + +import java.lang.annotation.Annotation; + +/** + * For private binder's expose() method. + */ +public class ExposureBuilder implements AnnotatedElementBuilder { + private final Binder binder; + private final Object source; + private Key key; + + public ExposureBuilder(Binder binder, Object source, Key key) { + this.binder = binder; + this.source = source; + this.key = key; + } + + protected void checkNotAnnotated() { + if (key.getAnnotationType() != null) { + binder.addError(AbstractBindingBuilder.ANNOTATION_ALREADY_SPECIFIED); + } + } + + public void annotatedWith(Class annotationType) { + Preconditions.checkNotNull(annotationType, "annotationType"); + checkNotAnnotated(); + key = Key.get(key.getTypeLiteral(), annotationType); + } + + public void annotatedWith(Annotation annotation) { + Preconditions.checkNotNull(annotation, "annotation"); + checkNotAnnotated(); + key = Key.get(key.getTypeLiteral(), annotation); + } + + public Key getKey() { + return key; + } + + public Object getSource() { + return source; + } + + @Override + public String toString() { + return "AnnotatedElementBuilder"; + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/FactoryProxy.java b/src/main/java/com/google/inject/internal/FactoryProxy.java new file mode 100644 index 0000000..3347bfc --- /dev/null +++ b/src/main/java/com/google/inject/internal/FactoryProxy.java @@ -0,0 +1,53 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.inject.Key; +import com.google.inject.internal.InjectorImpl.JitLimitation; +import com.google.inject.spi.Dependency; + +/** + * A placeholder which enables us to swap in the real factory once the injector is created. + * Used for a linked binding, so that getting the linked binding returns the link's factory. + */ +final class FactoryProxy implements InternalFactory, CreationListener { + + private final InjectorImpl injector; + private final Key key; + private final Key targetKey; + private final Object source; + + private InternalFactory targetFactory; + + FactoryProxy(InjectorImpl injector, Key key, Key targetKey, Object source) { + this.injector = injector; + this.key = key; + this.targetKey = targetKey; + this.source = source; + } + + public void notify(final Errors errors) { + try { + targetFactory = injector.getInternalFactory(targetKey, errors.withSource(source), JitLimitation.NEW_OR_EXISTING_JIT); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + context.pushState(targetKey, source); + try { + return targetFactory.get(errors.withSource(targetKey), context, dependency, true); + } finally { + context.popState(); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(FactoryProxy.class) + .add("key", key) + .add("provider", targetFactory) + .toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/FailableCache.java b/src/main/java/com/google/inject/internal/FailableCache.java new file mode 100644 index 0000000..89e4525 --- /dev/null +++ b/src/main/java/com/google/inject/internal/FailableCache.java @@ -0,0 +1,44 @@ +package com.google.inject.internal; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +/** + * Lazily creates (and caches) values for keys. If creating the value fails (with errors), an + * exception is thrown on retrieval. + */ +public abstract class FailableCache { + + private final LoadingCache delegate = CacheBuilder.newBuilder().build( + new CacheLoader() { + public Object load(K key) { + Errors errors = new Errors(); + V result = null; + try { + result = FailableCache.this.create(key, errors); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + return errors.hasErrors() ? errors : result; + } + }); + + protected abstract V create(K key, Errors errors) throws ErrorsException; + + public V get(K key, Errors errors) throws ErrorsException { + Object resultOrError = delegate.getUnchecked(key); + if (resultOrError instanceof Errors) { + errors.merge((Errors) resultOrError); + throw errors.toException(); + } else { + @SuppressWarnings("unchecked") // create returned a non-error result, so this is safe + V result = (V) resultOrError; + return result; + } + } + + boolean remove(K key) { + return delegate.asMap().remove(key) != null; + } +} diff --git a/src/main/java/com/google/inject/internal/InheritingState.java b/src/main/java/com/google/inject/internal/InheritingState.java new file mode 100644 index 0000000..1d79797 --- /dev/null +++ b/src/main/java/com/google/inject/internal/InheritingState.java @@ -0,0 +1,160 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Scope; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; +import com.google.inject.spi.ProvisionListenerBinding; +import com.google.inject.spi.ScopeBinding; +import com.google.inject.spi.TypeConverterBinding; +import com.google.inject.spi.TypeListenerBinding; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +final class InheritingState implements State { + + private final State parent; + + // Must be a linked hashmap in order to preserve order of bindings in Modules. + private final Map, Binding> explicitBindingsMutable = Maps.newLinkedHashMap(); + private final Map, Binding> explicitBindings + = Collections.unmodifiableMap(explicitBindingsMutable); + private final Map, ScopeBinding> scopes = Maps.newHashMap(); + private final List converters = Lists.newArrayList(); + private final List typeListenerBindings = Lists.newArrayList(); + private final List provisionListenerBindings = Lists.newArrayList(); + private final List scannerBindings = Lists.newArrayList(); + private final WeakKeySet blacklistedKeys; + private final Object lock; + + InheritingState(State parent) { + this.parent = checkNotNull(parent, "parent"); + this.lock = (parent == State.NONE) ? this : parent.lock(); + this.blacklistedKeys = new WeakKeySet(lock); + } + + public State parent() { + return parent; + } + + @SuppressWarnings("unchecked") // we only put in BindingImpls that match their key types + public BindingImpl getExplicitBinding(Key key) { + Binding binding = explicitBindings.get(key); + return binding != null ? (BindingImpl) binding : parent.getExplicitBinding(key); + } + + public Map, Binding> getExplicitBindingsThisLevel() { + return explicitBindings; + } + + public void putBinding(Key key, BindingImpl binding) { + explicitBindingsMutable.put(key, binding); + } + + public ScopeBinding getScopeBinding(Class annotationType) { + ScopeBinding scopeBinding = scopes.get(annotationType); + return scopeBinding != null ? scopeBinding : parent.getScopeBinding(annotationType); + } + + public void putScopeBinding(Class annotationType, ScopeBinding scope) { + scopes.put(annotationType, scope); + } + + public Iterable getConvertersThisLevel() { + return converters; + } + + public void addConverter(TypeConverterBinding typeConverterBinding) { + converters.add(typeConverterBinding); + } + + public TypeConverterBinding getConverter( + String stringValue, TypeLiteral type, Errors errors, Object source) { + TypeConverterBinding matchingConverter = null; + for (State s = this; s != State.NONE; s = s.parent()) { + for (TypeConverterBinding converter : s.getConvertersThisLevel()) { + if (converter.getTypeMatcher().matches(type)) { + if (matchingConverter != null) { + errors.ambiguousTypeConversion(stringValue, source, type, matchingConverter, converter); + } + matchingConverter = converter; + } + } + } + return matchingConverter; + } + + public void addTypeListener(TypeListenerBinding listenerBinding) { + typeListenerBindings.add(listenerBinding); + } + + public List getTypeListenerBindings() { + List parentBindings = parent.getTypeListenerBindings(); + List result = + Lists.newArrayListWithCapacity(parentBindings.size() + typeListenerBindings.size()); + result.addAll(parentBindings); + result.addAll(typeListenerBindings); + return result; + } + + public void addProvisionListener(ProvisionListenerBinding listenerBinding) { + provisionListenerBindings.add(listenerBinding); + } + + public List getProvisionListenerBindings() { + List parentBindings = parent.getProvisionListenerBindings(); + List result = + Lists.newArrayListWithCapacity(parentBindings.size() + provisionListenerBindings.size()); + result.addAll(parentBindings); + result.addAll(provisionListenerBindings); + return result; + } + + public void addScanner(ModuleAnnotatedMethodScannerBinding scanner) { + scannerBindings.add(scanner); + } + + public List getScannerBindings() { + List parentBindings = parent.getScannerBindings(); + List result = + Lists.newArrayListWithCapacity(parentBindings.size() + scannerBindings.size()); + result.addAll(parentBindings); + result.addAll(scannerBindings); + return result; + } + + public void blacklist(Key key, State state, Object source) { + parent.blacklist(key, state, source); + blacklistedKeys.add(key, state, source); + } + + public boolean isBlacklisted(Key key) { + return blacklistedKeys.contains(key); + } + + public Set getSourcesForBlacklistedKey(Key key) { + return blacklistedKeys.getSources(key); + } + + public Object lock() { + return lock; + } + + public Map, Scope> getScopes() { + ImmutableMap.Builder, Scope> builder = ImmutableMap.builder(); + for (Map.Entry, ScopeBinding> entry : scopes.entrySet()) { + builder.put(entry.getKey(), entry.getValue().getScope()); + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/inject/internal/Initializable.java b/src/main/java/com/google/inject/internal/Initializable.java new file mode 100644 index 0000000..f8df3c5 --- /dev/null +++ b/src/main/java/com/google/inject/internal/Initializable.java @@ -0,0 +1,12 @@ +package com.google.inject.internal; + +/** + * Holds a reference that requires initialization to be performed before it can be used. + */ +interface Initializable { + + /** + * Ensures the reference is initialized, then returns it. + */ + T get(Errors errors) throws ErrorsException; +} diff --git a/src/main/java/com/google/inject/internal/Initializables.java b/src/main/java/com/google/inject/internal/Initializables.java new file mode 100644 index 0000000..9ae6c8d --- /dev/null +++ b/src/main/java/com/google/inject/internal/Initializables.java @@ -0,0 +1,20 @@ +package com.google.inject.internal; + +final class Initializables { + + /** + * Returns an initializable for an instance that requires no initialization. + */ + static Initializable of(final T instance) { + return new Initializable() { + public T get(Errors errors) throws ErrorsException { + return instance; + } + + @Override + public String toString() { + return String.valueOf(instance); + } + }; + } +} diff --git a/src/main/java/com/google/inject/internal/Initializer.java b/src/main/java/com/google/inject/internal/Initializer.java new file mode 100644 index 0000000..dbd80bd --- /dev/null +++ b/src/main/java/com/google/inject/internal/Initializer.java @@ -0,0 +1,181 @@ +package com.google.inject.internal; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionPoint; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Manages and injects instances at injector-creation time. This is made more complicated by + * instances that request other instances while they're being injected. We overcome this by using + * {@link Initializable}, which attempts to perform injection before use. + * + */ +final class Initializer { + + /** + * the only thread that we'll use to inject members. + */ + private final Thread creatingThread = Thread.currentThread(); + + /** + * zero means everything is injected. + */ + private final CountDownLatch ready = new CountDownLatch(1); + + /** + * Maps from instances that need injection to the MembersInjector that will inject them. + */ + private final Map> pendingMembersInjectors = + Maps.newIdentityHashMap(); + + /** + * Maps instances that need injection to a source that registered them + */ + private final Map> pendingInjection = Maps.newIdentityHashMap(); + + /** + * Registers an instance for member injection when that step is performed. + * + * @param instance an instance that optionally has members to be injected (each annotated with + * @param binding the binding that caused this initializable to be created, if it exists. + * @param source the source location that this injection was requested + * @Inject). + */ + Initializable requestInjection(InjectorImpl injector, T instance, Binding binding, + Object source, Set injectionPoints) { + checkNotNull(source); + + ProvisionListenerStackCallback provisionCallback = + binding == null ? null : injector.provisionListenerStore.get(binding); + + // short circuit if the object has no injections or listeners. + if (instance == null || (injectionPoints.isEmpty() + && !injector.membersInjectorStore.hasTypeListeners() + && (provisionCallback == null || !provisionCallback.hasListeners()))) { + return Initializables.of(instance); + } + + InjectableReference initializable = new InjectableReference( + injector, instance, binding == null ? null : binding.getKey(), provisionCallback, source); + pendingInjection.put(instance, initializable); + return initializable; + } + + /** + * Prepares member injectors for all injected instances. This prompts Guice to do static analysis + * on the injected instances. + */ + void validateOustandingInjections(Errors errors) { + for (InjectableReference reference : pendingInjection.values()) { + try { + pendingMembersInjectors.put(reference.instance, reference.validate(errors)); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + } + + /** + * Performs creation-time injections on all objects that require it. Whenever fulfilling an + * injection depends on another object that requires injection, we inject it first. If the two + * instances are codependent (directly or transitively), ordering of injection is arbitrary. + */ + void injectAll(final Errors errors) { + // loop over a defensive copy since ensureInjected() mutates the set. Unfortunately, that copy + // is made complicated by a bug in IBM's JDK, wherein entrySet().toArray(Object[]) doesn't work + for (InjectableReference reference : Lists.newArrayList(pendingInjection.values())) { + try { + reference.get(errors); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + + if (!pendingInjection.isEmpty()) { + throw new AssertionError("Failed to satisfy " + pendingInjection); + } + + ready.countDown(); + } + + private class InjectableReference implements Initializable { + private final InjectorImpl injector; + private final T instance; + private final Object source; + private final Key key; + private final ProvisionListenerStackCallback provisionCallback; + + public InjectableReference(InjectorImpl injector, T instance, Key key, + ProvisionListenerStackCallback provisionCallback, Object source) { + this.injector = injector; + this.key = key; // possibly null! + this.provisionCallback = provisionCallback; // possibly null! + this.instance = checkNotNull(instance, "instance"); + this.source = checkNotNull(source, "source"); + } + + public MembersInjectorImpl validate(Errors errors) throws ErrorsException { + @SuppressWarnings("unchecked") // the type of 'T' is a TypeLiteral + TypeLiteral type = TypeLiteral.get((Class) instance.getClass()); + return injector.membersInjectorStore.get(type, errors.withSource(source)); + } + + /** + * Reentrant. If {@code instance} was registered for injection at injector-creation time, this + * method will ensure that all its members have been injected before returning. + */ + public T get(Errors errors) throws ErrorsException { + if (ready.getCount() == 0) { + return instance; + } + + // just wait for everything to be injected by another thread + if (Thread.currentThread() != creatingThread) { + try { + ready.await(); + return instance; + } catch (InterruptedException e) { + // Give up, since we don't know if our injection is ready + throw new RuntimeException(e); + } + } + + // toInject needs injection, do it right away. we only do this once, even if it fails + if (pendingInjection.remove(instance) != null) { + // safe because we only insert a members injector for the appropriate instance + @SuppressWarnings("unchecked") + MembersInjectorImpl membersInjector = + (MembersInjectorImpl) pendingMembersInjectors.remove(instance); + Preconditions.checkState(membersInjector != null, + "No membersInjector available for instance: %s, from key: %s", instance, key); + + // if in Stage.TOOL, we only want to inject & notify toolable injection points. + // (otherwise we'll inject all of them) + membersInjector.injectAndNotify(instance, + errors.withSource(source), + key, + provisionCallback, + source, + injector.options.stage == Stage.TOOL); + } + + return instance; + } + + @Override + public String toString() { + return instance.toString(); + } + } +} diff --git a/src/main/java/com/google/inject/internal/InjectionRequestProcessor.java b/src/main/java/com/google/inject/internal/InjectionRequestProcessor.java new file mode 100644 index 0000000..3cd8e3e --- /dev/null +++ b/src/main/java/com/google/inject/internal/InjectionRequestProcessor.java @@ -0,0 +1,119 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.inject.ConfigurationException; +import com.google.inject.Stage; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.InjectionRequest; +import com.google.inject.spi.StaticInjectionRequest; + +import java.util.List; +import java.util.Set; + +/** + * Handles {@code Binder.requestInjection} and {@code Binder.requestStaticInjection} commands. + * + */ +final class InjectionRequestProcessor extends AbstractProcessor { + + private final List staticInjections = Lists.newArrayList(); + private final Initializer initializer; + + InjectionRequestProcessor(Errors errors, Initializer initializer) { + super(errors); + this.initializer = initializer; + } + + @Override + public Boolean visit(StaticInjectionRequest request) { + staticInjections.add(new StaticInjection(injector, request)); + return true; + } + + @Override + public Boolean visit(InjectionRequest request) { + Set injectionPoints; + try { + injectionPoints = request.getInjectionPoints(); + } catch (ConfigurationException e) { + errors.merge(e.getErrorMessages()); + injectionPoints = e.getPartialValue(); + } + + initializer.requestInjection( + injector, request.getInstance(), null, request.getSource(), injectionPoints); + return true; + } + + void validate() { + for (StaticInjection staticInjection : staticInjections) { + staticInjection.validate(); + } + } + + void injectMembers() { + /* + * TODO: If you request both a parent class and one of its + * subclasses, the parent class's static members will be + * injected twice. + */ + for (StaticInjection staticInjection : staticInjections) { + staticInjection.injectMembers(); + } + } + + /** + * A requested static injection. + */ + private class StaticInjection { + final InjectorImpl injector; + final Object source; + final StaticInjectionRequest request; + ImmutableList memberInjectors; + + public StaticInjection(InjectorImpl injector, StaticInjectionRequest request) { + this.injector = injector; + this.source = request.getSource(); + this.request = request; + } + + void validate() { + Errors errorsForMember = errors.withSource(source); + Set injectionPoints; + try { + injectionPoints = request.getInjectionPoints(); + } catch (ConfigurationException e) { + errorsForMember.merge(e.getErrorMessages()); + injectionPoints = e.getPartialValue(); + } + if (injectionPoints != null) { + memberInjectors = injector.membersInjectorStore.getInjectors( + injectionPoints, errorsForMember); + } else { + memberInjectors = ImmutableList.of(); + } + + errors.merge(errorsForMember); + } + + void injectMembers() { + try { + injector.callInContext(new ContextualCallable() { + public Void call(InternalContext context) { + for (SingleMemberInjector memberInjector : memberInjectors) { + // Run injections if we're not in tool stage (ie, PRODUCTION or DEV), + // or if we are in tool stage and the injection point is toolable. + if (injector.options.stage != Stage.TOOL || memberInjector.getInjectionPoint().isToolable()) { + memberInjector.inject(errors, context, null); + } + } + return null; + } + }); + } catch (ErrorsException e) { + throw new AssertionError(); + } + } + } +} diff --git a/src/main/java/com/google/inject/internal/InjectorImpl.java b/src/main/java/com/google/inject/internal/InjectorImpl.java new file mode 100644 index 0000000..9ea9da2 --- /dev/null +++ b/src/main/java/com/google/inject/internal/InjectorImpl.java @@ -0,0 +1,1142 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +import com.google.inject.ImplementedBy; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Module; +import com.google.inject.ProvidedBy; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Scope; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.util.SourceProvider; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.ConvertedConstantBinding; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ProviderBinding; +import com.google.inject.spi.TypeConverterBinding; +import com.google.inject.util.Providers; + +import java.lang.annotation.Annotation; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +/** + * Default {@link Injector} implementation. + * + */ +final class InjectorImpl implements Injector, Lookups { + public static final TypeLiteral STRING_TYPE = TypeLiteral.get(String.class); + /** + * Synchronization: map value is modified only for the current thread, + * it's ok to read map values of other threads. It can change between your + * calls. + * + * @see #getGlobalInternalContext + */ + private static final ConcurrentMap globalInternalContext = + Maps.newConcurrentMap(); + final State state; + final InjectorImpl parent; + final BindingsMultimap bindingsMultimap = new BindingsMultimap(); + final InjectorOptions options; + /** + * Just-in-time binding cache. Guarded by state.lock() + */ + final Map, BindingImpl> jitBindings = Maps.newHashMap(); + /** + * Cache of Keys that we were unable to create JIT bindings for, so we don't + * keep trying. Also guarded by state.lock(). + */ + final Set> failedJitBindings = Sets.newHashSet(); + /** + * Cached constructor injectors for each type + */ + final ConstructorInjectorStore constructors = new ConstructorInjectorStore(this); + /** + * @see #getGlobalInternalContext + */ + private final ThreadLocal localContext; + Lookups lookups = new DeferredLookups(this); + /** + * Cached field and method injectors for each type. + */ + MembersInjectorStore membersInjectorStore; + /** + * Cached provision listener callbacks for each key. + */ + ProvisionListenerCallbackStore provisionListenerStore; + + InjectorImpl(InjectorImpl parent, State state, InjectorOptions injectorOptions) { + this.parent = parent; + this.state = state; + this.options = injectorOptions; + + if (parent != null) { + localContext = parent.localContext; + } else { + localContext = new ThreadLocal(); + } + } + + /** + * Returns true if the key type is Provider (but not a subclass of Provider). + */ + private static boolean isProvider(Key key) { + return key.getTypeLiteral().getRawType().equals(Provider.class); + } + + private static boolean isTypeLiteral(Key key) { + return key.getTypeLiteral().getRawType().equals(TypeLiteral.class); + } + + private static Key getProvidedKey(Key> key, Errors errors) throws ErrorsException { + Type providerType = key.getTypeLiteral().getType(); + + // If the Provider has no type parameter (raw Provider)... + if (!(providerType instanceof ParameterizedType)) { + throw errors.cannotInjectRawProvider().toException(); + } + + Type entryType = ((ParameterizedType) providerType).getActualTypeArguments()[0]; + + @SuppressWarnings("unchecked") // safe because T came from Key> + Key providedKey = (Key) key.ofType(entryType); + return providedKey; + } + + /** + * Returns true if the key type is MembersInjector (but not a subclass of MembersInjector). + */ + private static boolean isMembersInjector(Key key) { + return key.getTypeLiteral().getRawType().equals(MembersInjector.class) + && key.getAnnotationType() == null; + } + + /** + * Provides access to the internal context for the current injector of all threads. + * One does not need to use this from Guice source code as context could be passed on the stack. + * It is required for custom scopes which are called from Guice and sometimes do require + * access to current internal context, but it is not passed in. Contrary to {@link #localContext} + * it is not used to store injector-specific state, but to provide easy access to the current + * state. + * + * @return unmodifiable map + */ + static Map getGlobalInternalContext() { + return Collections.unmodifiableMap(globalInternalContext); + } + + /** + * Indexes bindings by type. + */ + void index() { + for (Binding binding : state.getExplicitBindingsThisLevel().values()) { + index(binding); + } + } + + void index(Binding binding) { + bindingsMultimap.put(binding.getKey().getTypeLiteral(), binding); + } + + public List> findBindingsByType(TypeLiteral type) { + return bindingsMultimap.getAll(type); + } + + /** + * Returns the binding for {@code key} + */ + public BindingImpl getBinding(Key key) { + Errors errors = new Errors(key); + try { + BindingImpl result = getBindingOrThrow(key, errors, JitLimitation.EXISTING_JIT); + errors.throwConfigurationExceptionIfErrorsExist(); + return result; + } catch (ErrorsException e) { + throw new ConfigurationException(errors.merge(e.getErrors()).getMessages()); + } + } + + public BindingImpl getExistingBinding(Key key) { + // Check explicit bindings, i.e. bindings created by modules. + BindingImpl explicitBinding = state.getExplicitBinding(key); + if (explicitBinding != null) { + return explicitBinding; + } + synchronized (state.lock()) { + // See if any jit bindings have been created for this key. + for (InjectorImpl injector = this; injector != null; injector = injector.parent) { + @SuppressWarnings("unchecked") + BindingImpl jitBinding = (BindingImpl) injector.jitBindings.get(key); + if (jitBinding != null) { + return jitBinding; + } + } + } + + // If Key is a Provider, we have to see if the type it is providing exists, + // and, if so, we have to create the binding for the provider. + if (isProvider(key)) { + try { + // This is safe because isProvider above ensures that T is a Provider + @SuppressWarnings({"unchecked", "cast"}) + Key providedKey = (Key) getProvidedKey((Key) key, new Errors()); + if (getExistingBinding(providedKey) != null) { + return getBinding(key); + } + } catch (ErrorsException e) { + throw new ConfigurationException(e.getErrors().getMessages()); + } + } + + // No existing binding exists. + return null; + } + + /** + * Gets a binding implementation. First, it check to see if the parent has a binding. If the + * parent has a binding and the binding is scoped, it will use that binding. Otherwise, this + * checks for an explicit binding. If no explicit binding is found, it looks for a just-in-time + * binding. + */ + BindingImpl getBindingOrThrow(Key key, Errors errors, JitLimitation jitType) + throws ErrorsException { + // Check explicit bindings, i.e. bindings created by modules. + BindingImpl binding = state.getExplicitBinding(key); + if (binding != null) { + return binding; + } + + // Look for an on-demand binding. + return getJustInTimeBinding(key, errors, jitType); + } + + public Binding getBinding(Class type) { + return getBinding(Key.get(type)); + } + + public Injector getParent() { + return parent; + } + + public Injector createChildInjector(Iterable modules) { + return new InternalInjectorCreator() + .parentInjector(this) + .addModules(modules) + .build(); + } + + public Injector createChildInjector(Module... modules) { + return createChildInjector(ImmutableList.copyOf(modules)); + } + + /** + * Returns a just-in-time binding for {@code key}, creating it if necessary. + * + * @throws ErrorsException if the binding could not be created. + */ + private BindingImpl getJustInTimeBinding(Key key, Errors errors, JitLimitation jitType) + throws ErrorsException { + + boolean jitOverride = isProvider(key) || isTypeLiteral(key) || isMembersInjector(key); + synchronized (state.lock()) { + // first try to find a JIT binding that we've already created + for (InjectorImpl injector = this; injector != null; injector = injector.parent) { + @SuppressWarnings("unchecked") // we only store bindings that match their key + BindingImpl binding = (BindingImpl) injector.jitBindings.get(key); + + if (binding != null) { + // If we found a JIT binding and we don't allow them, + // fail. (But allow bindings created through TypeConverters.) + if (options.jitDisabled + && jitType == JitLimitation.NO_JIT + && !jitOverride + && !(binding instanceof ConvertedConstantBindingImpl)) { + throw errors.jitDisabled(key).toException(); + } else { + return binding; + } + } + } + + // If we previously failed creating this JIT binding and our Errors has + // already recorded an error, then just directly throw that error. + // We need to do this because it's possible we already cleaned up the + // entry in jitBindings (during cleanup), and we may be trying + // to create it again (in the case of a recursive JIT binding). + // We need both of these guards for different reasons + // failedJitBindings.contains: We want to continue processing if we've never + // failed before, so that our initial error message contains + // as much useful information as possible about what errors exist. + // errors.hasErrors: If we haven't already failed, then it's OK to + // continue processing, to make sure the ultimate error message + // is the correct one. + // See: ImplicitBindingsTest#testRecursiveJitBindingsCleanupCorrectly + // for where this guard compes into play. + if (failedJitBindings.contains(key) && errors.hasErrors()) { + throw errors.toException(); + } + return createJustInTimeBindingRecursive(key, errors, options.jitDisabled, jitType); + } // end synchronized(state.lock()) + } + + private BindingImpl> createMembersInjectorBinding( + Key> key, Errors errors) throws ErrorsException { + Type membersInjectorType = key.getTypeLiteral().getType(); + if (!(membersInjectorType instanceof ParameterizedType)) { + throw errors.cannotInjectRawMembersInjector().toException(); + } + + @SuppressWarnings("unchecked") // safe because T came from Key> + TypeLiteral instanceType = (TypeLiteral) TypeLiteral.get( + ((ParameterizedType) membersInjectorType).getActualTypeArguments()[0]); + MembersInjector membersInjector = membersInjectorStore.get(instanceType, errors); + + InternalFactory> factory = new ConstantFactory>( + Initializables.of(membersInjector)); + + + return new InstanceBindingImpl>(this, key, SourceProvider.UNKNOWN_SOURCE, + factory, ImmutableSet.of(), membersInjector); + } + + /** + * Creates a synthetic binding to {@code Provider}, i.e. a binding to the provider from + * {@code Binding}. + */ + private BindingImpl> createProviderBinding(Key> key, Errors errors) + throws ErrorsException { + Key providedKey = getProvidedKey(key, errors); + BindingImpl delegate = getBindingOrThrow(providedKey, errors, JitLimitation.NO_JIT); + return new ProviderBindingImpl(this, key, delegate); + } + + /** + * Converts a constant string binding to the required type. + * + * @return the binding if it could be resolved, or null if the binding doesn't exist + * @throws com.google.inject.internal.ErrorsException if there was an error resolving the binding + */ + private BindingImpl convertConstantStringBinding(Key key, Errors errors) + throws ErrorsException { + // Find a constant string binding. + Key stringKey = key.ofType(STRING_TYPE); + BindingImpl stringBinding = state.getExplicitBinding(stringKey); + if (stringBinding == null || !stringBinding.isConstant()) { + return null; + } + + String stringValue = stringBinding.getProvider().get(); + Object source = stringBinding.getSource(); + + // Find a matching type converter. + TypeLiteral type = key.getTypeLiteral(); + TypeConverterBinding typeConverterBinding = state.getConverter(stringValue, type, errors, source); + + if (typeConverterBinding == null) { + // No converter can handle the given type. + return null; + } + + // Try to convert the string. A failed conversion results in an error. + try { + @SuppressWarnings("unchecked") // This cast is safe because we double check below. + T converted = (T) typeConverterBinding.getTypeConverter().convert(stringValue, type); + + if (converted == null) { + throw errors.converterReturnedNull(stringValue, source, type, typeConverterBinding) + .toException(); + } + + if (!type.getRawType().isInstance(converted)) { + throw errors.conversionTypeError(stringValue, source, type, typeConverterBinding, converted) + .toException(); + } + + return new ConvertedConstantBindingImpl(this, key, converted, stringBinding, + typeConverterBinding); + } catch (ErrorsException e) { + throw e; + } catch (RuntimeException e) { + throw errors.conversionError(stringValue, source, type, typeConverterBinding, e) + .toException(); + } + } + + void initializeBinding(BindingImpl binding, Errors errors) throws ErrorsException { + if (binding instanceof DelayedInitialize) { + ((DelayedInitialize) binding).initialize(this, errors); + } + } + + void initializeJitBinding(BindingImpl binding, Errors errors) throws ErrorsException { + // Put the partially constructed binding in the map a little early. This enables us to handle + // circular dependencies. Example: FooImpl -> BarImpl -> FooImpl. + // Note: We don't need to synchronize on state.lock() during injector creation. + if (binding instanceof DelayedInitialize) { + Key key = binding.getKey(); + jitBindings.put(key, binding); + boolean successful = false; + DelayedInitialize delayed = (DelayedInitialize) binding; + try { + delayed.initialize(this, errors); + successful = true; + } finally { + if (!successful) { + // We do not pass cb.getInternalConstructor as the second parameter + // so that cached exceptions while constructing it get stored. + // See TypeListenerTest#testTypeListenerThrows + removeFailedJitBinding(binding, null); + cleanup(binding, new HashSet()); + } + } + } + } + + /** + * Iterates through the binding's dependencies to clean up any stray bindings that were leftover + * from a failed JIT binding. This is required because the bindings are eagerly & + * optimistically added to allow circular dependency support, so dependencies may pass where they + * should have failed. + */ + private boolean cleanup(BindingImpl binding, Set encountered) { + boolean bindingFailed = false; + Set> deps = getInternalDependencies(binding); + for (Dependency dep : deps) { + Key depKey = dep.getKey(); + InjectionPoint ip = dep.getInjectionPoint(); + if (encountered.add(depKey)) { // only check if we haven't looked at this key yet + BindingImpl depBinding = jitBindings.get(depKey); + if (depBinding != null) { // if the binding still exists, validate + boolean failed = cleanup(depBinding, encountered); // if children fail, we fail + if (depBinding instanceof ConstructorBindingImpl) { + ConstructorBindingImpl ctorBinding = (ConstructorBindingImpl) depBinding; + ip = ctorBinding.getInternalConstructor(); + if (!ctorBinding.isInitialized()) { + failed = true; + } + } + if (failed) { + removeFailedJitBinding(depBinding, ip); + bindingFailed = true; + } + } else if (state.getExplicitBinding(depKey) == null) { + // ignore keys if they were explicitly bound, but if neither JIT + // nor explicit, it's also invalid & should let parent know. + bindingFailed = true; + } + } + } + return bindingFailed; + } + + /** + * Cleans up any state that may have been cached when constructing the JIT binding. + */ + private void removeFailedJitBinding(Binding binding, InjectionPoint ip) { + failedJitBindings.add(binding.getKey()); + jitBindings.remove(binding.getKey()); + membersInjectorStore.remove(binding.getKey().getTypeLiteral()); + provisionListenerStore.remove(binding); + if (ip != null) { + constructors.remove(ip); + } + } + + /** + * Safely gets the dependencies of possibly not initialized bindings. + */ + @SuppressWarnings("unchecked") + private Set> getInternalDependencies(BindingImpl binding) { + if (binding instanceof ConstructorBindingImpl) { + return ((ConstructorBindingImpl) binding).getInternalDependencies(); + } else if (binding instanceof HasDependencies) { + return ((HasDependencies) binding).getDependencies(); + } else { + return ImmutableSet.of(); + } + } + + /** + * Creates a binding for an injectable type with the given scope. Looks for a scope on the type if + * none is specified. + */ + BindingImpl createUninitializedBinding(Key key, Scoping scoping, Object source, + Errors errors, boolean jitBinding) throws ErrorsException { + Class rawType = key.getTypeLiteral().getRawType(); + + ImplementedBy implementedBy = rawType.getAnnotation(ImplementedBy.class); + + // Don't try to inject arrays or enums annotated with @ImplementedBy. + if (rawType.isArray() || (rawType.isEnum() && implementedBy != null)) { + throw errors.missingImplementation(key).toException(); + } + + // Handle TypeLiteral by binding the inner type + if (rawType == TypeLiteral.class) { + @SuppressWarnings("unchecked") // we have to fudge the inner type as Object + BindingImpl binding = (BindingImpl) createTypeLiteralBinding( + (Key>) key, errors); + return binding; + } + + // Handle @ImplementedBy + if (implementedBy != null) { + Annotations.checkForMisplacedScopeAnnotations(rawType, source, errors); + return createImplementedByBinding(key, scoping, implementedBy, errors); + } + + // Handle @ProvidedBy. + ProvidedBy providedBy = rawType.getAnnotation(ProvidedBy.class); + if (providedBy != null) { + Annotations.checkForMisplacedScopeAnnotations(rawType, source, errors); + return createProvidedByBinding(key, scoping, providedBy, errors); + } + + + return ConstructorBindingImpl.create(this, + key, + null, /* use default constructor */ + source, + scoping, + errors, + jitBinding && options.jitDisabled, + options.atInjectRequired); + } + + /** + * Converts a binding for a {@code Key>} to the value {@code TypeLiteral}. It's + * a bit awkward because we have to pull out the inner type in the type literal. + */ + private BindingImpl> createTypeLiteralBinding( + Key> key, Errors errors) throws ErrorsException { + Type typeLiteralType = key.getTypeLiteral().getType(); + if (!(typeLiteralType instanceof ParameterizedType)) { + throw errors.cannotInjectRawTypeLiteral().toException(); + } + + ParameterizedType parameterizedType = (ParameterizedType) typeLiteralType; + Type innerType = parameterizedType.getActualTypeArguments()[0]; + + // this is unforunate. We don't support building TypeLiterals for type variable like 'T'. If + // this proves problematic, we can probably fix TypeLiteral to support type variables + if (!(innerType instanceof Class) + && !(innerType instanceof GenericArrayType) + && !(innerType instanceof ParameterizedType)) { + throw errors.cannotInjectTypeLiteralOf(innerType).toException(); + } + + @SuppressWarnings("unchecked") // by definition, innerType == T, so this is safe + TypeLiteral value = (TypeLiteral) TypeLiteral.get(innerType); + InternalFactory> factory = new ConstantFactory>( + Initializables.of(value)); + return new InstanceBindingImpl>(this, key, SourceProvider.UNKNOWN_SOURCE, + factory, ImmutableSet.of(), value); + } + + /** + * Creates a binding for a type annotated with @ProvidedBy. + */ + BindingImpl createProvidedByBinding(Key key, Scoping scoping, + ProvidedBy providedBy, Errors errors) throws ErrorsException { + Class rawType = key.getTypeLiteral().getRawType(); + Class> providerType = providedBy.value(); + + // Make sure it's not the same type. TODO: Can we check for deeper loops? + if (providerType == rawType) { + throw errors.recursiveProviderType().toException(); + } + + // Assume the provider provides an appropriate type. We double check at runtime. + @SuppressWarnings("unchecked") + Key> providerKey = (Key>) Key.get(providerType); + ProvidedByInternalFactory internalFactory = + new ProvidedByInternalFactory(rawType, providerType, providerKey); + Object source = rawType; + BindingImpl binding = LinkedProviderBindingImpl.createWithInitializer( + this, + key, + source, + Scoping.scope(key, this, internalFactory, source, scoping), + scoping, + providerKey, + internalFactory); + internalFactory.setProvisionListenerCallback(provisionListenerStore.get(binding)); + return binding; + } + + /** + * Creates a binding for a type annotated with @ImplementedBy. + */ + private BindingImpl createImplementedByBinding(Key key, Scoping scoping, + ImplementedBy implementedBy, Errors errors) + throws ErrorsException { + Class rawType = key.getTypeLiteral().getRawType(); + Class implementationType = implementedBy.value(); + + // Make sure it's not the same type. TODO: Can we check for deeper cycles? + if (implementationType == rawType) { + throw errors.recursiveImplementationType().toException(); + } + + // Make sure implementationType extends type. + if (!rawType.isAssignableFrom(implementationType)) { + throw errors.notASubtype(implementationType, rawType).toException(); + } + + @SuppressWarnings("unchecked") // After the preceding check, this cast is safe. + Class subclass = (Class) implementationType; + + // Look up the target binding. + final Key targetKey = Key.get(subclass); + final BindingImpl targetBinding = getBindingOrThrow(targetKey, errors, JitLimitation.NEW_OR_EXISTING_JIT); + + InternalFactory internalFactory = new InternalFactory() { + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + context.pushState(targetKey, targetBinding.getSource()); + try { + return targetBinding.getInternalFactory().get( + errors.withSource(targetKey), context, dependency, true); + } finally { + context.popState(); + } + } + }; + + Object source = rawType; + return new LinkedBindingImpl( + this, + key, + source, + Scoping.scope(key, this, internalFactory, source, scoping), + scoping, + targetKey); + } + + /** + * Attempts to create a just-in-time binding for {@code key} in the root injector, falling back to + * other ancestor injectors until this injector is tried. + */ + private BindingImpl createJustInTimeBindingRecursive(Key key, Errors errors, + boolean jitDisabled, JitLimitation jitType) throws ErrorsException { + // ask the parent to create the JIT binding + if (parent != null) { + if (jitType == JitLimitation.NEW_OR_EXISTING_JIT + && jitDisabled && !parent.options.jitDisabled) { + // If the binding would be forbidden here but allowed in a parent, report an error instead + throw errors.jitDisabledInParent(key).toException(); + } + + try { + return parent.createJustInTimeBindingRecursive(key, new Errors(), jitDisabled, + parent.options.jitDisabled ? JitLimitation.NO_JIT : jitType); + } catch (ErrorsException ignored) { + } + } + + // Retrieve the sources before checking for blacklisting to guard against sources becoming null + // due to a full GC happening after calling state.isBlacklisted and + // state.getSourcesForBlacklistedKey. + // TODO(user): Consolidate these two APIs. + Set sources = state.getSourcesForBlacklistedKey(key); + if (state.isBlacklisted(key)) { + throw errors.childBindingAlreadySet(key, sources).toException(); + } + + key = MoreTypes.canonicalizeKey(key); // before storing the key long-term, canonicalize it. + BindingImpl binding = createJustInTimeBinding(key, errors, jitDisabled, jitType); + state.parent().blacklist(key, state, binding.getSource()); + jitBindings.put(key, binding); + return binding; + } + + /** + * Returns a new just-in-time binding created by resolving {@code key}. The strategies used to + * create just-in-time bindings are: + *
    + *
  1. Internalizing Providers. If the requested binding is for {@code Provider}, we delegate + * to the binding for {@code T}. + *
  2. Converting constants. + *
  3. ImplementedBy and ProvidedBy annotations. Only for unannotated keys. + *
  4. The constructor of the raw type. Only for unannotated keys. + *
+ * + * @throws com.google.inject.internal.ErrorsException if the binding cannot be created. + */ + private BindingImpl createJustInTimeBinding(Key key, Errors errors, + boolean jitDisabled, JitLimitation jitType) throws ErrorsException { + int numErrorsBefore = errors.size(); + + // Retrieve the sources before checking for blacklisting to guard against sources becoming null + // due to a full GC happening after calling state.isBlacklisted and + // state.getSourcesForBlacklistedKey. + // TODO(user): Consolidate these two APIs. + Set sources = state.getSourcesForBlacklistedKey(key); + if (state.isBlacklisted(key)) { + throw errors.childBindingAlreadySet(key, sources).toException(); + } + + // Handle cases where T is a Provider. + if (isProvider(key)) { + // These casts are safe. We know T extends Provider and that given Key>, + // createProviderBinding() will return BindingImpl>. + @SuppressWarnings({"unchecked", "cast"}) + BindingImpl binding = (BindingImpl) createProviderBinding((Key) key, errors); + return binding; + } + + // Handle cases where T is a MembersInjector + if (isMembersInjector(key)) { + // These casts are safe. T extends MembersInjector and that given Key>, + // createMembersInjectorBinding() will return BindingImpl>. + @SuppressWarnings({"unchecked", "cast"}) + BindingImpl binding = (BindingImpl) createMembersInjectorBinding((Key) key, errors); + return binding; + } + + // Try to convert a constant string binding to the requested type. + BindingImpl convertedBinding = convertConstantStringBinding(key, errors); + if (convertedBinding != null) { + return convertedBinding; + } + + if (!isTypeLiteral(key) + && jitDisabled + && jitType != JitLimitation.NEW_OR_EXISTING_JIT) { + throw errors.jitDisabled(key).toException(); + } + + // If the key has an annotation... + if (key.getAnnotationType() != null) { + // Look for a binding without annotation attributes or return null. + if (key.hasAttributes() && !options.exactBindingAnnotationsRequired) { + try { + Errors ignored = new Errors(); + return getBindingOrThrow(key.withoutAttributes(), ignored, JitLimitation.NO_JIT); + } catch (ErrorsException ignored) { + // throw with a more appropriate message below + } + } + throw errors.missingImplementation(key).toException(); + } + + Object source = key.getTypeLiteral().getRawType(); + BindingImpl binding = createUninitializedBinding(key, Scoping.UNSCOPED, source, errors, true); + errors.throwIfNewErrors(numErrorsBefore); + initializeJitBinding(binding, errors); + return binding; + } + + InternalFactory getInternalFactory(Key key, Errors errors, JitLimitation jitType) + throws ErrorsException { + return getBindingOrThrow(key, errors, jitType).getInternalFactory(); + } + + public Map, Binding> getBindings() { + return state.getExplicitBindingsThisLevel(); + } + + public Map, Binding> getAllBindings() { + synchronized (state.lock()) { + return new ImmutableMap.Builder, Binding>() + .putAll(state.getExplicitBindingsThisLevel()) + .putAll(jitBindings) + .build(); + } + } + + public Map, Scope> getScopeBindings() { + return ImmutableMap.copyOf(state.getScopes()); + } + + public Set getTypeConverterBindings() { + return ImmutableSet.copyOf(state.getConvertersThisLevel()); + } + + /** + * Returns parameter injectors, or {@code null} if there are no parameters. + */ + SingleParameterInjector[] getParametersInjectors( + List> parameters, Errors errors) throws ErrorsException { + if (parameters.isEmpty()) { + return null; + } + + int numErrorsBefore = errors.size(); + SingleParameterInjector[] result = new SingleParameterInjector[parameters.size()]; + int i = 0; + for (Dependency parameter : parameters) { + try { + result[i++] = createParameterInjector(parameter, errors.withSource(parameter)); + } catch (ErrorsException rethrownBelow) { + // rethrown below + } + } + + errors.throwIfNewErrors(numErrorsBefore); + return result; + } + + SingleParameterInjector createParameterInjector(final Dependency dependency, + final Errors errors) throws ErrorsException { + BindingImpl binding = getBindingOrThrow(dependency.getKey(), errors, JitLimitation.NO_JIT); + return new SingleParameterInjector(dependency, binding); + } + + @SuppressWarnings("unchecked") // the members injector type is consistent with instance's type + public void injectMembers(Object instance) { + MembersInjector membersInjector = getMembersInjector(instance.getClass()); + membersInjector.injectMembers(instance); + } + + public MembersInjector getMembersInjector(TypeLiteral typeLiteral) { + Errors errors = new Errors(typeLiteral); + try { + return membersInjectorStore.get(typeLiteral, errors); + } catch (ErrorsException e) { + throw new ConfigurationException(errors.merge(e.getErrors()).getMessages()); + } + } + + public MembersInjector getMembersInjector(Class type) { + return getMembersInjector(TypeLiteral.get(type)); + } + + public Provider getProvider(Class type) { + return getProvider(Key.get(type)); + } + + Provider getProviderOrThrow(final Dependency dependency, Errors errors) throws ErrorsException { + final Key key = dependency.getKey(); + final BindingImpl binding = getBindingOrThrow(key, errors, JitLimitation.NO_JIT); + + return new Provider() { + public T get() { + final Errors errors = new Errors(dependency); + try { + T t = callInContext(new ContextualCallable() { + public T call(InternalContext context) throws ErrorsException { + Dependency previous = context.pushDependency(dependency, binding.getSource()); + try { + return binding.getInternalFactory().get(errors, context, dependency, false); + } finally { + context.popStateAndSetDependency(previous); + } + } + }); + errors.throwIfNewErrors(0); + return t; + } catch (ErrorsException e) { + throw new ProvisionException(errors.merge(e.getErrors()).getMessages()); + } + } + + @Override + public String toString() { + return binding.getInternalFactory().toString(); + } + }; + } + + public Provider getProvider(final Key key) { + Errors errors = new Errors(key); + try { + Provider result = getProviderOrThrow(Dependency.get(key), errors); + errors.throwIfNewErrors(0); + return result; + } catch (ErrorsException e) { + throw new ConfigurationException(errors.merge(e.getErrors()).getMessages()); + } + } + + public T getInstance(Key key) { + return getProvider(key).get(); + } + + public T getInstance(Class type) { + return getProvider(type).get(); + } + + /** + * Looks up thread local context. Creates (and removes) a new context if necessary. + */ + T callInContext(ContextualCallable callable) throws ErrorsException { + Object[] reference = localContext.get(); + if (reference == null) { + reference = new Object[1]; + localContext.set(reference); + } + Thread currentThread = Thread.currentThread(); + if (reference[0] == null) { + reference[0] = new InternalContext(options); + globalInternalContext.put(currentThread, (InternalContext) reference[0]); + try { + return callable.call((InternalContext) reference[0]); + } finally { + // Only clear contexts if this call created them. + reference[0] = null; + globalInternalContext.remove(currentThread); + } + } else { + Object previousGlobalInternalContext = globalInternalContext.get(currentThread); + globalInternalContext.put(currentThread, (InternalContext) reference[0]); + try { + // Someone else will clean up this local context. + return callable.call((InternalContext) reference[0]); + } finally { + if (previousGlobalInternalContext != null) { + globalInternalContext.put(currentThread, (InternalContext) previousGlobalInternalContext); + } else { + globalInternalContext.remove(currentThread); + } + } + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(Injector.class) + .add("bindings", state.getExplicitBindingsThisLevel().values()) + .toString(); + } + + /** + * some limitations on what just in time bindings are allowed. + */ + enum JitLimitation { + /** + * does not allow just in time bindings + */ + NO_JIT, + /** + * allows existing just in time bindings, but does not allow new ones + */ + EXISTING_JIT, + /** + * allows existing just in time bindings & allows new ones to be created + */ + NEW_OR_EXISTING_JIT, + } + + /** + * Invokes a method. + */ + interface MethodInvoker { + Object invoke(Object target, Object... parameters) + throws IllegalAccessException, InvocationTargetException; + } + + /** + * Options that control how the injector behaves. + */ + static class InjectorOptions { + final Stage stage; + final boolean jitDisabled; + final boolean disableCircularProxies; + final boolean atInjectRequired; + final boolean exactBindingAnnotationsRequired; + + InjectorOptions(Stage stage, boolean jitDisabled, boolean disableCircularProxies, + boolean atInjectRequired, boolean exactBindingAnnotationsRequired) { + this.stage = stage; + this.jitDisabled = jitDisabled; + this.disableCircularProxies = disableCircularProxies; + this.atInjectRequired = atInjectRequired; + this.exactBindingAnnotationsRequired = exactBindingAnnotationsRequired; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(getClass()) + .add("stage", stage) + .add("jitDisabled", jitDisabled) + .add("disableCircularProxies", disableCircularProxies) + .add("atInjectRequired", atInjectRequired) + .add("exactBindingAnnotationsRequired", exactBindingAnnotationsRequired) + .toString(); + } + } + + private static class ProviderBindingImpl extends BindingImpl> + implements ProviderBinding>, HasDependencies { + final BindingImpl providedBinding; + + ProviderBindingImpl(InjectorImpl injector, Key> key, Binding providedBinding) { + super(injector, key, providedBinding.getSource(), createInternalFactory(providedBinding), + Scoping.UNSCOPED); + this.providedBinding = (BindingImpl) providedBinding; + } + + static InternalFactory> createInternalFactory(Binding providedBinding) { + final Provider provider = providedBinding.getProvider(); + return new InternalFactory>() { + public Provider get(Errors errors, InternalContext context, Dependency dependency, boolean linked) { + return provider; + } + }; + } + + public Key getProvidedKey() { + return providedBinding.getKey(); + } + + public V acceptTargetVisitor(BindingTargetVisitor, V> visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + throw new UnsupportedOperationException("This element represents a synthetic binding."); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ProviderBinding.class) + .add("key", getKey()) + .add("providedKey", getProvidedKey()) + .toString(); + } + + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(getProvidedKey())); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ProviderBindingImpl) { + ProviderBindingImpl o = (ProviderBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(providedBinding, o.providedBinding); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping(), providedBinding); + } + } + + private static class ConvertedConstantBindingImpl + extends BindingImpl implements ConvertedConstantBinding { + final T value; + final Provider provider; + final Binding originalBinding; + final TypeConverterBinding typeConverterBinding; + + ConvertedConstantBindingImpl( + InjectorImpl injector, Key key, T value, Binding originalBinding, + TypeConverterBinding typeConverterBinding) { + super(injector, key, originalBinding.getSource(), + new ConstantFactory(Initializables.of(value)), Scoping.UNSCOPED); + this.value = value; + provider = Providers.of(value); + this.originalBinding = originalBinding; + this.typeConverterBinding = typeConverterBinding; + } + + @Override + public Provider getProvider() { + return provider; + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + return visitor.visit(this); + } + + public T getValue() { + return value; + } + + public TypeConverterBinding getTypeConverterBinding() { + return typeConverterBinding; + } + + public Key getSourceKey() { + return originalBinding.getKey(); + } + + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(getSourceKey())); + } + + public void applyTo(Binder binder) { + throw new UnsupportedOperationException("This element represents a synthetic binding."); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ConvertedConstantBinding.class) + .add("key", getKey()) + .add("sourceKey", getSourceKey()) + .add("value", value) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ConvertedConstantBindingImpl) { + ConvertedConstantBindingImpl o = (ConvertedConstantBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(value, o.value); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping(), value); + } + } + + private static class BindingsMultimap { + final Map, List>> multimap = Maps.newHashMap(); + + void put(TypeLiteral type, Binding binding) { + List> bindingsForType = multimap.get(type); + if (bindingsForType == null) { + bindingsForType = Lists.newArrayList(); + multimap.put(type, bindingsForType); + } + bindingsForType.add(binding); + } + + + @SuppressWarnings("unchecked") + // safe because we only put matching entries into the map + List> getAll(TypeLiteral type) { + List> bindings = multimap.get(type); + return bindings != null + ? Collections.>unmodifiableList((List) multimap.get(type)) + : ImmutableList.>of(); + } + } +} diff --git a/src/main/java/com/google/inject/internal/InjectorOptionsProcessor.java b/src/main/java/com/google/inject/internal/InjectorOptionsProcessor.java new file mode 100644 index 0000000..d84e8c3 --- /dev/null +++ b/src/main/java/com/google/inject/internal/InjectorOptionsProcessor.java @@ -0,0 +1,71 @@ +package com.google.inject.internal; + +import com.google.inject.Stage; +import com.google.inject.internal.InjectorImpl.InjectorOptions; +import com.google.inject.spi.DisableCircularProxiesOption; +import com.google.inject.spi.RequireAtInjectOnConstructorsOption; +import com.google.inject.spi.RequireExactBindingAnnotationsOption; +import com.google.inject.spi.RequireExplicitBindingsOption; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * A processor to gather injector options. + */ +class InjectorOptionsProcessor extends AbstractProcessor { + + private boolean disableCircularProxies = false; + private boolean jitDisabled = false; + private boolean atInjectRequired = false; + private boolean exactBindingAnnotationsRequired = false; + + InjectorOptionsProcessor(Errors errors) { + super(errors); + } + + @Override + public Boolean visit(DisableCircularProxiesOption option) { + disableCircularProxies = true; + return true; + } + + @Override + public Boolean visit(RequireExplicitBindingsOption option) { + jitDisabled = true; + return true; + } + + @Override + public Boolean visit(RequireAtInjectOnConstructorsOption option) { + atInjectRequired = true; + return true; + } + + @Override + public Boolean visit(RequireExactBindingAnnotationsOption option) { + exactBindingAnnotationsRequired = true; + return true; + } + + InjectorOptions getOptions(Stage stage, InjectorOptions parentOptions) { + checkNotNull(stage, "stage must be set"); + if (parentOptions == null) { + return new InjectorOptions( + stage, + jitDisabled, + disableCircularProxies, + atInjectRequired, + exactBindingAnnotationsRequired); + } else { + checkState(stage == parentOptions.stage, "child & parent stage don't match"); + return new InjectorOptions( + stage, + jitDisabled || parentOptions.jitDisabled, + disableCircularProxies || parentOptions.disableCircularProxies, + atInjectRequired || parentOptions.atInjectRequired, + exactBindingAnnotationsRequired || parentOptions.exactBindingAnnotationsRequired); + } + } + +} diff --git a/src/main/java/com/google/inject/internal/InjectorShell.java b/src/main/java/com/google/inject/internal/InjectorShell.java new file mode 100644 index 0000000..542b3fd --- /dev/null +++ b/src/main/java/com/google/inject/internal/InjectorShell.java @@ -0,0 +1,296 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import com.google.inject.internal.InjectorImpl.InjectorOptions; +import com.google.inject.internal.util.SourceProvider; +import com.google.inject.internal.util.Stopwatch; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; +import com.google.inject.spi.PrivateElements; +import com.google.inject.spi.ProvisionListenerBinding; +import com.google.inject.spi.TypeListenerBinding; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.inject.Scopes.SINGLETON; + +/** + * A partially-initialized injector. See {@link InternalInjectorCreator}, which + * uses this to build a tree of injectors in batch. + */ +final class InjectorShell { + + private final List elements; + private final InjectorImpl injector; + + private InjectorShell(Builder builder, List elements, InjectorImpl injector) { + this.elements = elements; + this.injector = injector; + } + + /** + * The Injector is a special case because we allow both parent and child injectors to both have + * a binding for that key. + */ + private static void bindInjector(InjectorImpl injector) { + Key key = Key.get(Injector.class); + InjectorFactory injectorFactory = new InjectorFactory(injector); + injector.state.putBinding(key, + new ProviderInstanceBindingImpl(injector, key, SourceProvider.UNKNOWN_SOURCE, + injectorFactory, Scoping.UNSCOPED, injectorFactory, + ImmutableSet.of())); + } + + /** + * The Logger is a special case because it knows the injection point of the injected member. It's + * the only binding that does this. + */ + /*private static void bindLogger(InjectorImpl injector) { + Key key = Key.get(Logger.class); + LoggerFactory loggerFactory = new LoggerFactory(); + injector.state.putBinding(key, + new ProviderInstanceBindingImpl(injector, key, + SourceProvider.UNKNOWN_SOURCE, loggerFactory, Scoping.UNSCOPED, + loggerFactory, ImmutableSet.of())); + }*/ + + private static void bindStage(InjectorImpl injector, Stage stage) { + Key key = Key.get(Stage.class); + InstanceBindingImpl stageBinding = new InstanceBindingImpl( + injector, + key, + SourceProvider.UNKNOWN_SOURCE, + new ConstantFactory(Initializables.of(stage)), + ImmutableSet.of(), + stage); + injector.state.putBinding(key, stageBinding); + } + + InjectorImpl getInjector() { + return injector; + } + + List getElements() { + return elements; + } + + static class Builder { + private final List elements = Lists.newArrayList(); + private final List modules = Lists.newArrayList(); + + /** + * lazily constructed + */ + private State state; + + private InjectorImpl parent; + private InjectorOptions options; + private Stage stage; + + /** + * null unless this exists in a {@link Binder#newPrivateBinder private environment} + */ + private PrivateElementsImpl privateElements; + + Builder stage(Stage stage) { + this.stage = stage; + return this; + } + + Builder parent(InjectorImpl parent) { + this.parent = parent; + this.state = new InheritingState(parent.state); + this.options = parent.options; + this.stage = options.stage; + return this; + } + + Builder privateElements(PrivateElements privateElements) { + this.privateElements = (PrivateElementsImpl) privateElements; + this.elements.addAll(privateElements.getElements()); + return this; + } + + void addModules(Iterable modules) { + if (modules != null) { + for (Module module : modules) { + this.modules.add(module); + } + } + } + + Stage getStage() { + return options.stage; + } + + /** + * Synchronize on this before calling {@link #build}. + */ + Object lock() { + return getState().lock(); + } + + /** + * Creates and returns the injector shells for the current modules. Multiple shells will be + * returned if any modules contain {@link Binder#newPrivateBinder private environments}. The + * primary injector will be first in the returned list. + */ + List build( + Initializer initializer, + ProcessedBindingData bindingData, + Stopwatch stopwatch, + Errors errors) { + checkState(stage != null, "Stage not initialized"); + checkState(privateElements == null || parent != null, "PrivateElements with no parent"); + checkState(state != null, "no state. Did you remember to lock() ?"); + + // bind Singleton if this is a top-level injector + if (parent == null) { + modules.add(0, new RootModule()); + } else { + modules.add(0, new InheritedScannersModule(parent.state)); + } + elements.addAll(Elements.getElements(stage, modules)); + + // Look for injector-changing options + InjectorOptionsProcessor optionsProcessor = new InjectorOptionsProcessor(errors); + optionsProcessor.process(null, elements); + options = optionsProcessor.getOptions(stage, options); + + InjectorImpl injector = new InjectorImpl(parent, state, options); + if (privateElements != null) { + privateElements.initInjector(injector); + } + + // add default type converters if this is a top-level injector + if (parent == null) { + TypeConverterBindingProcessor.prepareBuiltInConverters(injector); + } + + stopwatch.resetAndLog("Module execution"); + + new MessageProcessor(errors).process(injector, elements); + + new ListenerBindingProcessor(errors).process(injector, elements); + List typeListenerBindings = injector.state.getTypeListenerBindings(); + injector.membersInjectorStore = new MembersInjectorStore(injector, typeListenerBindings); + List provisionListenerBindings = + injector.state.getProvisionListenerBindings(); + injector.provisionListenerStore = + new ProvisionListenerCallbackStore(provisionListenerBindings); + stopwatch.resetAndLog("TypeListeners & ProvisionListener creation"); + + new ScopeBindingProcessor(errors).process(injector, elements); + stopwatch.resetAndLog("Scopes creation"); + + new TypeConverterBindingProcessor(errors).process(injector, elements); + stopwatch.resetAndLog("Converters creation"); + + bindStage(injector, stage); + bindInjector(injector); + //bindLogger(injector); + + // Process all normal bindings, then UntargettedBindings. + // This is necessary because UntargettedBindings can create JIT bindings + // and need all their other dependencies set up ahead of time. + new BindingProcessor(errors, initializer, bindingData).process(injector, elements); + new UntargettedBindingProcessor(errors, bindingData).process(injector, elements); + stopwatch.resetAndLog("Binding creation"); + + new ModuleAnnotatedMethodScannerProcessor(errors).process(injector, elements); + stopwatch.resetAndLog("Module annotated method scanners creation"); + + List injectorShells = Lists.newArrayList(); + injectorShells.add(new InjectorShell(this, elements, injector)); + + // recursively build child shells + PrivateElementProcessor processor = new PrivateElementProcessor(errors); + processor.process(injector, elements); + for (Builder builder : processor.getInjectorShellBuilders()) { + injectorShells.addAll(builder.build(initializer, bindingData, stopwatch, errors)); + } + stopwatch.resetAndLog("Private environment creation"); + + return injectorShells; + } + + private State getState() { + if (state == null) { + state = new InheritingState(State.NONE); + } + return state; + } + } + + private static class InjectorFactory implements InternalFactory, Provider { + private final Injector injector; + + private InjectorFactory(Injector injector) { + this.injector = injector; + } + + public Injector get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + return injector; + } + + public Injector get() { + return injector; + } + + public String toString() { + return "Provider"; + } + } + + /*private static class LoggerFactory implements InternalFactory, Provider { + public Logger get(Errors errors, InternalContext context, Dependency dependency, boolean linked) { + InjectionPoint injectionPoint = dependency.getInjectionPoint(); + return injectionPoint == null + ? Logger.getAnonymousLogger() + : Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); + } + + public Logger get() { + return Logger.getAnonymousLogger(); + } + + public String toString() { + return "Provider"; + } + }*/ + + private static class RootModule implements Module { + public void configure(Binder binder) { + binder = binder.withSource(SourceProvider.UNKNOWN_SOURCE); + binder.bindScope(Singleton.class, SINGLETON); + binder.bindScope(javax.inject.Singleton.class, SINGLETON); + } + } + + private static class InheritedScannersModule implements Module { + private final State state; + + InheritedScannersModule(State state) { + this.state = state; + } + + public void configure(Binder binder) { + for (ModuleAnnotatedMethodScannerBinding binding : state.getScannerBindings()) { + binding.applyTo(binder); + } + } + } +} diff --git a/src/main/java/com/google/inject/internal/InstanceBindingImpl.java b/src/main/java/com/google/inject/internal/InstanceBindingImpl.java new file mode 100644 index 0000000..bb72868 --- /dev/null +++ b/src/main/java/com/google/inject/internal/InstanceBindingImpl.java @@ -0,0 +1,102 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.util.Providers; + +import java.util.Set; + +final class InstanceBindingImpl extends BindingImpl implements InstanceBinding { + + final T instance; + final Provider provider; + final ImmutableSet injectionPoints; + + public InstanceBindingImpl(InjectorImpl injector, Key key, Object source, + InternalFactory internalFactory, Set injectionPoints, + T instance) { + super(injector, key, source, internalFactory, Scoping.EAGER_SINGLETON); + this.injectionPoints = ImmutableSet.copyOf(injectionPoints); + this.instance = instance; + this.provider = Providers.of(instance); + } + + public InstanceBindingImpl(Object source, Key key, Scoping scoping, + Set injectionPoints, T instance) { + super(source, key, scoping); + this.injectionPoints = ImmutableSet.copyOf(injectionPoints); + this.instance = instance; + this.provider = Providers.of(instance); + } + + @Override + public Provider getProvider() { + return this.provider; + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + return visitor.visit(this); + } + + public T getInstance() { + return instance; + } + + public Set getInjectionPoints() { + return injectionPoints; + } + + public Set> getDependencies() { + return instance instanceof HasDependencies + ? ImmutableSet.copyOf(((HasDependencies) instance).getDependencies()) + : Dependency.forInjectionPoints(injectionPoints); + } + + public BindingImpl withScoping(Scoping scoping) { + return new InstanceBindingImpl(getSource(), getKey(), scoping, injectionPoints, instance); + } + + public BindingImpl withKey(Key key) { + return new InstanceBindingImpl(getSource(), key, getScoping(), injectionPoints, instance); + } + + public void applyTo(Binder binder) { + // instance bindings aren't scoped + binder.withSource(getSource()).bind(getKey()).toInstance(instance); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(InstanceBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .add("instance", instance) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof InstanceBindingImpl) { + InstanceBindingImpl o = (InstanceBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(instance, o.instance); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping()); + } +} diff --git a/src/main/java/com/google/inject/internal/InternalContext.java b/src/main/java/com/google/inject/internal/InternalContext.java new file mode 100644 index 0000000..298c4c8 --- /dev/null +++ b/src/main/java/com/google/inject/internal/InternalContext.java @@ -0,0 +1,136 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.google.inject.Key; +import com.google.inject.internal.InjectorImpl.InjectorOptions; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.DependencyAndSource; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Internal context. Used to coordinate injections and support circular + * dependencies. + */ +final class InternalContext { + + private final InjectorOptions options; + /** + * Keeps track of the hierarchy of types needed during injection. + */ + private final DependencyStack state = new DependencyStack(); + private Map> constructionContexts = Maps.newHashMap(); + /** + * Keeps track of the type that is currently being requested for injection. + */ + private Dependency dependency; + + InternalContext(InjectorOptions options) { + this.options = options; + } + + public InjectorOptions getInjectorOptions() { + return options; + } + + @SuppressWarnings("unchecked") + public ConstructionContext getConstructionContext(Object key) { + ConstructionContext constructionContext + = (ConstructionContext) constructionContexts.get(key); + if (constructionContext == null) { + constructionContext = new ConstructionContext(); + constructionContexts.put(key, constructionContext); + } + return constructionContext; + } + + public Dependency getDependency() { + return dependency; + } + + /** + * Sets the new current dependency & adds it to the state. + */ + public Dependency pushDependency(Dependency dependency, Object source) { + Dependency previous = this.dependency; + this.dependency = dependency; + state.add(dependency, source); + return previous; + } + + /** + * Pops the current state & sets the new dependency. + */ + public void popStateAndSetDependency(Dependency newDependency) { + state.pop(); + this.dependency = newDependency; + } + + /** + * Adds to the state without setting the dependency. + */ + public void pushState(Key key, Object source) { + state.add(key, source); + } + + /** + * Pops from the state without setting a dependency. + */ + public void popState() { + state.pop(); + } + + /** + * Returns the current dependency chain (all the state). + */ + public List getDependencyChain() { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < state.size(); i += 2) { + Object evenEntry = state.get(i); + Dependency dependency; + if (evenEntry instanceof Key) { + dependency = Dependency.get((Key) evenEntry); + } else { + dependency = (Dependency) evenEntry; + } + builder.add(new DependencyAndSource(dependency, state.get(i + 1))); + } + return builder.build(); + } + + /** + * Keeps track of the hierarchy of types needed during injection. + * + *

This is a pairwise combination of dependencies and sources, with dependencies or keys on + * even indices, and sources on odd indices. This structure is to avoid the memory overhead of + * DependencyAndSource objects, which can add to several tens of megabytes in large applications. + */ + private static final class DependencyStack { + private Object[] elements = new Object[16]; + private int size = 0; + + public void add(Object dependencyOrKey, Object source) { + if (elements.length < size + 2) { + elements = Arrays.copyOf(elements, (elements.length * 3) / 2 + 2); + } + elements[size++] = dependencyOrKey; + elements[size++] = source; + } + + public void pop() { + elements[--size] = null; + elements[--size] = null; + } + + public Object get(int i) { + return elements[i]; + } + + public int size() { + return size; + } + } +} diff --git a/src/main/java/com/google/inject/internal/InternalFactory.java b/src/main/java/com/google/inject/internal/InternalFactory.java new file mode 100644 index 0000000..99a564b --- /dev/null +++ b/src/main/java/com/google/inject/internal/InternalFactory.java @@ -0,0 +1,20 @@ +package com.google.inject.internal; + +import com.google.inject.spi.Dependency; + +/** + * Creates objects which will be injected. + */ +interface InternalFactory { + + /** + * Creates an object to be injected. + * + * @param context of this injection + * @param linked true if getting as a result of a linked binding + * @return instance to be injected + * @throws com.google.inject.internal.ErrorsException if a value cannot be provided + */ + T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException; +} diff --git a/src/main/java/com/google/inject/internal/InternalFactoryToInitializableAdapter.java b/src/main/java/com/google/inject/internal/InternalFactoryToInitializableAdapter.java new file mode 100644 index 0000000..bb8520c --- /dev/null +++ b/src/main/java/com/google/inject/internal/InternalFactoryToInitializableAdapter.java @@ -0,0 +1,45 @@ +package com.google.inject.internal; + +import com.google.inject.spi.Dependency; +import com.google.inject.spi.ProviderInstanceBinding; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Adapts {@link ProviderInstanceBinding} providers, ensuring circular proxies + * fail (or proxy) properly. + */ +final class InternalFactoryToInitializableAdapter extends ProviderInternalFactory { + + private final ProvisionListenerStackCallback provisionCallback; + private final Initializable> initializable; + + public InternalFactoryToInitializableAdapter( + Initializable> initializable, + Object source, ProvisionListenerStackCallback provisionCallback) { + super(source); + this.provisionCallback = checkNotNull(provisionCallback, "provisionCallback"); + this.initializable = checkNotNull(initializable, "provider"); + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + return circularGet(initializable.get(errors), errors, context, dependency, + provisionCallback); + } + + @Override + protected T provision(javax.inject.Provider provider, Errors errors, + Dependency dependency, ConstructionContext constructionContext) throws ErrorsException { + try { + return super.provision(provider, errors, dependency, constructionContext); + } catch (RuntimeException userException) { + throw errors.withSource(source).errorInProvider(userException).toException(); + } + } + + @Override + public String toString() { + return initializable.toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/InternalFactoryToProviderAdapter.java b/src/main/java/com/google/inject/internal/InternalFactoryToProviderAdapter.java new file mode 100644 index 0000000..fa5025e --- /dev/null +++ b/src/main/java/com/google/inject/internal/InternalFactoryToProviderAdapter.java @@ -0,0 +1,32 @@ +package com.google.inject.internal; + +import com.google.inject.Provider; +import com.google.inject.spi.Dependency; + +import static com.google.common.base.Preconditions.checkNotNull; + +final class InternalFactoryToProviderAdapter implements InternalFactory { + + private final Provider provider; + private final Object source; + + public InternalFactoryToProviderAdapter(Provider provider, Object source) { + this.provider = checkNotNull(provider, "provider"); + this.source = checkNotNull(source, "source"); + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + // TODO(sameb): Does this need to push state into the context? + try { + return errors.checkForNull(provider.get(), source, dependency); + } catch (RuntimeException userException) { + throw errors.withSource(source).errorInProvider(userException).toException(); + } + } + + @Override + public String toString() { + return provider.toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/InternalFlags.java b/src/main/java/com/google/inject/internal/InternalFlags.java new file mode 100644 index 0000000..cc6db1f --- /dev/null +++ b/src/main/java/com/google/inject/internal/InternalFlags.java @@ -0,0 +1,132 @@ +package com.google.inject.internal; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +/** + * Contains flags for Guice. + */ +public class InternalFlags { + + private static final IncludeStackTraceOption INCLUDE_STACK_TRACES + = parseIncludeStackTraceOption(); + + private static final CustomClassLoadingOption CUSTOM_CLASS_LOADING + = parseCustomClassLoadingOption(); + + private static final NullableProvidesOption NULLABLE_PROVIDES + = parseNullableProvidesOption(NullableProvidesOption.ERROR); + + public static IncludeStackTraceOption getIncludeStackTraceOption() { + return INCLUDE_STACK_TRACES; + } + + public static CustomClassLoadingOption getCustomClassLoadingOption() { + return CUSTOM_CLASS_LOADING; + } + + public static NullableProvidesOption getNullableProvidesOption() { + return NULLABLE_PROVIDES; + } + + private static IncludeStackTraceOption parseIncludeStackTraceOption() { + return getSystemOption("guice_include_stack_traces", + IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE); + } + + private static CustomClassLoadingOption parseCustomClassLoadingOption() { + return getSystemOption("guice_custom_class_loading", + CustomClassLoadingOption.BRIDGE, CustomClassLoadingOption.OFF); + } + + private static NullableProvidesOption parseNullableProvidesOption( + NullableProvidesOption defaultValue) { + return getSystemOption("guice_check_nullable_provides_params", defaultValue); + } + + /** + * Gets the system option indicated by the specified key; runs as a privileged action. + * + * @param name of the system option + * @param defaultValue if the option is not set + * @return value of the option, defaultValue if not set + */ + private static > T getSystemOption(final String name, T defaultValue) { + return getSystemOption(name, defaultValue, defaultValue); + } + + /** + * Gets the system option indicated by the specified key; runs as a privileged action. + * + * @param name of the system option + * @param defaultValue if the option is not set + * @param secureValue if the security manager disallows access to the option + * @return value of the option, defaultValue if not set, secureValue if no access + */ + private static > T getSystemOption(final String name, T defaultValue, + T secureValue) { + Class enumType = defaultValue.getDeclaringClass(); + String value = null; + try { + value = AccessController.doPrivileged(new PrivilegedAction() { + public String run() { + return System.getProperty(name); + } + }); + return (value != null && value.length() > 0) ? Enum.valueOf(enumType, value) : defaultValue; + } catch (SecurityException e) { + return secureValue; + } catch (IllegalArgumentException e) { + //logger.warning(value + " is not a valid flag value for " + name + ". " + // + " Values must be one of " + Arrays.asList(enumType.getEnumConstants())); + return defaultValue; + } + } + + /** + * The options for Guice stack trace collection. + */ + public enum IncludeStackTraceOption { + /** + * No stack trace collection + */ + OFF, + /** + * Minimum stack trace collection (Default) + */ + ONLY_FOR_DECLARING_SOURCE, + /** + * Full stack trace for everything + */ + COMPLETE + } + + /** + * The options for Guice custom class loading. + */ + public enum CustomClassLoadingOption { + /** + * No custom class loading + */ + OFF, + /** + * Automatically bridge between class loaders (Default) + */ + BRIDGE + } + + public enum NullableProvidesOption { + /** + * Ignore null parameters to @Provides methods. + */ + IGNORE, + /** + * Warn if null parameters are passed to non-@Nullable parameters of provides methods. + */ + WARN, + /** + * Error if null parameters are passed to non-@Nullable parameters of provides parameters + */ + ERROR + } +} diff --git a/src/main/java/com/google/inject/internal/InternalInjectorCreator.java b/src/main/java/com/google/inject/internal/InternalInjectorCreator.java new file mode 100644 index 0000000..5d9bec3 --- /dev/null +++ b/src/main/java/com/google/inject/internal/InternalInjectorCreator.java @@ -0,0 +1,308 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.util.Stopwatch; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.TypeConverterBinding; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Builds a tree of injectors. This is a primary injector, plus child injectors needed for each + * {@code Binder.newPrivateBinder() private environment}. The primary injector is not necessarily a + * top-level injector. + * + *

Injector construction happens in two phases. + *

    + *
  1. Static building. In this phase, we interpret commands, create bindings, and inspect + * dependencies. During this phase, we hold a lock to ensure consistency with parent injectors. + * No user code is executed in this phase.
  2. + *
  3. Dynamic injection. In this phase, we call user code. We inject members that requested + * injection. This may require user's objects be created and their providers be called. And we + * create eager singletons. In this phase, user code may have started other threads. This phase + * is not executed for injectors created using {@link Stage#TOOL the tool stage}
  4. + *
+ * + */ +public final class InternalInjectorCreator { + + private final Stopwatch stopwatch = new Stopwatch(); + private final Errors errors = new Errors(); + + private final Initializer initializer = new Initializer(); + private final ProcessedBindingData bindingData; + private final InjectionRequestProcessor injectionRequestProcessor; + + private final InjectorShell.Builder shellBuilder = new InjectorShell.Builder(); + private List shells; + + public InternalInjectorCreator() { + injectionRequestProcessor = new InjectionRequestProcessor(errors, initializer); + bindingData = new ProcessedBindingData(); + } + + public InternalInjectorCreator stage(Stage stage) { + shellBuilder.stage(stage); + return this; + } + + /** + * Sets the parent of the injector to-be-constructed. As a side effect, this sets this injector's + * stage to the stage of {@code parent}. + */ + public InternalInjectorCreator parentInjector(InjectorImpl parent) { + shellBuilder.parent(parent); + return this; + } + + public InternalInjectorCreator addModules(Iterable modules) { + shellBuilder.addModules(modules); + return this; + } + + public Injector build() { + + // Synchronize while we're building up the bindings and other injector state. This ensures that + // the JIT bindings in the parent injector don't change while we're being built + synchronized (shellBuilder.lock()) { + shells = shellBuilder.build(initializer, bindingData, stopwatch, errors); + stopwatch.resetAndLog("Injector construction"); + + initializeStatically(); + } + + injectDynamically(); + + if (shellBuilder.getStage() == Stage.TOOL) { + // wrap the primaryInjector in a ToolStageInjector + // to prevent non-tool-friendy methods from being called. + return new ToolStageInjector(primaryInjector()); + } else { + return primaryInjector(); + } + } + + /** + * Initialize and validate everything. + */ + private void initializeStatically() { + bindingData.initializeBindings(); + stopwatch.resetAndLog("Binding initialization"); + + for (InjectorShell shell : shells) { + shell.getInjector().index(); + } + stopwatch.resetAndLog("Binding indexing"); + + injectionRequestProcessor.process(shells); + stopwatch.resetAndLog("Collecting injection requests"); + + bindingData.runCreationListeners(errors); + stopwatch.resetAndLog("Binding validation"); + + injectionRequestProcessor.validate(); + stopwatch.resetAndLog("Static validation"); + + initializer.validateOustandingInjections(errors); + stopwatch.resetAndLog("Instance member validation"); + + new LookupProcessor(errors).process(shells); + for (InjectorShell shell : shells) { + ((DeferredLookups) shell.getInjector().lookups).initialize(errors); + } + stopwatch.resetAndLog("Provider verification"); + + for (InjectorShell shell : shells) { + if (!shell.getElements().isEmpty()) { + throw new AssertionError("Failed to execute " + shell.getElements()); + } + } + + errors.throwCreationExceptionIfErrorsExist(); + } + + /** + * Returns the injector being constructed. This is not necessarily the root injector. + */ + private Injector primaryInjector() { + return shells.get(0).getInjector(); + } + + /** + * Inject everything that can be injected. This method is intentionally not synchronized. If we + * locked while injecting members (ie. running user code), things would deadlock should the user + * code build a just-in-time binding from another thread. + */ + private void injectDynamically() { + injectionRequestProcessor.injectMembers(); + stopwatch.resetAndLog("Static member injection"); + + initializer.injectAll(errors); + stopwatch.resetAndLog("Instance injection"); + errors.throwCreationExceptionIfErrorsExist(); + + if (shellBuilder.getStage() != Stage.TOOL) { + for (InjectorShell shell : shells) { + loadEagerSingletons(shell.getInjector(), shellBuilder.getStage(), errors); + } + stopwatch.resetAndLog("Preloading singletons"); + } + errors.throwCreationExceptionIfErrorsExist(); + } + + /** + * Loads eager singletons, or all singletons if we're in Stage.PRODUCTION. Bindings discovered + * while we're binding these singletons are not be eager. + */ + void loadEagerSingletons(InjectorImpl injector, Stage stage, final Errors errors) { + @SuppressWarnings("unchecked") // casting Collection to Collection is safe + Iterable> candidateBindings = ImmutableList.copyOf(Iterables.concat( + (Collection) injector.state.getExplicitBindingsThisLevel().values(), + injector.jitBindings.values())); + for (final BindingImpl binding : candidateBindings) { + if (isEagerSingleton(injector, binding, stage)) { + try { + injector.callInContext(new ContextualCallable() { + Dependency dependency = Dependency.get(binding.getKey()); + + public Void call(InternalContext context) { + Dependency previous = context.pushDependency(dependency, binding.getSource()); + Errors errorsForBinding = errors.withSource(dependency); + try { + binding.getInternalFactory().get(errorsForBinding, context, dependency, false); + } catch (ErrorsException e) { + errorsForBinding.merge(e.getErrors()); + } finally { + context.popStateAndSetDependency(previous); + } + + return null; + } + }); + } catch (ErrorsException e) { + throw new AssertionError(); + } + } + } + } + + private boolean isEagerSingleton(InjectorImpl injector, BindingImpl binding, Stage stage) { + if (binding.getScoping().isEagerSingleton(stage)) { + return true; + } + + // handle a corner case where a child injector links to a binding in a parent injector, and + // that binding is singleton. We won't catch this otherwise because we only iterate the child's + // bindings. + if (binding instanceof LinkedBindingImpl) { + Key linkedBinding = ((LinkedBindingImpl) binding).getLinkedKey(); + return isEagerSingleton(injector, injector.getBinding(linkedBinding), stage); + } + + return false; + } + + /** + * {@link Injector} exposed to users in {@link Stage#TOOL}. + */ + static class ToolStageInjector implements Injector { + private final Injector delegateInjector; + + ToolStageInjector(Injector delegateInjector) { + this.delegateInjector = delegateInjector; + } + + public void injectMembers(Object o) { + throw new UnsupportedOperationException( + "Injector.injectMembers(Object) is not supported in Stage.TOOL"); + } + + public Map, Binding> getBindings() { + return this.delegateInjector.getBindings(); + } + + public Map, Binding> getAllBindings() { + return this.delegateInjector.getAllBindings(); + } + + public Binding getBinding(Key key) { + return this.delegateInjector.getBinding(key); + } + + public Binding getBinding(Class type) { + return this.delegateInjector.getBinding(type); + } + + public Binding getExistingBinding(Key key) { + return this.delegateInjector.getExistingBinding(key); + } + + public List> findBindingsByType(TypeLiteral type) { + return this.delegateInjector.findBindingsByType(type); + } + + public Injector getParent() { + return delegateInjector.getParent(); + } + + public Injector createChildInjector(Iterable modules) { + return delegateInjector.createChildInjector(modules); + } + + public Injector createChildInjector(Module... modules) { + return delegateInjector.createChildInjector(modules); + } + + public Map, Scope> getScopeBindings() { + return delegateInjector.getScopeBindings(); + } + + public Set getTypeConverterBindings() { + return delegateInjector.getTypeConverterBindings(); + } + + public Provider getProvider(Key key) { + throw new UnsupportedOperationException( + "Injector.getProvider(Key) is not supported in Stage.TOOL"); + } + + public Provider getProvider(Class type) { + throw new UnsupportedOperationException( + "Injector.getProvider(Class) is not supported in Stage.TOOL"); + } + + public MembersInjector getMembersInjector(TypeLiteral typeLiteral) { + throw new UnsupportedOperationException( + "Injector.getMembersInjector(TypeLiteral) is not supported in Stage.TOOL"); + } + + public MembersInjector getMembersInjector(Class type) { + throw new UnsupportedOperationException( + "Injector.getMembersInjector(Class) is not supported in Stage.TOOL"); + } + + public T getInstance(Key key) { + throw new UnsupportedOperationException( + "Injector.getInstance(Key) is not supported in Stage.TOOL"); + } + + public T getInstance(Class type) { + throw new UnsupportedOperationException( + "Injector.getInstance(Class) is not supported in Stage.TOOL"); + } + } +} diff --git a/src/main/java/com/google/inject/internal/LinkedBindingImpl.java b/src/main/java/com/google/inject/internal/LinkedBindingImpl.java new file mode 100644 index 0000000..4b90fee --- /dev/null +++ b/src/main/java/com/google/inject/internal/LinkedBindingImpl.java @@ -0,0 +1,81 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.LinkedKeyBinding; + +import java.util.Set; + +public final class LinkedBindingImpl extends BindingImpl implements LinkedKeyBinding, HasDependencies { + + final Key targetKey; + + public LinkedBindingImpl(InjectorImpl injector, Key key, Object source, + InternalFactory internalFactory, Scoping scoping, + Key targetKey) { + super(injector, key, source, internalFactory, scoping); + this.targetKey = targetKey; + } + + public LinkedBindingImpl(Object source, Key key, Scoping scoping, Key targetKey) { + super(source, key, scoping); + this.targetKey = targetKey; + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + return visitor.visit(this); + } + + public Key getLinkedKey() { + return targetKey; + } + + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(targetKey)); + } + + public BindingImpl withScoping(Scoping scoping) { + return new LinkedBindingImpl(getSource(), getKey(), scoping, targetKey); + } + + public BindingImpl withKey(Key key) { + return new LinkedBindingImpl(getSource(), key, getScoping(), targetKey); + } + + public void applyTo(Binder binder) { + getScoping().applyTo(binder.withSource(getSource()).bind(getKey()).to(getLinkedKey())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(LinkedKeyBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .add("scope", getScoping()) + .add("target", targetKey) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LinkedBindingImpl) { + LinkedBindingImpl o = (LinkedBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(targetKey, o.targetKey); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping(), targetKey); + } +} diff --git a/src/main/java/com/google/inject/internal/LinkedProviderBindingImpl.java b/src/main/java/com/google/inject/internal/LinkedProviderBindingImpl.java new file mode 100644 index 0000000..1482ed4 --- /dev/null +++ b/src/main/java/com/google/inject/internal/LinkedProviderBindingImpl.java @@ -0,0 +1,108 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.ProviderKeyBinding; + +import java.util.Set; + +final class LinkedProviderBindingImpl + extends BindingImpl implements ProviderKeyBinding, HasDependencies, DelayedInitialize { + + final Key> providerKey; + final DelayedInitialize delayedInitializer; + + private LinkedProviderBindingImpl(InjectorImpl injector, Key key, Object source, + InternalFactory internalFactory, Scoping scoping, + Key> providerKey, + DelayedInitialize delayedInitializer) { + super(injector, key, source, internalFactory, scoping); + this.providerKey = providerKey; + this.delayedInitializer = delayedInitializer; + } + + public LinkedProviderBindingImpl(InjectorImpl injector, Key key, Object source, + InternalFactory internalFactory, Scoping scoping, + Key> providerKey) { + this(injector, key, source, internalFactory, scoping, providerKey, null); + } + + LinkedProviderBindingImpl(Object source, Key key, Scoping scoping, + Key> providerKey) { + super(source, key, scoping); + this.providerKey = providerKey; + this.delayedInitializer = null; + } + + static LinkedProviderBindingImpl createWithInitializer(InjectorImpl injector, Key key, + Object source, InternalFactory internalFactory, Scoping scoping, + Key> providerKey, + DelayedInitialize delayedInitializer) { + return new LinkedProviderBindingImpl(injector, key, source, internalFactory, scoping, + providerKey, delayedInitializer); + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + return visitor.visit(this); + } + + public Key> getProviderKey() { + return providerKey; + } + + public void initialize(InjectorImpl injector, Errors errors) throws ErrorsException { + if (delayedInitializer != null) { + delayedInitializer.initialize(injector, errors); + } + } + + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(providerKey)); + } + + public BindingImpl withScoping(Scoping scoping) { + return new LinkedProviderBindingImpl(getSource(), getKey(), scoping, providerKey); + } + + public BindingImpl withKey(Key key) { + return new LinkedProviderBindingImpl(getSource(), key, getScoping(), providerKey); + } + + public void applyTo(Binder binder) { + getScoping().applyTo(binder.withSource(getSource()) + .bind(getKey()).toProvider(getProviderKey())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ProviderKeyBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .add("scope", getScoping()) + .add("provider", providerKey) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LinkedProviderBindingImpl) { + LinkedProviderBindingImpl o = (LinkedProviderBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(providerKey, o.providerKey); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping(), providerKey); + } +} diff --git a/src/main/java/com/google/inject/internal/ListenerBindingProcessor.java b/src/main/java/com/google/inject/internal/ListenerBindingProcessor.java new file mode 100644 index 0000000..ba77ef2 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ListenerBindingProcessor.java @@ -0,0 +1,26 @@ +package com.google.inject.internal; + +import com.google.inject.spi.ProvisionListenerBinding; +import com.google.inject.spi.TypeListenerBinding; + +/** + * Handles {@code Binder#bindListener} commands. + */ +final class ListenerBindingProcessor extends AbstractProcessor { + + ListenerBindingProcessor(Errors errors) { + super(errors); + } + + @Override + public Boolean visit(TypeListenerBinding binding) { + injector.state.addTypeListener(binding); + return true; + } + + @Override + public Boolean visit(ProvisionListenerBinding binding) { + injector.state.addProvisionListener(binding); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/LookupProcessor.java b/src/main/java/com/google/inject/internal/LookupProcessor.java new file mode 100644 index 0000000..633eb0a --- /dev/null +++ b/src/main/java/com/google/inject/internal/LookupProcessor.java @@ -0,0 +1,42 @@ +package com.google.inject.internal; + +import com.google.inject.MembersInjector; +import com.google.inject.Provider; +import com.google.inject.spi.MembersInjectorLookup; +import com.google.inject.spi.ProviderLookup; + +/** + * Handles {@code Binder.getProvider} and {@code Binder.getMembersInjector(TypeLiteral)} commands. + */ +final class LookupProcessor extends AbstractProcessor { + + LookupProcessor(Errors errors) { + super(errors); + } + + @Override + public Boolean visit(MembersInjectorLookup lookup) { + try { + MembersInjector membersInjector + = injector.membersInjectorStore.get(lookup.getType(), errors); + lookup.initializeDelegate(membersInjector); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); // TODO: source + } + + return true; + } + + @Override + public Boolean visit(ProviderLookup lookup) { + // ensure the provider can be created + try { + Provider provider = injector.getProviderOrThrow(lookup.getDependency(), errors); + lookup.initializeDelegate(provider); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); // TODO: source + } + + return true; + } +} diff --git a/src/main/java/com/google/inject/internal/Lookups.java b/src/main/java/com/google/inject/internal/Lookups.java new file mode 100644 index 0000000..fab0a2d --- /dev/null +++ b/src/main/java/com/google/inject/internal/Lookups.java @@ -0,0 +1,17 @@ +package com.google.inject.internal; + +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; + +/** + * Accessors for providers and members injectors. The returned values will not be functional until + * the injector has been created. + */ +interface Lookups { + + Provider getProvider(Key key); + + MembersInjector getMembersInjector(TypeLiteral type); +} diff --git a/src/main/java/com/google/inject/internal/MembersInjectorImpl.java b/src/main/java/com/google/inject/internal/MembersInjectorImpl.java new file mode 100644 index 0000000..aacb75d --- /dev/null +++ b/src/main/java/com/google/inject/internal/MembersInjectorImpl.java @@ -0,0 +1,138 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.ProvisionListenerStackCallback.ProvisionCallback; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.InjectionPoint; + +/** + * Injects members of instances of a given type. + */ +final class MembersInjectorImpl implements MembersInjector { + private final TypeLiteral typeLiteral; + private final InjectorImpl injector; + private final ImmutableList memberInjectors; + private final ImmutableSet> userMembersInjectors; + private final ImmutableSet> injectionListeners; + + MembersInjectorImpl(InjectorImpl injector, TypeLiteral typeLiteral, + EncounterImpl encounter, ImmutableList memberInjectors) { + this.injector = injector; + this.typeLiteral = typeLiteral; + this.memberInjectors = memberInjectors; + this.userMembersInjectors = encounter.getMembersInjectors(); + this.injectionListeners = encounter.getInjectionListeners(); + } + + public ImmutableList getMemberInjectors() { + return memberInjectors; + } + + public void injectMembers(T instance) { + Errors errors = new Errors(typeLiteral); + try { + injectAndNotify(instance, errors, null, null, typeLiteral, false); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + + errors.throwProvisionExceptionIfErrorsExist(); + } + + void injectAndNotify(final T instance, + final Errors errors, + final Key key, // possibly null! + final ProvisionListenerStackCallback provisionCallback, // possibly null! + final Object source, + final boolean toolableOnly) throws ErrorsException { + if (instance == null) { + return; + } + + injector.callInContext(new ContextualCallable() { + @Override + public Void call(final InternalContext context) throws ErrorsException { + context.pushState(key, source); + try { + if (provisionCallback != null && provisionCallback.hasListeners()) { + provisionCallback.provision(errors, context, new ProvisionCallback() { + @Override + public T call() { + injectMembers(instance, errors, context, toolableOnly); + return instance; + } + }); + } else { + injectMembers(instance, errors, context, toolableOnly); + } + } finally { + context.popState(); + } + return null; + } + }); + + // TODO: We *could* notify listeners too here, + // but it's not clear if we want to. There's no way to know + // if a MembersInjector from the usersMemberInjector list wants + // toolable injections, so do we really want to notify + // about injection? (We could take a strategy of only notifying + // if atleast one InjectionPoint was toolable, in which case + // the above callInContext could return 'true' if it injected + // anything.) + if (!toolableOnly) { + notifyListeners(instance, errors); + } + } + + void notifyListeners(T instance, Errors errors) throws ErrorsException { + int numErrorsBefore = errors.size(); + for (InjectionListener injectionListener : injectionListeners) { + try { + injectionListener.afterInjection(instance); + } catch (RuntimeException e) { + errors.errorNotifyingInjectionListener(injectionListener, typeLiteral, e); + } + } + errors.throwIfNewErrors(numErrorsBefore); + } + + void injectMembers(T t, Errors errors, InternalContext context, boolean toolableOnly) { + // optimization: use manual for/each to save allocating an iterator here + for (int i = 0, size = memberInjectors.size(); i < size; i++) { + SingleMemberInjector injector = memberInjectors.get(i); + if (!toolableOnly || injector.getInjectionPoint().isToolable()) { + injector.inject(errors, context, t); + } + } + + // TODO: There's no way to know if a user's MembersInjector wants toolable injections. + if (!toolableOnly) { + for (MembersInjector userMembersInjector : userMembersInjectors) { + try { + userMembersInjector.injectMembers(t); + } catch (RuntimeException e) { + errors.errorInUserInjector(userMembersInjector, typeLiteral, e); + } + } + } + } + + @Override + public String toString() { + return "MembersInjector<" + typeLiteral + ">"; + } + + public ImmutableSet getInjectionPoints() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (SingleMemberInjector memberInjector : memberInjectors) { + builder.add(memberInjector.getInjectionPoint()); + } + return builder.build(); + } + +} diff --git a/src/main/java/com/google/inject/internal/MembersInjectorStore.java b/src/main/java/com/google/inject/internal/MembersInjectorStore.java new file mode 100644 index 0000000..0524864 --- /dev/null +++ b/src/main/java/com/google/inject/internal/MembersInjectorStore.java @@ -0,0 +1,124 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.ConfigurationException; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.TypeListener; +import com.google.inject.spi.TypeListenerBinding; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; + +/** + * Members injectors by type. + */ +final class MembersInjectorStore { + private final InjectorImpl injector; + private final ImmutableList typeListenerBindings; + + private final FailableCache, MembersInjectorImpl> cache + = new FailableCache, MembersInjectorImpl>() { + @Override + protected MembersInjectorImpl create(TypeLiteral type, Errors errors) + throws ErrorsException { + return createWithListeners(type, errors); + } + }; + + MembersInjectorStore(InjectorImpl injector, + List typeListenerBindings) { + this.injector = injector; + this.typeListenerBindings = ImmutableList.copyOf(typeListenerBindings); + } + + /** + * Returns true if any type listeners are installed. Other code may take shortcuts when there + * aren't any type listeners. + */ + public boolean hasTypeListeners() { + return !typeListenerBindings.isEmpty(); + } + + /** + * Returns a new complete members injector with injection listeners registered. + */ + @SuppressWarnings("unchecked") // the MembersInjector type always agrees with the passed type + public MembersInjectorImpl get(TypeLiteral key, Errors errors) throws ErrorsException { + return (MembersInjectorImpl) cache.get(key, errors); + } + + /** + * Purges a type literal from the cache. Use this only if the type is not actually valid for + * binding and needs to be purged. (See issue 319 and + * ImplicitBindingTest#testCircularJitBindingsLeaveNoResidue and + * #testInstancesRequestingProvidersForThemselvesWithChildInjectors for examples of when this is + * necessary.) + * + * Returns true if the type was stored in the cache, false otherwise. + */ + boolean remove(TypeLiteral type) { + return cache.remove(type); + } + + /** + * Creates a new members injector and attaches both injection listeners and method aspects. + */ + private MembersInjectorImpl createWithListeners(TypeLiteral type, Errors errors) + throws ErrorsException { + int numErrorsBefore = errors.size(); + + Set injectionPoints; + try { + injectionPoints = InjectionPoint.forInstanceMethodsAndFields(type); + } catch (ConfigurationException e) { + errors.merge(e.getErrorMessages()); + injectionPoints = e.getPartialValue(); + } + ImmutableList injectors = getInjectors(injectionPoints, errors); + errors.throwIfNewErrors(numErrorsBefore); + + EncounterImpl encounter = new EncounterImpl(errors, injector.lookups); + Set alreadySeenListeners = Sets.newHashSet(); + for (TypeListenerBinding binding : typeListenerBindings) { + TypeListener typeListener = binding.getListener(); + if (!alreadySeenListeners.contains(typeListener) && binding.getTypeMatcher().matches(type)) { + alreadySeenListeners.add(typeListener); + try { + typeListener.hear(type, encounter); + } catch (RuntimeException e) { + errors.errorNotifyingTypeListener(binding, type, e); + } + } + } + encounter.invalidate(); + errors.throwIfNewErrors(numErrorsBefore); + + return new MembersInjectorImpl(injector, type, encounter, injectors); + } + + /** + * Returns the injectors for the specified injection points. + */ + ImmutableList getInjectors( + Set injectionPoints, Errors errors) { + List injectors = Lists.newArrayList(); + for (InjectionPoint injectionPoint : injectionPoints) { + try { + Errors errorsForMember = injectionPoint.isOptional() + ? new Errors(injectionPoint) + : errors.withSource(injectionPoint); + SingleMemberInjector injector = injectionPoint.getMember() instanceof Field + ? new SingleFieldInjector(this.injector, injectionPoint, errorsForMember) + : new SingleMethodInjector(this.injector, injectionPoint, errorsForMember); + injectors.add(injector); + } catch (ErrorsException ignoredForNow) { + // ignored for now + } + } + return ImmutableList.copyOf(injectors); + } +} diff --git a/src/main/java/com/google/inject/internal/MessageProcessor.java b/src/main/java/com/google/inject/internal/MessageProcessor.java new file mode 100644 index 0000000..9437891 --- /dev/null +++ b/src/main/java/com/google/inject/internal/MessageProcessor.java @@ -0,0 +1,32 @@ +package com.google.inject.internal; + +import com.google.inject.spi.Message; + +/** + * Handles {@code Binder.addError} commands. + * + */ +final class MessageProcessor extends AbstractProcessor { + + MessageProcessor(Errors errors) { + super(errors); + } + + public static String getRootMessage(Throwable t) { + Throwable cause = t.getCause(); + return cause == null ? t.toString() : getRootMessage(cause); + } + + @Override + public Boolean visit(Message message) { + if (message.getCause() != null) { + String rootMessage = getRootMessage(message.getCause()); + /*logger.log(Level.INFO, + "An exception was caught and reported. Message: " + rootMessage, + message.getCause());*/ + } + + errors.addMessage(message); + return true; + } +} diff --git a/src/main/java/com/google/inject/internal/ModuleAnnotatedMethodScannerProcessor.java b/src/main/java/com/google/inject/internal/ModuleAnnotatedMethodScannerProcessor.java new file mode 100644 index 0000000..f3e48c2 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ModuleAnnotatedMethodScannerProcessor.java @@ -0,0 +1,19 @@ +package com.google.inject.internal; + +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; + +/** + * Handles {@code Binder.scanModulesForAnnotatedMethods} commands. + */ +final class ModuleAnnotatedMethodScannerProcessor extends AbstractProcessor { + + ModuleAnnotatedMethodScannerProcessor(Errors errors) { + super(errors); + } + + @Override + public Boolean visit(ModuleAnnotatedMethodScannerBinding command) { + injector.state.addScanner(command); + return true; + } +} diff --git a/src/main/java/com/google/inject/internal/MoreTypes.java b/src/main/java/com/google/inject/internal/MoreTypes.java new file mode 100644 index 0000000..ce35127 --- /dev/null +++ b/src/main/java/com/google/inject/internal/MoreTypes.java @@ -0,0 +1,535 @@ +package com.google.inject.internal; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; +import com.google.inject.ConfigurationException; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.util.Types; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Map; +import java.util.NoSuchElementException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Static methods for working with types that we aren't publishing in the + * public {@code Types} API. + */ +public class MoreTypes { + + public static final Type[] EMPTY_TYPE_ARRAY = new Type[]{}; + + private MoreTypes() { + } + + private static final Map, TypeLiteral> PRIMITIVE_TO_WRAPPER + = new ImmutableMap.Builder, TypeLiteral>() + .put(TypeLiteral.get(boolean.class), TypeLiteral.get(Boolean.class)) + .put(TypeLiteral.get(byte.class), TypeLiteral.get(Byte.class)) + .put(TypeLiteral.get(short.class), TypeLiteral.get(Short.class)) + .put(TypeLiteral.get(int.class), TypeLiteral.get(Integer.class)) + .put(TypeLiteral.get(long.class), TypeLiteral.get(Long.class)) + .put(TypeLiteral.get(float.class), TypeLiteral.get(Float.class)) + .put(TypeLiteral.get(double.class), TypeLiteral.get(Double.class)) + .put(TypeLiteral.get(char.class), TypeLiteral.get(Character.class)) + .put(TypeLiteral.get(void.class), TypeLiteral.get(Void.class)) + .build(); + + /** + * Returns a key that doesn't hold any references to parent classes. + * This is necessary for anonymous keys, so ensure we don't hold a ref + * to the containing module (or class) forever. + */ + public static Key canonicalizeKey(Key key) { + // If we know this isn't a subclass, return as-is. + // Otherwise, recreate the key to avoid the subclass + if (key.getClass() == Key.class) { + return key; + } else if (key.getAnnotation() != null) { + return Key.get(key.getTypeLiteral(), key.getAnnotation()); + } else if (key.getAnnotationType() != null) { + return Key.get(key.getTypeLiteral(), key.getAnnotationType()); + } else { + return Key.get(key.getTypeLiteral()); + } + } + + /** + * Returns an type that's appropriate for use in a key. + * + *

If the raw type of {@code typeLiteral} is a {@code javax.inject.Provider}, this returns a + * {@code com.google.inject.Provider} with the same type parameters. + * + *

If the type is a primitive, the corresponding wrapper type will be returned. + * + * @throws ConfigurationException if {@code type} contains a type variable + */ + public static TypeLiteral canonicalizeForKey(TypeLiteral typeLiteral) { + Type type = typeLiteral.getType(); + if (!isFullySpecified(type)) { + Errors errors = new Errors().keyNotFullySpecified(typeLiteral); + throw new ConfigurationException(errors.getMessages()); + } + + if (typeLiteral.getRawType() == javax.inject.Provider.class) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // the following casts are generally unsafe, but com.google.inject.Provider extends + // javax.inject.Provider and is covariant + @SuppressWarnings("unchecked") + TypeLiteral guiceProviderType = (TypeLiteral) TypeLiteral.get( + Types.providerOf(parameterizedType.getActualTypeArguments()[0])); + return guiceProviderType; + } + + @SuppressWarnings("unchecked") + TypeLiteral wrappedPrimitives = (TypeLiteral) PRIMITIVE_TO_WRAPPER.get(typeLiteral); + if (wrappedPrimitives != null) { + return wrappedPrimitives; + } + + // If we know this isn't a subclass, return as-is. + if (typeLiteral.getClass() == TypeLiteral.class) { + return typeLiteral; + } + + // recreate the TypeLiteral to avoid anonymous TypeLiterals from holding refs to their + // surrounding classes. + @SuppressWarnings("unchecked") + TypeLiteral recreated = (TypeLiteral) TypeLiteral.get(typeLiteral.getType()); + return recreated; + } + + /** + * Returns true if {@code type} is free from type variables. + */ + private static boolean isFullySpecified(Type type) { + if (type instanceof Class) { + return true; + + } else if (type instanceof CompositeType) { + return ((CompositeType) type).isFullySpecified(); + + } else { + return !(type instanceof TypeVariable) && ((CompositeType) canonicalize(type)).isFullySpecified(); + } + } + + /** + * Returns a type that is functionally equal but not necessarily equal + * according to {@link Object#equals(Object) Object.equals()}. + */ + public static Type canonicalize(Type type) { + if (type instanceof Class) { + Class c = (Class) type; + return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c; + + } else if (type instanceof CompositeType) { + return type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType) type; + return new ParameterizedTypeImpl(p.getOwnerType(), + p.getRawType(), p.getActualTypeArguments()); + + } else if (type instanceof GenericArrayType) { + GenericArrayType g = (GenericArrayType) type; + return new GenericArrayTypeImpl(g.getGenericComponentType()); + + } else if (type instanceof WildcardType) { + WildcardType w = (WildcardType) type; + return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds()); + + } else { + return type; + } + } + + public static Class getRawType(Type type) { + if (type instanceof Class) { + // type is a normal class. + return (Class) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // I'm not exactly sure why getRawType() returns Type instead of Class. + // Neal isn't either but suspects some pathological case related + // to nested classes exists. + Type rawType = parameterizedType.getRawType(); + checkArgument(rawType instanceof Class, + "Expected a Class, but <%s> is of type %s", type, type.getClass().getName()); + return (Class) rawType; + + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + return Array.newInstance(getRawType(componentType), 0).getClass(); + + } else if (type instanceof TypeVariable) { + // we could use the variable's bounds, but that'll won't work if there are multiple. + // having a raw type that's more general than necessary is okay + return Object.class; + + } else { + throw new IllegalArgumentException("Expected a Class, ParameterizedType, or " + + "GenericArrayType, but <" + type + "> is of type " + type.getClass().getName()); + } + } + + /** + * Returns true if {@code a} and {@code b} are equal. + */ + public static boolean equals(Type a, Type b) { + if (a == b) { + // also handles (a == null && b == null) + return true; + + } else if (a instanceof Class) { + // Class already specifies equals(). + return a.equals(b); + + } else if (a instanceof ParameterizedType) { + if (!(b instanceof ParameterizedType)) { + return false; + } + + // TODO: save a .clone() call + ParameterizedType pa = (ParameterizedType) a; + ParameterizedType pb = (ParameterizedType) b; + return Objects.equal(pa.getOwnerType(), pb.getOwnerType()) + && pa.getRawType().equals(pb.getRawType()) + && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); + + } else if (a instanceof GenericArrayType) { + if (!(b instanceof GenericArrayType)) { + return false; + } + + GenericArrayType ga = (GenericArrayType) a; + GenericArrayType gb = (GenericArrayType) b; + return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); + + } else if (a instanceof WildcardType) { + if (!(b instanceof WildcardType)) { + return false; + } + + WildcardType wa = (WildcardType) a; + WildcardType wb = (WildcardType) b; + return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) + && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); + + } else if (a instanceof TypeVariable) { + if (!(b instanceof TypeVariable)) { + return false; + } + TypeVariable va = (TypeVariable) a; + TypeVariable vb = (TypeVariable) b; + return va.getGenericDeclaration().equals(vb.getGenericDeclaration()) + && va.getName().equals(vb.getName()); + + } else { + // This isn't a type we support. Could be a generic array type, wildcard type, etc. + return false; + } + } + + private static int hashCodeOrZero(Object o) { + return o != null ? o.hashCode() : 0; + } + + public static String typeToString(Type type) { + return type instanceof Class ? ((Class) type).getName() : type.toString(); + } + + /** + * Returns the generic supertype for {@code type}. For example, given a class {@code IntegerSet}, + * the result for when supertype is {@code Set.class} is {@code Set} and the result + * when the supertype is {@code Collection.class} is {@code Collection}. + */ + public static Type getGenericSupertype(Type type, Class rawType, Class toResolve) { + if (toResolve == rawType) { + return type; + } + + // we skip searching through interfaces if unknown is an interface + if (toResolve.isInterface()) { + Class[] interfaces = rawType.getInterfaces(); + for (int i = 0, length = interfaces.length; i < length; i++) { + if (interfaces[i] == toResolve) { + return rawType.getGenericInterfaces()[i]; + } else if (toResolve.isAssignableFrom(interfaces[i])) { + return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve); + } + } + } + + // check our supertypes + if (!rawType.isInterface()) { + while (rawType != Object.class) { + Class rawSupertype = rawType.getSuperclass(); + if (rawSupertype == toResolve) { + return rawType.getGenericSuperclass(); + } else if (toResolve.isAssignableFrom(rawSupertype)) { + return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve); + } + rawType = rawSupertype; + } + } + + // we can't resolve this further + return toResolve; + } + + public static Type resolveTypeVariable(Type type, Class rawType, TypeVariable unknown) { + Class declaredByRaw = declaringClassOf(unknown); + + // we can't reduce this further + if (declaredByRaw == null) { + return unknown; + } + + Type declaredBy = getGenericSupertype(type, rawType, declaredByRaw); + if (declaredBy instanceof ParameterizedType) { + int index = indexOf(declaredByRaw.getTypeParameters(), unknown); + return ((ParameterizedType) declaredBy).getActualTypeArguments()[index]; + } + + return unknown; + } + + private static int indexOf(Object[] array, Object toFind) { + for (int i = 0; i < array.length; i++) { + if (toFind.equals(array[i])) { + return i; + } + } + throw new NoSuchElementException(); + } + + /** + * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by + * a class. + */ + private static Class declaringClassOf(TypeVariable typeVariable) { + GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + return genericDeclaration instanceof Class + ? (Class) genericDeclaration + : null; + } + + private static void checkNotPrimitive(Type type, String use) { + checkArgument(!(type instanceof Class) || !((Class) type).isPrimitive(), + "Primitive types are not allowed in %s: %s", use, type); + } + + /** + * A type formed from other types, such as arrays, parameterized types or wildcard types + */ + private interface CompositeType { + /** + * Returns true if there are no type variables in this type. + */ + boolean isFullySpecified(); + } + + public static class ParameterizedTypeImpl + implements ParameterizedType, CompositeType { + private final Type ownerType; + private final Type rawType; + private final Type[] typeArguments; + + public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { + // require an owner type if the raw type needs it + ensureOwnerType(ownerType, rawType); + + this.ownerType = ownerType == null ? null : canonicalize(ownerType); + this.rawType = canonicalize(rawType); + this.typeArguments = typeArguments.clone(); + for (int t = 0; t < this.typeArguments.length; t++) { + checkNotNull(this.typeArguments[t], "type parameter"); + checkNotPrimitive(this.typeArguments[t], "type parameters"); + this.typeArguments[t] = canonicalize(this.typeArguments[t]); + } + } + + private static void ensureOwnerType(Type ownerType, Type rawType) { + if (rawType instanceof Class) { + Class rawTypeAsClass = (Class) rawType; + checkArgument(ownerType != null || rawTypeAsClass.getEnclosingClass() == null, + "No owner type for enclosed %s", rawType); + checkArgument(ownerType == null || rawTypeAsClass.getEnclosingClass() != null, + "Owner type for unenclosed %s", rawType); + } + } + + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + public Type getRawType() { + return rawType; + } + + public Type getOwnerType() { + return ownerType; + } + + public boolean isFullySpecified() { + if (ownerType != null && !MoreTypes.isFullySpecified(ownerType)) { + return false; + } + + if (!MoreTypes.isFullySpecified(rawType)) { + return false; + } + + for (Type type : typeArguments) { + if (!MoreTypes.isFullySpecified(type)) { + return false; + } + } + + return true; + } + + @Override + public boolean equals(Object other) { + return other instanceof ParameterizedType + && MoreTypes.equals(this, (ParameterizedType) other); + } + + @Override + public int hashCode() { + return Arrays.hashCode(typeArguments) + ^ rawType.hashCode() + ^ hashCodeOrZero(ownerType); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(30 * (typeArguments.length + 1)); + stringBuilder.append(typeToString(rawType)); + + if (typeArguments.length == 0) { + return stringBuilder.toString(); + } + + stringBuilder.append("<").append(typeToString(typeArguments[0])); + for (int i = 1; i < typeArguments.length; i++) { + stringBuilder.append(", ").append(typeToString(typeArguments[i])); + } + return stringBuilder.append(">").toString(); + } + } + + public static class GenericArrayTypeImpl + implements GenericArrayType, CompositeType { + private final Type componentType; + + public GenericArrayTypeImpl(Type componentType) { + this.componentType = canonicalize(componentType); + } + + public Type getGenericComponentType() { + return componentType; + } + + public boolean isFullySpecified() { + return MoreTypes.isFullySpecified(componentType); + } + + @Override + public boolean equals(Object o) { + return o instanceof GenericArrayType + && MoreTypes.equals(this, (GenericArrayType) o); + } + + @Override + public int hashCode() { + return componentType.hashCode(); + } + + @Override + public String toString() { + return typeToString(componentType) + "[]"; + } + } + + /** + * The WildcardType interface supports multiple upper bounds and multiple + * lower bounds. We only support what the Java 6 language needs - at most one + * bound. If a lower bound is set, the upper bound must be Object.class. + */ + public static class WildcardTypeImpl + implements WildcardType, CompositeType { + private final Type upperBound; + private final Type lowerBound; + + public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + checkArgument(lowerBounds.length <= 1, "Must have at most one lower bound."); + checkArgument(upperBounds.length == 1, "Must have exactly one upper bound."); + + if (lowerBounds.length == 1) { + checkNotNull(lowerBounds[0], "lowerBound"); + checkNotPrimitive(lowerBounds[0], "wildcard bounds"); + checkArgument(upperBounds[0] == Object.class, "bounded both ways"); + this.lowerBound = canonicalize(lowerBounds[0]); + this.upperBound = Object.class; + + } else { + checkNotNull(upperBounds[0], "upperBound"); + checkNotPrimitive(upperBounds[0], "wildcard bounds"); + this.lowerBound = null; + this.upperBound = canonicalize(upperBounds[0]); + } + } + + public Type[] getUpperBounds() { + return new Type[]{upperBound}; + } + + public Type[] getLowerBounds() { + return lowerBound != null ? new Type[]{lowerBound} : EMPTY_TYPE_ARRAY; + } + + public boolean isFullySpecified() { + return MoreTypes.isFullySpecified(upperBound) + && (lowerBound == null || MoreTypes.isFullySpecified(lowerBound)); + } + + @Override + public boolean equals(Object other) { + return other instanceof WildcardType + && MoreTypes.equals(this, (WildcardType) other); + } + + @Override + public int hashCode() { + // this equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()); + return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) + ^ (31 + upperBound.hashCode()); + } + + @Override + public String toString() { + if (lowerBound != null) { + return "? super " + typeToString(lowerBound); + } else if (upperBound == Object.class) { + return "?"; + } else { + return "? extends " + typeToString(upperBound); + } + } + } +} diff --git a/src/main/java/com/google/inject/internal/Nullability.java b/src/main/java/com/google/inject/internal/Nullability.java new file mode 100644 index 0000000..8d9cd4b --- /dev/null +++ b/src/main/java/com/google/inject/internal/Nullability.java @@ -0,0 +1,32 @@ +package com.google.inject.internal; + +import java.lang.annotation.Annotation; + +/** + * Whether a member supports null values injected. + * + *

Support for {@code Nullable} annotations in Guice is loose. + * Any annotation type whose simplename is "Nullable" is sufficient to indicate + * support for null values injected. + * + *

This allows support for JSR-305's + * + * javax.annotation.meta.Nullable annotation and IntelliJ IDEA's + * + * org.jetbrains.annotations.Nullable. + * + */ +public class Nullability { + private Nullability() { + } + + public static boolean allowsNull(Annotation[] annotations) { + for (Annotation a : annotations) { + Class type = a.annotationType(); + if ("Nullable".equals(type.getSimpleName())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/google/inject/internal/PrivateElementProcessor.java b/src/main/java/com/google/inject/internal/PrivateElementProcessor.java new file mode 100644 index 0000000..1c6c837 --- /dev/null +++ b/src/main/java/com/google/inject/internal/PrivateElementProcessor.java @@ -0,0 +1,31 @@ +package com.google.inject.internal; + +import com.google.common.collect.Lists; +import com.google.inject.spi.PrivateElements; + +import java.util.List; + +/** + * Handles {@code Binder.newPrivateBinder()} elements. + */ +final class PrivateElementProcessor extends AbstractProcessor { + + private final List injectorShellBuilders = Lists.newArrayList(); + + PrivateElementProcessor(Errors errors) { + super(errors); + } + + @Override + public Boolean visit(PrivateElements privateElements) { + InjectorShell.Builder builder = new InjectorShell.Builder() + .parent(injector) + .privateElements(privateElements); + injectorShellBuilders.add(builder); + return true; + } + + public List getInjectorShellBuilders() { + return injectorShellBuilders; + } +} diff --git a/src/main/java/com/google/inject/internal/PrivateElementsImpl.java b/src/main/java/com/google/inject/internal/PrivateElementsImpl.java new file mode 100644 index 0000000..45a434c --- /dev/null +++ b/src/main/java/com/google/inject/internal/PrivateElementsImpl.java @@ -0,0 +1,126 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.PrivateBinder; +import com.google.inject.spi.Element; +import com.google.inject.spi.ElementVisitor; +import com.google.inject.spi.PrivateElements; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public final class PrivateElementsImpl implements PrivateElements { + + /* + * This class acts as both a value object and as a builder. When getElements() is called, an + * immutable collection of elements is constructed and the original mutable list is nulled out. + * Similarly, the exposed keys are made immutable on access. + */ + + private final Object source; + + private List elementsMutable = Lists.newArrayList(); + private List> exposureBuilders = Lists.newArrayList(); + + /** + * lazily instantiated + */ + private ImmutableList elements; + + /** + * lazily instantiated + */ + private ImmutableMap, Object> exposedKeysToSources; + private Injector injector; + + public PrivateElementsImpl(Object source) { + this.source = checkNotNull(source, "source"); + } + + public Object getSource() { + return source; + } + + public List getElements() { + if (elements == null) { + elements = ImmutableList.copyOf(elementsMutable); + elementsMutable = null; + } + + return elements; + } + + public Injector getInjector() { + return injector; + } + + public void initInjector(Injector injector) { + checkState(this.injector == null, "injector already initialized"); + this.injector = checkNotNull(injector, "injector"); + } + + public Set> getExposedKeys() { + if (exposedKeysToSources == null) { + Map, Object> exposedKeysToSourcesMutable = Maps.newLinkedHashMap(); + for (ExposureBuilder exposureBuilder : exposureBuilders) { + exposedKeysToSourcesMutable.put(exposureBuilder.getKey(), exposureBuilder.getSource()); + } + exposedKeysToSources = ImmutableMap.copyOf(exposedKeysToSourcesMutable); + exposureBuilders = null; + } + + return exposedKeysToSources.keySet(); + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public List getElementsMutable() { + return elementsMutable; + } + + public void addExposureBuilder(ExposureBuilder exposureBuilder) { + exposureBuilders.add(exposureBuilder); + } + + public void applyTo(Binder binder) { + PrivateBinder privateBinder = binder.withSource(source).newPrivateBinder(); + + for (Element element : getElements()) { + element.applyTo(privateBinder); + } + + getExposedKeys(); // ensure exposedKeysToSources is populated + for (Map.Entry, Object> entry : exposedKeysToSources.entrySet()) { + privateBinder.withSource(entry.getValue()).expose(entry.getKey()); + } + } + + public Object getExposedSource(Key key) { + getExposedKeys(); // ensure exposedKeysToSources is populated + Object source = exposedKeysToSources.get(key); + checkArgument(source != null, "%s not exposed by %s.", key, this); + return source; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(PrivateElements.class) + .add("exposedKeys", getExposedKeys()) + .add("source", getSource()) + .toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/ProcessedBindingData.java b/src/main/java/com/google/inject/internal/ProcessedBindingData.java new file mode 100644 index 0000000..68f8f57 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProcessedBindingData.java @@ -0,0 +1,36 @@ +package com.google.inject.internal; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Keeps track of creation listeners & uninitialized bindings, + * so they can be processed after bindings are recorded. + */ +class ProcessedBindingData { + + private final List creationListeners = Lists.newArrayList(); + private final List uninitializedBindings = Lists.newArrayList(); + + void addCreationListener(CreationListener listener) { + creationListeners.add(listener); + } + + void addUninitializedBinding(Runnable runnable) { + uninitializedBindings.add(runnable); + } + + void initializeBindings() { + for (Runnable initializer : uninitializedBindings) { + initializer.run(); + } + } + + void runCreationListeners(Errors errors) { + for (CreationListener creationListener : creationListeners) { + creationListener.notify(errors); + } + } + +} diff --git a/src/main/java/com/google/inject/internal/ProvidedByInternalFactory.java b/src/main/java/com/google/inject/internal/ProvidedByInternalFactory.java new file mode 100644 index 0000000..c6ea4dc --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProvidedByInternalFactory.java @@ -0,0 +1,73 @@ +package com.google.inject.internal; + +import com.google.inject.Key; +import com.google.inject.ProvidedBy; +import com.google.inject.Provider; +import com.google.inject.internal.InjectorImpl.JitLimitation; +import com.google.inject.spi.Dependency; + +import static com.google.common.base.Preconditions.checkState; + +/** + * An {@link InternalFactory} for {@literal @}{@link ProvidedBy} bindings. + */ +class ProvidedByInternalFactory extends ProviderInternalFactory + implements DelayedInitialize { + + private final Class rawType; + private final Class> providerType; + private final Key> providerKey; + private BindingImpl> providerBinding; + private ProvisionListenerStackCallback provisionCallback; + + ProvidedByInternalFactory( + Class rawType, + Class> providerType, + Key> providerKey) { + super(providerKey); + this.rawType = rawType; + this.providerType = providerType; + this.providerKey = providerKey; + } + + void setProvisionListenerCallback(ProvisionListenerStackCallback listener) { + provisionCallback = listener; + } + + public void initialize(InjectorImpl injector, Errors errors) throws ErrorsException { + providerBinding = + injector.getBindingOrThrow(providerKey, errors, JitLimitation.NEW_OR_EXISTING_JIT); + } + + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) + throws ErrorsException { + checkState(providerBinding != null, "not initialized"); + + context.pushState(providerKey, providerBinding.getSource()); + try { + errors = errors.withSource(providerKey); + Provider provider = providerBinding.getInternalFactory().get( + errors, context, dependency, true); + return circularGet(provider, errors, context, dependency, provisionCallback); + } finally { + context.popState(); + } + } + + @Override + protected T provision(javax.inject.Provider provider, Errors errors, + Dependency dependency, ConstructionContext constructionContext) + throws ErrorsException { + try { + Object o = super.provision(provider, errors, dependency, constructionContext); + if (o != null && !rawType.isInstance(o)) { + throw errors.subtypeNotProvided(providerType, rawType).toException(); + } + @SuppressWarnings("unchecked") // protected by isInstance() check above + T t = (T) o; + return t; + } catch (RuntimeException e) { + throw errors.errorInProvider(e).toException(); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ProviderInstanceBindingImpl.java b/src/main/java/com/google/inject/internal/ProviderInstanceBindingImpl.java new file mode 100644 index 0000000..ff5a882 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProviderInstanceBindingImpl.java @@ -0,0 +1,104 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; + +import java.util.Set; + +final class ProviderInstanceBindingImpl extends BindingImpl + implements ProviderInstanceBinding { + + final javax.inject.Provider providerInstance; + final ImmutableSet injectionPoints; + + public ProviderInstanceBindingImpl(InjectorImpl injector, Key key, + Object source, InternalFactory internalFactory, Scoping scoping, + javax.inject.Provider providerInstance, + Set injectionPoints) { + super(injector, key, source, internalFactory, scoping); + this.providerInstance = providerInstance; + this.injectionPoints = ImmutableSet.copyOf(injectionPoints); + } + + public ProviderInstanceBindingImpl(Object source, Key key, Scoping scoping, + Set injectionPoints, javax.inject.Provider providerInstance) { + super(source, key, scoping); + this.injectionPoints = ImmutableSet.copyOf(injectionPoints); + this.providerInstance = providerInstance; + } + + @SuppressWarnings("unchecked") // the extension type is always consistent with the provider type + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + if (providerInstance instanceof ProviderWithExtensionVisitor) { + return ((ProviderWithExtensionVisitor) providerInstance) + .acceptExtensionVisitor(visitor, this); + } else { + return visitor.visit(this); + } + } + + public javax.inject.Provider getUserSuppliedProvider() { + return providerInstance; + } + + public Set getInjectionPoints() { + return injectionPoints; + } + + public Set> getDependencies() { + return providerInstance instanceof HasDependencies + ? ImmutableSet.copyOf(((HasDependencies) providerInstance).getDependencies()) + : Dependency.forInjectionPoints(injectionPoints); + } + + public BindingImpl withScoping(Scoping scoping) { + return new ProviderInstanceBindingImpl( + getSource(), getKey(), scoping, injectionPoints, providerInstance); + } + + public BindingImpl withKey(Key key) { + return new ProviderInstanceBindingImpl( + getSource(), key, getScoping(), injectionPoints, providerInstance); + } + + public void applyTo(Binder binder) { + getScoping().applyTo( + binder.withSource(getSource()).bind(getKey()).toProvider(getUserSuppliedProvider())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ProviderInstanceBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .add("scope", getScoping()) + .add("provider", providerInstance) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ProviderInstanceBindingImpl) { + ProviderInstanceBindingImpl o = (ProviderInstanceBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()) + && Objects.equal(providerInstance, o.providerInstance); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping()); + } +} diff --git a/src/main/java/com/google/inject/internal/ProviderInternalFactory.java b/src/main/java/com/google/inject/internal/ProviderInternalFactory.java new file mode 100644 index 0000000..b0576b4 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProviderInternalFactory.java @@ -0,0 +1,67 @@ +package com.google.inject.internal; + +import com.google.inject.internal.ProvisionListenerStackCallback.ProvisionCallback; +import com.google.inject.spi.Dependency; + +import javax.inject.Provider; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Base class for InternalFactories that are used by Providers, to handle + * circular dependencies. + * + */ +abstract class ProviderInternalFactory implements InternalFactory { + + protected final Object source; + + ProviderInternalFactory(Object source) { + this.source = checkNotNull(source, "source"); + } + + protected T circularGet(final Provider provider, final Errors errors, + InternalContext context, final Dependency dependency, + ProvisionListenerStackCallback provisionCallback) + throws ErrorsException { + final ConstructionContext constructionContext = context.getConstructionContext(this); + + // We have a circular reference between constructors. Return a proxy. + if (constructionContext.isConstructing()) { + Class expectedType = dependency.getKey().getTypeLiteral().getRawType(); + // TODO: if we can't proxy this object, can we proxy the other object? + @SuppressWarnings("unchecked") + T proxyType = (T) constructionContext.createProxy( + errors, context.getInjectorOptions(), expectedType); + return proxyType; + } + + // Optimization: Don't go through the callback stack if no one's listening. + constructionContext.startConstruction(); + try { + if (!provisionCallback.hasListeners()) { + return provision(provider, errors, dependency, constructionContext); + } else { + return provisionCallback.provision(errors, context, new ProvisionCallback() { + public T call() throws ErrorsException { + return provision(provider, errors, dependency, constructionContext); + } + }); + } + } finally { + constructionContext.removeCurrentReference(); + constructionContext.finishConstruction(); + } + } + + /** + * Provisions a new instance. Subclasses should override this to catch + * exceptions & rethrow as ErrorsExceptions. + */ + protected T provision(Provider provider, Errors errors, Dependency dependency, + ConstructionContext constructionContext) throws ErrorsException { + T t = errors.checkForNull(provider.get(), source, dependency); + constructionContext.setProxyDelegates(t); + return t; + } +} diff --git a/src/main/java/com/google/inject/internal/ProviderMethod.java b/src/main/java/com/google/inject/internal/ProviderMethod.java new file mode 100644 index 0000000..8c38d0f --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProviderMethod.java @@ -0,0 +1,220 @@ +package com.google.inject.internal; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Exposed; +import com.google.inject.Key; +import com.google.inject.PrivateBinder; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.internal.util.StackTraceElements; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import com.google.inject.spi.ProvidesMethodBinding; +import com.google.inject.spi.ProvidesMethodTargetVisitor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Set; + +/** + * A provider that invokes a method and returns its result. + * + */ +public abstract class ProviderMethod implements ProviderWithExtensionVisitor, HasDependencies, + ProvidesMethodBinding { + + protected final Object instance; + protected final Method method; + private final Key key; + private final Class scopeAnnotation; + private final ImmutableSet> dependencies; + private final List> parameterProviders; + private final boolean exposed; + private final Annotation annotation; + /** + * @param method the method to invoke. It's return type must be the same type as {@code key}. + */ + private ProviderMethod(Key key, Method method, Object instance, + ImmutableSet> dependencies, List> parameterProviders, + Class scopeAnnotation, Annotation annotation) { + this.key = key; + this.scopeAnnotation = scopeAnnotation; + this.instance = instance; + this.dependencies = dependencies; + this.method = method; + this.parameterProviders = parameterProviders; + this.exposed = method.isAnnotationPresent(Exposed.class); + this.annotation = annotation; + } + + /** + * Creates a {@link ProviderMethod}. + */ + static ProviderMethod create(Key key, Method method, Object instance, + ImmutableSet> dependencies, List> parameterProviders, + Class scopeAnnotation, boolean skipFastClassGeneration, + Annotation annotation) { + int modifiers = method.getModifiers(); + + if (!Modifier.isPublic(modifiers) || + !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + method.setAccessible(true); + } + + return new ReflectionProviderMethod(key, + method, + instance, + dependencies, + parameterProviders, + scopeAnnotation, + annotation); + } + + @Override + public Key getKey() { + return key; + } + + @Override + public Method getMethod() { + return method; + } + + // exposed for GIN + public Object getInstance() { + return instance; + } + + @Override + public Object getEnclosingInstance() { + return instance; + } + + @Override + public Annotation getAnnotation() { + return annotation; + } + + public void configure(Binder binder) { + binder = binder.withSource(method); + + if (scopeAnnotation != null) { + binder.bind(key).toProvider(this).in(scopeAnnotation); + } else { + binder.bind(key).toProvider(this); + } + + if (exposed) { + // the cast is safe 'cause the only binder we have implements PrivateBinder. If there's a + // misplaced @Exposed, calling this will add an error to the binder's error queue + ((PrivateBinder) binder).expose(key); + } + } + + @Override + public T get() { + Object[] parameters = new Object[parameterProviders.size()]; + for (int i = 0; i < parameters.length; i++) { + parameters[i] = parameterProviders.get(i).get(); + } + + try { + @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"}) + T result = (T) doProvision(parameters); + return result; + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw Exceptions.rethrowCause(e); + } + } + + /** + * Extension point for our subclasses to implement the provisioning strategy. + */ + abstract Object doProvision(Object[] parameters) + throws IllegalAccessException, InvocationTargetException; + + @Override + public Set> getDependencies() { + return dependencies; + } + + @Override + @SuppressWarnings("unchecked") + public V acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + if (visitor instanceof ProvidesMethodTargetVisitor) { + return ((ProvidesMethodTargetVisitor) visitor).visit(this); + } + return visitor.visit(binding); + } + + @Override + public String toString() { + String annotationString = annotation.toString(); + // Show @Provides w/o the com.google.inject prefix. + if (annotation.annotationType() == Provides.class) { + annotationString = "@Provides"; + } else if (annotationString.endsWith("()")) { + // Remove the common "()" suffix if there are no values. + annotationString = annotationString.substring(0, annotationString.length() - 2); + } + return annotationString + " " + StackTraceElements.forMember(method); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ProviderMethod) { + ProviderMethod o = (ProviderMethod) obj; + return method.equals(o.method) + && instance.equals(o.instance) + && annotation.equals(o.annotation); + } else { + return false; + } + } + + @Override + public int hashCode() { + // Avoid calling hashCode on 'instance', which is a user-object + // that might not be expecting it. + // (We need to call equals, so we do. But we can avoid hashCode.) + return Objects.hashCode(method, annotation); + } + + /** + * A {@link ProviderMethod} implementation that invokes the method using normal java reflection. + */ + private static final class ReflectionProviderMethod extends ProviderMethod { + ReflectionProviderMethod(Key key, + Method method, + Object instance, + ImmutableSet> dependencies, + List> parameterProviders, + Class scopeAnnotation, + Annotation annotation) { + super(key, + method, + instance, + dependencies, + parameterProviders, + scopeAnnotation, + annotation); + } + + @Override + Object doProvision(Object[] parameters) throws IllegalAccessException, + InvocationTargetException { + return method.invoke(instance, parameters); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ProviderMethodsModule.java b/src/main/java/com/google/inject/internal/ProviderMethodsModule.java new file mode 100644 index 0000000..6db88d0 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProviderMethodsModule.java @@ -0,0 +1,281 @@ +package com.google.inject.internal; + +import com.google.common.base.Optional; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.Message; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; +import com.google.inject.util.Modules; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Creates bindings to methods annotated with {@literal @}{@link Provides}. Use the scope and + * binding annotations on the provider method to configure the binding. + * + */ +public final class ProviderMethodsModule implements Module { + + private static ModuleAnnotatedMethodScanner PROVIDES_BUILDER = + new ModuleAnnotatedMethodScanner() { + @Override + public Key prepareMethod( + Binder binder, Annotation annotation, Key key, InjectionPoint injectionPoint) { + return key; + } + + @Override + public Set> annotationClasses() { + return ImmutableSet.of(Provides.class); + } + }; + + private final Object delegate; + private final TypeLiteral typeLiteral; + private final boolean skipFastClassGeneration; + private final ModuleAnnotatedMethodScanner scanner; + + private ProviderMethodsModule(Object delegate, boolean skipFastClassGeneration, + ModuleAnnotatedMethodScanner scanner) { + this.delegate = checkNotNull(delegate, "delegate"); + this.typeLiteral = TypeLiteral.get(this.delegate.getClass()); + this.skipFastClassGeneration = skipFastClassGeneration; + this.scanner = scanner; + } + + /** + * Returns a module which creates bindings for provider methods from the given module. + */ + public static Module forModule(Module module) { + return forObject(module, false, PROVIDES_BUILDER); + } + + /** + * Returns a module which creates bindings methods in the module that match the scanner. + */ + public static Module forModule(Object module, ModuleAnnotatedMethodScanner scanner) { + return forObject(module, false, scanner); + } + + /** + * Returns a module which creates bindings for provider methods from the given object. + * This is useful notably for GIN + * + *

This will skip bytecode generation for provider methods, since it is assumed that callers + * are only interested in Module metadata. + */ + public static Module forObject(Object object) { + return forObject(object, true, PROVIDES_BUILDER); + } + + private static Module forObject(Object object, boolean skipFastClassGeneration, + ModuleAnnotatedMethodScanner scanner) { + // avoid infinite recursion, since installing a module always installs itself + if (object instanceof ProviderMethodsModule) { + return Modules.EMPTY_MODULE; + } + + return new ProviderMethodsModule(object, skipFastClassGeneration, scanner); + } + + /** + * Returns true if a overrides b, assumes that the signatures match + */ + private static boolean overrides(Method a, Method b) { + // See JLS section 8.4.8.1 + int modifiers = b.getModifiers(); + if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) { + return true; + } + if (Modifier.isPrivate(modifiers)) { + return false; + } + // b must be package-private + return a.getDeclaringClass().getPackage().equals(b.getDeclaringClass().getPackage()); + } + + public Object getDelegateModule() { + return delegate; + } + + @Override + public synchronized void configure(Binder binder) { + for (ProviderMethod providerMethod : getProviderMethods(binder)) { + providerMethod.configure(binder); + } + } + + public List> getProviderMethods(Binder binder) { + List> result = Lists.newArrayList(); + Multimap methodsBySignature = HashMultimap.create(); + for (Class c = delegate.getClass(); c != Object.class; c = c.getSuperclass()) { + for (Method method : c.getDeclaredMethods()) { + // private/static methods cannot override or be overridden by other methods, so there is no + // point in indexing them. + // Skip synthetic methods and bridge methods since java will automatically generate + // synthetic overrides in some cases where we don't want to generate an error (e.g. + // increasing visibility of a subclass). + if (((method.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC)) == 0) + && !method.isBridge() && !method.isSynthetic()) { + methodsBySignature.put(new Signature(method), method); + } + Optional annotation = isProvider(binder, method); + if (annotation.isPresent()) { + result.add(createProviderMethod(binder, method, annotation.get())); + } + } + } + // we have found all the providers and now need to identify if any were overridden + // In the worst case this will have O(n^2) in the number of @Provides methods, but that is only + // assuming that every method is an override, in general it should be very quick. + for (ProviderMethod provider : result) { + Method method = provider.getMethod(); + for (Method matchingSignature : methodsBySignature.get(new Signature(method))) { + // matching signature is in the same class or a super class, therefore method cannot be + // overridding it. + if (matchingSignature.getDeclaringClass().isAssignableFrom(method.getDeclaringClass())) { + continue; + } + // now we know matching signature is in a subtype of method.getDeclaringClass() + if (overrides(matchingSignature, method)) { + String annotationString = provider.getAnnotation().annotationType() == Provides.class + ? "@Provides" : "@" + provider.getAnnotation().annotationType().getCanonicalName(); + binder.addError( + "Overriding " + annotationString + " methods is not allowed." + + "\n\t" + annotationString + " method: %s\n\toverridden by: %s", + method, + matchingSignature); + break; + } + } + } + return result; + } + + /** + * Returns true if the method is a provider. + * + * Synthetic bridge methods are excluded. Starting with JDK 8, javac copies annotations onto + * bridge methods (which always have erased signatures). + */ + private Optional isProvider(Binder binder, Method method) { + if (method.isBridge() || method.isSynthetic()) { + return Optional.absent(); + } + Annotation annotation = null; + for (Class annotationClass : scanner.annotationClasses()) { + Annotation foundAnnotation = method.getAnnotation(annotationClass); + if (foundAnnotation != null) { + if (annotation != null) { + binder.addError("More than one annotation claimed by %s on method %s." + + " Methods can only have one annotation claimed per scanner.", + scanner, method); + return Optional.absent(); + } + annotation = foundAnnotation; + } + } + return Optional.fromNullable(annotation); + } + + private ProviderMethod createProviderMethod(Binder binder, Method method, + Annotation annotation) { + binder = binder.withSource(method); + Errors errors = new Errors(method); + + // prepare the parameter providers + InjectionPoint point = InjectionPoint.forMethod(method, typeLiteral); + List> dependencies = point.getDependencies(); + List> parameterProviders = Lists.newArrayList(); + for (Dependency dependency : point.getDependencies()) { + parameterProviders.add(binder.getProvider(dependency)); + } + + @SuppressWarnings("unchecked") // Define T as the method's return type. + TypeLiteral returnType = (TypeLiteral) typeLiteral.getReturnType(method); + Key key = getKey(errors, returnType, method, method.getAnnotations()); + try { + key = scanner.prepareMethod(binder, annotation, key, point); + } catch (Throwable t) { + binder.addError(t); + } + Class scopeAnnotation + = Annotations.findScopeAnnotation(errors, method.getAnnotations()); + for (Message message : errors.getMessages()) { + binder.addError(message); + } + return ProviderMethod.create(key, method, delegate, ImmutableSet.copyOf(dependencies), + parameterProviders, scopeAnnotation, skipFastClassGeneration, annotation); + } + + Key getKey(Errors errors, TypeLiteral type, Member member, Annotation[] annotations) { + Annotation bindingAnnotation = Annotations.findBindingAnnotation(errors, member, annotations); + return bindingAnnotation == null ? Key.get(type) : Key.get(type, bindingAnnotation); + } + + @Override + public boolean equals(Object o) { + return o instanceof ProviderMethodsModule + && ((ProviderMethodsModule) o).delegate == delegate + && ((ProviderMethodsModule) o).scanner == scanner; + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + private final class Signature { + final Class[] parameters; + final String name; + final int hashCode; + + Signature(Method method) { + this.name = method.getName(); + // We need to 'resolve' the parameters against the actual class type in case this method uses + // type parameters. This is so we can detect overrides of generic superclass methods where + // the subclass specifies the type parameter. javac implements these kinds of overrides via + // bridge methods, but we don't want to give errors on bridge methods (but rather the target + // of the bridge). + List> resolvedParameterTypes = typeLiteral.getParameterTypes(method); + this.parameters = new Class[resolvedParameterTypes.size()]; + int i = 0; + for (TypeLiteral type : resolvedParameterTypes) { + parameters[i] = type.getRawType(); + } + this.hashCode = name.hashCode() + 31 * Arrays.hashCode(parameters); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Signature) { + Signature other = (Signature) obj; + return other.name.equals(name) && Arrays.equals(parameters, other.parameters); + } + return false; + } + + @Override + public int hashCode() { + return hashCode; + } + } +} diff --git a/src/main/java/com/google/inject/internal/ProviderToInternalFactoryAdapter.java b/src/main/java/com/google/inject/internal/ProviderToInternalFactoryAdapter.java new file mode 100644 index 0000000..c527bed --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProviderToInternalFactoryAdapter.java @@ -0,0 +1,41 @@ +package com.google.inject.internal; + +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.spi.Dependency; + +final class ProviderToInternalFactoryAdapter implements Provider { + + private final InjectorImpl injector; + private final InternalFactory internalFactory; + + public ProviderToInternalFactoryAdapter(InjectorImpl injector, + InternalFactory internalFactory) { + this.injector = injector; + this.internalFactory = internalFactory; + } + + public T get() { + final Errors errors = new Errors(); + try { + T t = injector.callInContext(new ContextualCallable() { + public T call(InternalContext context) throws ErrorsException { + Dependency dependency = context.getDependency(); + // Always pretend that we are a linked binding, to support + // scoping implicit bindings. If we are not actually a linked + // binding, we'll fail properly elsewhere in the chain. + return internalFactory.get(errors, context, dependency, true); + } + }); + errors.throwIfNewErrors(0); + return t; + } catch (ErrorsException e) { + throw new ProvisionException(errors.merge(e.getErrors()).getMessages()); + } + } + + @Override + public String toString() { + return internalFactory.toString(); + } +} diff --git a/src/main/java/com/google/inject/internal/ProvisionListenerCallbackStore.java b/src/main/java/com/google/inject/internal/ProvisionListenerCallbackStore.java new file mode 100644 index 0000000..2160843 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProvisionListenerCallbackStore.java @@ -0,0 +1,114 @@ +package com.google.inject.internal; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Stage; +import com.google.inject.spi.ProvisionListener; +import com.google.inject.spi.ProvisionListenerBinding; + +import java.util.List; +import java.util.Set; + +/** + * {@link ProvisionListenerStackCallback} for each key. + * + */ +final class ProvisionListenerCallbackStore { + + // TODO(sameb): Consider exposing this in the API somehow? Maybe? + // Lots of code often want to skip over the internal stuffs. + private static final Set> INTERNAL_BINDINGS = + ImmutableSet.of(Key.get(Injector.class), Key.get(Stage.class)); + + private final ImmutableList listenerBindings; + + private final LoadingCache> cache + = CacheBuilder.newBuilder().build( + new CacheLoader>() { + public ProvisionListenerStackCallback load(KeyBinding key) { + return create(key.binding); + } + }); + + ProvisionListenerCallbackStore(List listenerBindings) { + this.listenerBindings = ImmutableList.copyOf(listenerBindings); + } + + /** + * Returns a new {@link ProvisionListenerStackCallback} for the key. + */ + @SuppressWarnings("unchecked") // the ProvisionListenerStackCallback type always agrees with the passed type + public ProvisionListenerStackCallback get(Binding binding) { + // Never notify any listeners for internal bindings. + if (!INTERNAL_BINDINGS.contains(binding.getKey())) { + return (ProvisionListenerStackCallback) cache.getUnchecked( + new KeyBinding(binding.getKey(), binding)); + } + return ProvisionListenerStackCallback.emptyListener(); + } + + /** + * Purges a key from the cache. Use this only if the type is not actually valid for + * binding and needs to be purged. (See issue 319 and + * ImplicitBindingTest#testCircularJitBindingsLeaveNoResidue and + * #testInstancesRequestingProvidersForThemselvesWithChildInjectors for examples of when this is + * necessary.) + * + * Returns true if the type was stored in the cache, false otherwise. + */ + boolean remove(Binding type) { + return cache.asMap().remove(type) != null; + } + + /** + * Creates a new {@link ProvisionListenerStackCallback} with the correct listeners + * for the key. + */ + private ProvisionListenerStackCallback create(Binding binding) { + List listeners = null; + for (ProvisionListenerBinding provisionBinding : listenerBindings) { + if (provisionBinding.getBindingMatcher().matches(binding)) { + if (listeners == null) { + listeners = Lists.newArrayList(); + } + listeners.addAll(provisionBinding.getListeners()); + } + } + if (listeners == null || listeners.isEmpty()) { + // Optimization: don't bother constructing the callback if there are + // no listeners. + return ProvisionListenerStackCallback.emptyListener(); + } + return new ProvisionListenerStackCallback(binding, listeners); + } + + /** + * A struct that holds key & binding but uses just key for equality/hashcode. + */ + private static class KeyBinding { + final Key key; + final Binding binding; + + KeyBinding(Key key, Binding binding) { + this.key = key; + this.binding = binding; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof KeyBinding && key.equals(((KeyBinding) obj).key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ProvisionListenerStackCallback.java b/src/main/java/com/google/inject/internal/ProvisionListenerStackCallback.java new file mode 100644 index 0000000..7e9f2fc --- /dev/null +++ b/src/main/java/com/google/inject/internal/ProvisionListenerStackCallback.java @@ -0,0 +1,137 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.ProvisionException; +import com.google.inject.spi.DependencyAndSource; +import com.google.inject.spi.ProvisionListener; + +import java.util.List; +import java.util.Set; + +/** + * Intercepts provisions with a stack of listeners. + * + */ +final class ProvisionListenerStackCallback { + + private static final ProvisionListener EMPTY_LISTENER[] = new ProvisionListener[0]; + @SuppressWarnings({"rawtypes", "unchecked"}) + private static final ProvisionListenerStackCallback EMPTY_CALLBACK = + new ProvisionListenerStackCallback(null /* unused, so ok */, ImmutableList.of()); + + private final ProvisionListener[] listeners; + private final Binding binding; + + public ProvisionListenerStackCallback(Binding binding, List listeners) { + this.binding = binding; + if (listeners.isEmpty()) { + this.listeners = EMPTY_LISTENER; + } else { + Set deDuplicated = Sets.newLinkedHashSet(listeners); + this.listeners = deDuplicated.toArray(new ProvisionListener[deDuplicated.size()]); + } + } + + @SuppressWarnings("unchecked") + public static ProvisionListenerStackCallback emptyListener() { + return (ProvisionListenerStackCallback) EMPTY_CALLBACK; + } + + public boolean hasListeners() { + return listeners.length > 0; + } + + public T provision(Errors errors, InternalContext context, ProvisionCallback callable) + throws ErrorsException { + Provision provision = new Provision(errors, context, callable); + RuntimeException caught = null; + try { + provision.provision(); + } catch (RuntimeException t) { + caught = t; + } + + if (provision.exceptionDuringProvision != null) { + throw provision.exceptionDuringProvision; + } else if (caught != null) { + Object listener = provision.erredListener != null ? + provision.erredListener.getClass() : "(unknown)"; + throw errors + .errorInUserCode(caught, "Error notifying ProvisionListener %s of %s.%n" + + " Reason: %s", listener, binding.getKey(), caught) + .toException(); + } else { + return provision.result; + } + } + + // TODO(sameb): Can this be more InternalFactory-like? + public interface ProvisionCallback { + T call() throws ErrorsException; + } + + private class Provision extends ProvisionListener.ProvisionInvocation { + + final Errors errors; + final int numErrorsBefore; + final InternalContext context; + final ProvisionCallback callable; + int index = -1; + T result; + ErrorsException exceptionDuringProvision; + ProvisionListener erredListener; + + public Provision(Errors errors, InternalContext context, ProvisionCallback callable) { + this.callable = callable; + this.context = context; + this.errors = errors; + this.numErrorsBefore = errors.size(); + } + + @Override + public T provision() { + index++; + if (index == listeners.length) { + try { + result = callable.call(); + // Make sure we don't return the provisioned object if there were any errors + // injecting its field/method dependencies. + errors.throwIfNewErrors(numErrorsBefore); + } catch (ErrorsException ee) { + exceptionDuringProvision = ee; + throw new ProvisionException(errors.merge(ee.getErrors()).getMessages()); + } + } else if (index < listeners.length) { + int currentIdx = index; + try { + listeners[index].onProvision(this); + } catch (RuntimeException re) { + erredListener = listeners[currentIdx]; + throw re; + } + if (currentIdx == index) { + // Our listener didn't provision -- do it for them. + provision(); + } + } else { + throw new IllegalStateException("Already provisioned in this listener."); + } + return result; + } + + @Override + public Binding getBinding() { + // TODO(sameb): Because so many places cast directly to BindingImpl & subclasses, + // we can't decorate this to prevent calling getProvider().get(), which means + // if someone calls that they'll get strange errors. + return binding; + } + + @Override + public List getDependencyChain() { + return context.getDependencyChain(); + } + } +} diff --git a/src/main/java/com/google/inject/internal/ScopeBindingProcessor.java b/src/main/java/com/google/inject/internal/ScopeBindingProcessor.java new file mode 100644 index 0000000..b702190 --- /dev/null +++ b/src/main/java/com/google/inject/internal/ScopeBindingProcessor.java @@ -0,0 +1,46 @@ +package com.google.inject.internal; + +import com.google.inject.Scope; +import com.google.inject.spi.ScopeBinding; + +import java.lang.annotation.Annotation; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Handles {@code Binder.bindScope} commands. + * + */ +final class ScopeBindingProcessor extends AbstractProcessor { + + ScopeBindingProcessor(Errors errors) { + super(errors); + } + + @Override + public Boolean visit(ScopeBinding command) { + Scope scope = checkNotNull(command.getScope(), "scope"); + Class annotationType = checkNotNull(command.getAnnotationType(), "annotation type"); + + if (!Annotations.isScopeAnnotation(annotationType)) { + errors.missingScopeAnnotation(annotationType); + // Go ahead and bind anyway so we don't get collateral errors. + } + + if (!Annotations.isRetainedAtRuntime(annotationType)) { + errors.missingRuntimeRetention(annotationType); + // Go ahead and bind anyway so we don't get collateral errors. + } + + ScopeBinding existing = injector.state.getScopeBinding(annotationType); + if (existing != null) { + if (!scope.equals(existing.getScope())) { + errors.duplicateScopes(existing, annotationType, scope); + } + } else { + injector.state.putScopeBinding(annotationType, command); + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/internal/Scoping.java b/src/main/java/com/google/inject/internal/Scoping.java new file mode 100644 index 0000000..5e19a64 --- /dev/null +++ b/src/main/java/com/google/inject/internal/Scoping.java @@ -0,0 +1,273 @@ +package com.google.inject.internal; + +import com.google.common.base.Objects; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import com.google.inject.binder.ScopedBindingBuilder; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.ScopeBinding; + +import java.lang.annotation.Annotation; + +/** + * References a scope, either directly (as a scope instance), or indirectly (as a scope annotation). + * The scope's eager or laziness is also exposed. + * + */ +public abstract class Scoping { + + /** + * No scoping annotation has been applied. Note that this is different from {@code + * in(Scopes.NO_SCOPE)}, where the 'NO_SCOPE' has been explicitly applied. + */ + public static final Scoping UNSCOPED = new Scoping() { + @Override + public V acceptVisitor(BindingScopingVisitor visitor) { + return visitor.visitNoScoping(); + } + + @Override + public Scope getScopeInstance() { + return Scopes.NO_SCOPE; + } + + @Override + public String toString() { + return Scopes.NO_SCOPE.toString(); + } + + @Override + public void applyTo(ScopedBindingBuilder scopedBindingBuilder) { + // do nothing + } + }; + + public static final Scoping SINGLETON_ANNOTATION = new Scoping() { + @Override + public V acceptVisitor(BindingScopingVisitor visitor) { + return visitor.visitScopeAnnotation(Singleton.class); + } + + @Override + public Class getScopeAnnotation() { + return Singleton.class; + } + + @Override + public String toString() { + return Singleton.class.getName(); + } + + @Override + public void applyTo(ScopedBindingBuilder scopedBindingBuilder) { + scopedBindingBuilder.in(Singleton.class); + } + }; + + public static final Scoping SINGLETON_INSTANCE = new Scoping() { + @Override + public V acceptVisitor(BindingScopingVisitor visitor) { + return visitor.visitScope(Scopes.SINGLETON); + } + + @Override + public Scope getScopeInstance() { + return Scopes.SINGLETON; + } + + @Override + public String toString() { + return Scopes.SINGLETON.toString(); + } + + @Override + public void applyTo(ScopedBindingBuilder scopedBindingBuilder) { + scopedBindingBuilder.in(Scopes.SINGLETON); + } + }; + + public static final Scoping EAGER_SINGLETON = new Scoping() { + @Override + public V acceptVisitor(BindingScopingVisitor visitor) { + return visitor.visitEagerSingleton(); + } + + @Override + public Scope getScopeInstance() { + return Scopes.SINGLETON; + } + + @Override + public String toString() { + return "eager singleton"; + } + + @Override + public void applyTo(ScopedBindingBuilder scopedBindingBuilder) { + scopedBindingBuilder.asEagerSingleton(); + } + }; + + private Scoping() { + } + + public static Scoping forAnnotation(final Class scopingAnnotation) { + if (scopingAnnotation == Singleton.class + || scopingAnnotation == javax.inject.Singleton.class) { + return SINGLETON_ANNOTATION; + } + + return new Scoping() { + @Override + public V acceptVisitor(BindingScopingVisitor visitor) { + return visitor.visitScopeAnnotation(scopingAnnotation); + } + + @Override + public Class getScopeAnnotation() { + return scopingAnnotation; + } + + @Override + public String toString() { + return scopingAnnotation.getName(); + } + + @Override + public void applyTo(ScopedBindingBuilder scopedBindingBuilder) { + scopedBindingBuilder.in(scopingAnnotation); + } + }; + } + + public static Scoping forInstance(final Scope scope) { + if (scope == Scopes.SINGLETON) { + return SINGLETON_INSTANCE; + } + + return new Scoping() { + @Override + public V acceptVisitor(BindingScopingVisitor visitor) { + return visitor.visitScope(scope); + } + + @Override + public Scope getScopeInstance() { + return scope; + } + + @Override + public String toString() { + return scope.toString(); + } + + @Override + public void applyTo(ScopedBindingBuilder scopedBindingBuilder) { + scopedBindingBuilder.in(scope); + } + }; + } + + /** + * Scopes an internal factory. + */ + static InternalFactory scope(Key key, InjectorImpl injector, + InternalFactory creator, Object source, Scoping scoping) { + + if (scoping.isNoScope()) { + return creator; + } + + Scope scope = scoping.getScopeInstance(); + + Provider scoped + = scope.scope(key, new ProviderToInternalFactoryAdapter(injector, creator)); + return new InternalFactoryToProviderAdapter(scoped, source); + } + + /** + * Replaces annotation scopes with instance scopes using the Injector's annotation-to-instance + * map. If the scope annotation has no corresponding instance, an error will be added and unscoped + * will be retuned. + */ + static Scoping makeInjectable(Scoping scoping, InjectorImpl injector, Errors errors) { + Class scopeAnnotation = scoping.getScopeAnnotation(); + if (scopeAnnotation == null) { + return scoping; + } + + ScopeBinding scope = injector.state.getScopeBinding(scopeAnnotation); + if (scope != null) { + return forInstance(scope.getScope()); + } + + errors.scopeNotFound(scopeAnnotation); + return UNSCOPED; + } + + /** + * Returns true if this scope was explicitly applied. If no scope was explicitly applied then the + * scoping annotation will be used. + */ + public boolean isExplicitlyScoped() { + return this != UNSCOPED; + } + + /** + * Returns true if this is the default scope. In this case a new instance will be provided for + * each injection. + */ + public boolean isNoScope() { + return getScopeInstance() == Scopes.NO_SCOPE; + } + + /** + * Returns true if this scope is a singleton that should be loaded eagerly in {@code stage}. + */ + public boolean isEagerSingleton(Stage stage) { + if (this == EAGER_SINGLETON) { + return true; + } + + return stage == Stage.PRODUCTION && (this == SINGLETON_ANNOTATION || this == SINGLETON_INSTANCE); + + } + + /** + * Returns the scope instance, or {@code null} if that isn't known for this instance. + */ + public Scope getScopeInstance() { + return null; + } + + /** + * Returns the scope annotation, or {@code null} if that isn't known for this instance. + */ + public Class getScopeAnnotation() { + return null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Scoping) { + Scoping o = (Scoping) obj; + return Objects.equal(getScopeAnnotation(), o.getScopeAnnotation()) + && Objects.equal(getScopeInstance(), o.getScopeInstance()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getScopeAnnotation(), getScopeInstance()); + } + + public abstract V acceptVisitor(BindingScopingVisitor visitor); + + public abstract void applyTo(ScopedBindingBuilder scopedBindingBuilder); +} diff --git a/src/main/java/com/google/inject/internal/SingleFieldInjector.java b/src/main/java/com/google/inject/internal/SingleFieldInjector.java new file mode 100644 index 0000000..d271483 --- /dev/null +++ b/src/main/java/com/google/inject/internal/SingleFieldInjector.java @@ -0,0 +1,48 @@ +package com.google.inject.internal; + +import com.google.inject.internal.InjectorImpl.JitLimitation; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.InjectionPoint; + +import java.lang.reflect.Field; + +/** + * Sets an injectable field. + */ +final class SingleFieldInjector implements SingleMemberInjector { + final Field field; + final InjectionPoint injectionPoint; + final Dependency dependency; + final BindingImpl binding; + + public SingleFieldInjector(InjectorImpl injector, InjectionPoint injectionPoint, Errors errors) + throws ErrorsException { + this.injectionPoint = injectionPoint; + this.field = (Field) injectionPoint.getMember(); + this.dependency = injectionPoint.getDependencies().get(0); + + // Ewwwww... + field.setAccessible(true); + binding = injector.getBindingOrThrow(dependency.getKey(), errors, JitLimitation.NO_JIT); + } + + public InjectionPoint getInjectionPoint() { + return injectionPoint; + } + + public void inject(Errors errors, InternalContext context, Object o) { + errors = errors.withSource(dependency); + + Dependency previous = context.pushDependency(dependency, binding.getSource()); + try { + Object value = binding.getInternalFactory().get(errors, context, dependency, false); + field.set(o, value); + } catch (ErrorsException e) { + errors.withSource(injectionPoint).merge(e.getErrors()); + } catch (IllegalAccessException e) { + throw new AssertionError(e); // a security manager is blocking us, we're hosed + } finally { + context.popStateAndSetDependency(previous); + } + } +} diff --git a/src/main/java/com/google/inject/internal/SingleMemberInjector.java b/src/main/java/com/google/inject/internal/SingleMemberInjector.java new file mode 100644 index 0000000..dc02ff5 --- /dev/null +++ b/src/main/java/com/google/inject/internal/SingleMemberInjector.java @@ -0,0 +1,12 @@ +package com.google.inject.internal; + +import com.google.inject.spi.InjectionPoint; + +/** + * Injects a field or method of a given object. + */ +interface SingleMemberInjector { + void inject(Errors errors, InternalContext context, Object o); + + InjectionPoint getInjectionPoint(); +} diff --git a/src/main/java/com/google/inject/internal/SingleMethodInjector.java b/src/main/java/com/google/inject/internal/SingleMethodInjector.java new file mode 100644 index 0000000..6a62c9b --- /dev/null +++ b/src/main/java/com/google/inject/internal/SingleMethodInjector.java @@ -0,0 +1,65 @@ +package com.google.inject.internal; + +import com.google.inject.internal.InjectorImpl.MethodInvoker; +import com.google.inject.spi.InjectionPoint; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Invokes an injectable method. + */ +final class SingleMethodInjector implements SingleMemberInjector { + private final MethodInvoker methodInvoker; + private final SingleParameterInjector[] parameterInjectors; + private final InjectionPoint injectionPoint; + + SingleMethodInjector(InjectorImpl injector, InjectionPoint injectionPoint, Errors errors) + throws ErrorsException { + this.injectionPoint = injectionPoint; + final Method method = (Method) injectionPoint.getMember(); + methodInvoker = createMethodInvoker(method); + parameterInjectors = injector.getParametersInjectors(injectionPoint.getDependencies(), errors); + } + + private MethodInvoker createMethodInvoker(final Method method) { + int modifiers = method.getModifiers(); + if (!Modifier.isPublic(modifiers) || + !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + method.setAccessible(true); + } + + return new MethodInvoker() { + public Object invoke(Object target, Object... parameters) + throws IllegalAccessException, InvocationTargetException { + return method.invoke(target, parameters); + } + }; + } + + public InjectionPoint getInjectionPoint() { + return injectionPoint; + } + + public void inject(Errors errors, InternalContext context, Object o) { + Object[] parameters; + try { + parameters = SingleParameterInjector.getAll(errors, context, parameterInjectors); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + return; + } + + try { + methodInvoker.invoke(o, parameters); + } catch (IllegalAccessException e) { + throw new AssertionError(e); // a security manager is blocking us, we're hosed + } catch (InvocationTargetException userException) { + Throwable cause = userException.getCause() != null + ? userException.getCause() + : userException; + errors.withSource(injectionPoint).errorInjectingMethod(cause); + } + } +} diff --git a/src/main/java/com/google/inject/internal/SingleParameterInjector.java b/src/main/java/com/google/inject/internal/SingleParameterInjector.java new file mode 100644 index 0000000..1d9b3e6 --- /dev/null +++ b/src/main/java/com/google/inject/internal/SingleParameterInjector.java @@ -0,0 +1,55 @@ +package com.google.inject.internal; + +import com.google.inject.spi.Dependency; + +/** + * Resolves a single parameter, to be used in a constructor or method invocation. + */ +final class SingleParameterInjector { + private static final Object[] NO_ARGUMENTS = {}; + + private final Dependency dependency; + private final BindingImpl binding; + + SingleParameterInjector(Dependency dependency, BindingImpl binding) { + this.dependency = dependency; + this.binding = binding; + } + + /** + * Returns an array of parameter values. + */ + static Object[] getAll(Errors errors, InternalContext context, + SingleParameterInjector[] parameterInjectors) throws ErrorsException { + if (parameterInjectors == null) { + return NO_ARGUMENTS; + } + + int numErrorsBefore = errors.size(); + + int size = parameterInjectors.length; + Object[] parameters = new Object[size]; + + // optimization: use manual for/each to save allocating an iterator here + for (int i = 0; i < size; i++) { + SingleParameterInjector parameterInjector = parameterInjectors[i]; + try { + parameters[i] = parameterInjector.inject(errors, context); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + + errors.throwIfNewErrors(numErrorsBefore); + return parameters; + } + + private T inject(Errors errors, InternalContext context) throws ErrorsException { + Dependency previous = context.pushDependency(dependency, binding.getSource()); + try { + return binding.getInternalFactory().get(errors.withSource(dependency), context, dependency, false); + } finally { + context.popStateAndSetDependency(previous); + } + } +} diff --git a/src/main/java/com/google/inject/internal/SingletonScope.java b/src/main/java/com/google/inject/internal/SingletonScope.java new file mode 100644 index 0000000..995b932 --- /dev/null +++ b/src/main/java/com/google/inject/internal/SingletonScope.java @@ -0,0 +1,341 @@ +package com.google.inject.internal; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.internal.CycleDetectingLock.CycleDetectingLockFactory; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.DependencyAndSource; +import com.google.inject.spi.Message; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * One instance per {@link Injector}. Also see {@code @}{@link Singleton}. + * + * Introduction from the author: + * Implementation of this class seems unreasonably complicated at the first sight. + * I fully agree with you, that the beast below is very complex + * and it's hard to reason on how does it work or not. + * Still I want to assure you that hundreds(?) of hours were thrown + * into making this code simple, while still maintaining Singleton contract. + * + * Anyway, why is it so complex? Singleton scope does not seem to be that unique. + * 1) Guice has never truly expected to be used in multi threading environment + * with many Injectors working alongside each other. There is almost no + * code with Guice that propagates state between threads. And Singleton + * scope is The exception. + * 2) Guice supports circular dependencies and thus manages proxy objects. + * There is no interface that allows user defined Scopes to create proxies, + * it is expected to be done by Guice. Singleton scope needs to be + * able to detect circular dependencies spanning several threads, + * therefore Singleton scope needs to be able to create these proxies. + * 3) To make things worse, Guice has a very tricky definition for a binding + * resolution when Injectors are in in a parent/child relationship. + * And Scope does not have access to this information by design, + * the only real action that Scope can do is to call or not to call a creator. + * 4) There is no readily available code in Guice that can detect a potential + * deadlock, and no code for handling dependency cycles spanning several threads. + * This is significantly harder as all the dependencies in a thread at runtime + * can be represented with a list, where in a multi threaded environment + * we have more complex dependency trees. + * 5) Guice has a pretty strong contract regarding Garbage Collection, + * which often prevents us from linking objects directly. + * So simple domain specific code can not be written and intermediary + * id objects need to be managed. + * 6) Guice is relatively fast and we should not make things worse. + * We're trying our best to optimize synchronization for speed and memory. + * Happy path should be almost as fast as in a single threaded solution + * and should not take much more memory. + * 7) Error message generation in Guice was not meant to be used like this and to work around + * its APIs we need a lot of code. Additional complexity comes from inherent data races + * as message is only generated when failure occurs on proxy object generation. + * Things get ugly pretty fast. + * + */ +public class SingletonScope implements Scope { + + /** + * A sentinel value representing null. + */ + private static final Object NULL = new Object(); + + /** + * Allows us to detect when circular proxies are necessary. It's only used during singleton + * instance initialization, after initialization direct access through volatile field is used. + * + * NB: Factory uses {@link Key}s as a user locks ids, different injectors can + * share them. Cycles are detected properly as cycle detection does not rely on user locks ids, + * but error message generated could be less than ideal. + * + * TODO(user): we may use one factory per injector tree for optimization reasons + */ + private static final CycleDetectingLockFactory> cycleDetectingLockFactory = + new CycleDetectingLockFactory>(); + + /** + * Provides singleton scope with the following properties: + * - creates no more than one instance per Key as a creator is used no more than once, + * - result is cached and returned quickly on subsequent calls, + * - exception in a creator is not treated as instance creation and is not cached, + * - creates singletons in parallel whenever possible, + * - waits for dependent singletons to be created even across threads and when dependencies + * are shared as long as no circular dependencies are detected, + * - returns circular proxy only when circular dependencies are detected, + * - aside from that, blocking synchronization is only used for proxy creation and initialization, + * + * @see CycleDetectingLockFactory + */ + public Provider scope(final Key key, final Provider creator) { + /** + * Locking strategy: + * - volatile instance: double-checked locking for quick exit when scope is initialized, + * - constructionContext: manipulations with proxies list or instance initialization + * - creationLock: singleton instance creation, + * -- allows to guarantee only one instance per singleton, + * -- special type of a lock, that prevents potential deadlocks, + * -- guards constructionContext for all operations except proxy creation + */ + return new Provider() { + /** + * Circular proxies are used when potential deadlocks are detected. Guarded by itself. + * ConstructionContext is not thread-safe, so each call should be synchronized. + */ + final ConstructionContext constructionContext = new ConstructionContext(); + /** For each binding there is a separate lock that we hold during object creation. */ + final CycleDetectingLock> creationLock = cycleDetectingLockFactory.create(key); + /** + * The lazily initialized singleton instance. Once set, this will either have type T or will + * be equal to NULL. Would never be reset to null. + */ + volatile Object instance; + + @SuppressWarnings("DoubleCheckedLocking") + public T get() { + // cache volatile variable for the usual case of already initialized object + final Object initialInstance = instance; + if (initialInstance == null) { + // instance is not initialized yet + + // acquire lock for current binding to initialize an instance + final ListMultimap> locksCycle = + creationLock.lockOrDetectPotentialLocksCycle(); + if (locksCycle.isEmpty()) { + // this thread now owns creation of an instance + try { + // intentionally reread volatile variable to prevent double initialization + if (instance == null) { + // creator throwing an exception can cause circular proxies created in + // different thread to never be resolved, just a warning + T provided = creator.get(); + Object providedNotNull = provided == null ? NULL : provided; + + // scope called recursively can initialize instance as a side effect + if (instance == null) { + // instance is still not initialized, se we can proceed + + // don't remember proxies created by Guice on circular dependency + // detection within the same thread; they are not real instances to cache + if (Scopes.isCircularProxy(provided)) { + return provided; + } + + synchronized (constructionContext) { + // guarantee thread-safety for instance and proxies initialization + instance = providedNotNull; + constructionContext.setProxyDelegates(provided); + } + } else { + // safety assert in case instance was initialized + Preconditions.checkState(instance == providedNotNull, + "Singleton is called recursively returning different results"); + } + } + } catch (RuntimeException e) { + // something went wrong, be sure to clean a construction context + // this helps to prevent potential memory leaks in circular proxies list + synchronized (constructionContext) { + constructionContext.finishConstruction(); + } + throw e; + } finally { + // always release our creation lock, even on failures + creationLock.unlock(); + } + } else { + // potential deadlock detected, creation lock is not taken by this thread + synchronized (constructionContext) { + // guarantee thread-safety for instance and proxies initialization + if (instance == null) { + // InjectorImpl.callInContext() sets this context when scope is called from Guice + Map globalInternalContext = + InjectorImpl.getGlobalInternalContext(); + InternalContext internalContext = globalInternalContext.get(Thread.currentThread()); + + // creating a proxy to satisfy circular dependency across several threads + Dependency dependency = Preconditions.checkNotNull( + internalContext.getDependency(), + "globalInternalContext.get(currentThread()).getDependency()"); + Class rawType = dependency.getKey().getTypeLiteral().getRawType(); + + try { + @SuppressWarnings("unchecked") + T proxy = (T) constructionContext.createProxy( + new Errors(), internalContext.getInjectorOptions(), rawType); + return proxy; + } catch (ErrorsException e) { + // best effort to create a rich error message + List exceptionErrorMessages = e.getErrors().getMessages(); + // we expect an error thrown + Preconditions.checkState(exceptionErrorMessages.size() == 1); + // explicitly copy the map to guarantee iteration correctness + // it's ok to have a data race with other threads that are locked + Message cycleDependenciesMessage = createCycleDependenciesMessage( + ImmutableMap.copyOf(globalInternalContext), + locksCycle, + exceptionErrorMessages.get(0)); + // adding stack trace generated by us in addition to a standard one + throw new ProvisionException(ImmutableList.of( + cycleDependenciesMessage, exceptionErrorMessages.get(0))); + } + } + } + } + // at this point we're sure that singleton was initialized, + // reread volatile variable to catch all corner cases + + // caching volatile variable to minimize number of reads performed + final Object initializedInstance = instance; + Preconditions.checkState(initializedInstance != null, + "Internal error: Singleton is not initialized contrary to our expectations"); + @SuppressWarnings("unchecked") + T initializedTypedInstance = (T) initializedInstance; + return initializedInstance == NULL ? null : initializedTypedInstance; + } else { + // singleton is already initialized and local cache can be used + @SuppressWarnings("unchecked") + T typedInitialIntance = (T) initialInstance; + return initialInstance == NULL ? null : typedInitialIntance; + } + } + + /** + * Helper method to create beautiful and rich error descriptions. Best effort and slow. + * Tries its best to provide dependency information from injectors currently available + * in a global internal context. + * + *

The main thing being done is creating a list of Dependencies involved into + * lock cycle across all the threads involved. This is a structure we're creating: + *

+             * { Current Thread, C.class, B.class, Other Thread, B.class, C.class, Current Thread }
+             * To be inserted in the beginning by Guice: { A.class, B.class, C.class }
+             * 
+ * When we're calling Guice to create A and it fails in the deadlock while trying to + * create C, which is being created by another thread, which waits for B. List would + * be reversed before printing it to the end user. + */ + private Message createCycleDependenciesMessage( + Map globalInternalContext, + ListMultimap> locksCycle, + Message proxyCreationError) { + // this is the main thing that we'll show in an error message, + // current thread is populate by Guice + List sourcesCycle = Lists.newArrayList(); + sourcesCycle.add(Thread.currentThread()); + // temp map to speed up look ups + Map threadById = Maps.newHashMap(); + for (Thread thread : globalInternalContext.keySet()) { + threadById.put(thread.getId(), thread); + } + for (long lockedThreadId : locksCycle.keySet()) { + Thread lockedThread = threadById.get(lockedThreadId); + List> lockedKeys = Collections.unmodifiableList(locksCycle.get(lockedThreadId)); + if (lockedThread == null) { + // thread in a lock cycle is already terminated + continue; + } + List dependencyChain = null; + boolean allLockedKeysAreFoundInDependencies = false; + // thread in a cycle is still present + InternalContext lockedThreadInternalContext = globalInternalContext.get(lockedThread); + if (lockedThreadInternalContext != null) { + dependencyChain = lockedThreadInternalContext.getDependencyChain(); + + // check that all of the keys are still present in dependency chain in order + List> lockedKeysToFind = Lists.newLinkedList(lockedKeys); + // check stack trace of the thread + for (DependencyAndSource d : dependencyChain) { + Dependency dependency = d.getDependency(); + if (dependency == null) { + continue; + } + if (dependency.getKey().equals(lockedKeysToFind.get(0))) { + lockedKeysToFind.remove(0); + if (lockedKeysToFind.isEmpty()) { + // everything is found! + allLockedKeysAreFoundInDependencies = true; + break; + } + } + } + } + if (allLockedKeysAreFoundInDependencies) { + // all keys are present in a dependency chain of a thread's last injector, + // highly likely that we just have discovered a dependency + // chain that is part of a lock cycle starting with the first lock owned + Key firstLockedKey = lockedKeys.get(0); + boolean firstLockedKeyFound = false; + for (DependencyAndSource d : dependencyChain) { + Dependency dependency = d.getDependency(); + if (dependency == null) { + continue; + } + if (firstLockedKeyFound) { + sourcesCycle.add(dependency); + sourcesCycle.add(d.getBindingSource()); + } else if (dependency.getKey().equals(firstLockedKey)) { + firstLockedKeyFound = true; + // for the very first one found we don't care why, so no dependency is added + sourcesCycle.add(d.getBindingSource()); + } + } + } else { + // something went wrong and not all keys are present in a state of an injector + // that was used last for a current thread. + // let's add all keys we're aware of, still better than nothing + sourcesCycle.addAll(lockedKeys); + } + // mentions that a tread is a part of a cycle + sourcesCycle.add(lockedThread); + } + return new Message( + sourcesCycle, + String.format("Encountered circular dependency spanning several threads. %s", + proxyCreationError.getMessage()), + null); + } + + @Override + public String toString() { + return String.format("%s[%s]", creator, Scopes.SINGLETON); + } + }; + } + + @Override + public String toString() { + return "Scopes.SINGLETON"; + } +} diff --git a/src/main/java/com/google/inject/internal/State.java b/src/main/java/com/google/inject/internal/State.java new file mode 100644 index 0000000..9cc4aa4 --- /dev/null +++ b/src/main/java/com/google/inject/internal/State.java @@ -0,0 +1,185 @@ +package com.google.inject.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Scope; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; +import com.google.inject.spi.ProvisionListenerBinding; +import com.google.inject.spi.ScopeBinding; +import com.google.inject.spi.TypeConverterBinding; +import com.google.inject.spi.TypeListenerBinding; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The inheritable data within an injector. This class is intended to allow parent and local + * injector data to be accessed as a unit. + * + */ +interface State { + + static final State NONE = new State() { + public State parent() { + throw new UnsupportedOperationException(); + } + + public BindingImpl getExplicitBinding(Key key) { + return null; + } + + public Map, Binding> getExplicitBindingsThisLevel() { + throw new UnsupportedOperationException(); + } + + public void putBinding(Key key, BindingImpl binding) { + throw new UnsupportedOperationException(); + } + + public ScopeBinding getScopeBinding(Class scopingAnnotation) { + return null; + } + + public void putScopeBinding(Class annotationType, ScopeBinding scope) { + throw new UnsupportedOperationException(); + } + + public void addConverter(TypeConverterBinding typeConverterBinding) { + throw new UnsupportedOperationException(); + } + + public TypeConverterBinding getConverter(String stringValue, TypeLiteral type, Errors errors, + Object source) { + throw new UnsupportedOperationException(); + } + + public Iterable getConvertersThisLevel() { + return ImmutableSet.of(); + } + + public void addTypeListener(TypeListenerBinding typeListenerBinding) { + throw new UnsupportedOperationException(); + } + + public List getTypeListenerBindings() { + return ImmutableList.of(); + } + + public void addProvisionListener(ProvisionListenerBinding provisionListenerBinding) { + throw new UnsupportedOperationException(); + } + + public List getProvisionListenerBindings() { + return ImmutableList.of(); + } + + public void addScanner(ModuleAnnotatedMethodScannerBinding scanner) { + throw new UnsupportedOperationException(); + } + + public List getScannerBindings() { + return ImmutableList.of(); + } + + public void blacklist(Key key, State state, Object source) { + } + + public boolean isBlacklisted(Key key) { + return true; + } + + public Set getSourcesForBlacklistedKey(Key key) { + throw new UnsupportedOperationException(); + } + + public Object lock() { + throw new UnsupportedOperationException(); + } + + public Object singletonCreationLock() { + throw new UnsupportedOperationException(); + } + + public Map, Scope> getScopes() { + return ImmutableMap.of(); + } + }; + + State parent(); + + /** + * Gets a binding which was specified explicitly in a module, or null. + */ + BindingImpl getExplicitBinding(Key key); + + /** + * Returns the explicit bindings at this level only. + */ + Map, Binding> getExplicitBindingsThisLevel(); + + void putBinding(Key key, BindingImpl binding); + + ScopeBinding getScopeBinding(Class scopingAnnotation); + + void putScopeBinding(Class annotationType, ScopeBinding scope); + + void addConverter(TypeConverterBinding typeConverterBinding); + + /** + * Returns the matching converter for {@code type}, or null if none match. + */ + TypeConverterBinding getConverter( + String stringValue, TypeLiteral type, Errors errors, Object source); + + /** + * Returns all converters at this level only. + */ + Iterable getConvertersThisLevel(); + + void addTypeListener(TypeListenerBinding typeListenerBinding); + + List getTypeListenerBindings(); + + void addProvisionListener(ProvisionListenerBinding provisionListenerBinding); + + List getProvisionListenerBindings(); + + void addScanner(ModuleAnnotatedMethodScannerBinding scanner); + + List getScannerBindings(); + + /** + * Forbids the corresponding injector from creating a binding to {@code key}. Child injectors + * blacklist their bound keys on their parent injectors to prevent just-in-time bindings on the + * parent injector that would conflict and pass along their state to control the lifetimes. + */ + void blacklist(Key key, State state, Object source); + + /** + * Returns true if {@code key} is forbidden from being bound in this injector. This indicates that + * one of this injector's descendent's has bound the key. + */ + boolean isBlacklisted(Key key); + + /** + * Returns the source of a blacklisted key. + */ + Set getSourcesForBlacklistedKey(Key key); + + /** + * Returns the shared lock for all injector data. This is a low-granularity, high-contention lock + * to be used when reading mutable data (ie. just-in-time bindings, and binding blacklists). + */ + Object lock(); + + /** + * Returns all the scope bindings at this level and parent levels. + */ + Map, Scope> getScopes(); +} diff --git a/src/main/java/com/google/inject/internal/TypeConverterBindingProcessor.java b/src/main/java/com/google/inject/internal/TypeConverterBindingProcessor.java new file mode 100644 index 0000000..59bf48a --- /dev/null +++ b/src/main/java/com/google/inject/internal/TypeConverterBindingProcessor.java @@ -0,0 +1,172 @@ +package com.google.inject.internal; + +import com.google.inject.TypeLiteral; +import com.google.inject.internal.util.SourceProvider; +import com.google.inject.matcher.AbstractMatcher; +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.TypeConverter; +import com.google.inject.spi.TypeConverterBinding; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * Handles {@code Binder.convertToTypes} commands. + * + */ +final class TypeConverterBindingProcessor extends AbstractProcessor { + + TypeConverterBindingProcessor(Errors errors) { + super(errors); + } + + /** + * Installs default converters for primitives, enums, and class literals. + */ + static void prepareBuiltInConverters(InjectorImpl injector) { + // Configure type converters. + convertToPrimitiveType(injector, int.class, Integer.class); + convertToPrimitiveType(injector, long.class, Long.class); + convertToPrimitiveType(injector, boolean.class, Boolean.class); + convertToPrimitiveType(injector, byte.class, Byte.class); + convertToPrimitiveType(injector, short.class, Short.class); + convertToPrimitiveType(injector, float.class, Float.class); + convertToPrimitiveType(injector, double.class, Double.class); + + convertToClass(injector, Character.class, new TypeConverter() { + public Object convert(String value, TypeLiteral toType) { + value = value.trim(); + if (value.length() != 1) { + throw new RuntimeException("Length != 1."); + } + return value.charAt(0); + } + + @Override + public String toString() { + return "TypeConverter"; + } + }); + + convertToClasses(injector, Matchers.subclassesOf(Enum.class), new TypeConverter() { + @SuppressWarnings("unchecked") + public Object convert(String value, TypeLiteral toType) { + return Enum.valueOf((Class) toType.getRawType(), value); + } + + @Override + public String toString() { + return "TypeConverter>"; + } + }); + + internalConvertToTypes(injector, new AbstractMatcher>() { + public boolean matches(TypeLiteral typeLiteral) { + return typeLiteral.getRawType() == Class.class; + } + + @Override + public String toString() { + return "Class"; + } + }, + new TypeConverter() { + @SuppressWarnings("unchecked") + public Object convert(String value, TypeLiteral toType) { + try { + return Class.forName(value); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + public String toString() { + return "TypeConverter>"; + } + } + ); + } + + private static void convertToPrimitiveType(InjectorImpl injector, Class primitiveType, + final Class wrapperType) { + try { + final Method parser = wrapperType.getMethod( + "parse" + capitalize(primitiveType.getName()), String.class); + + TypeConverter typeConverter = new TypeConverter() { + @SuppressWarnings("unchecked") + public Object convert(String value, TypeLiteral toType) { + try { + return parser.invoke(null, value); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getTargetException().getMessage()); + } + } + + @Override + public String toString() { + return "TypeConverter<" + wrapperType.getSimpleName() + ">"; + } + }; + + convertToClass(injector, wrapperType, typeConverter); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + private static void convertToClass(InjectorImpl injector, Class type, + TypeConverter converter) { + convertToClasses(injector, Matchers.identicalTo(type), converter); + } + + private static void convertToClasses(InjectorImpl injector, + final Matcher> typeMatcher, TypeConverter converter) { + internalConvertToTypes(injector, new AbstractMatcher>() { + public boolean matches(TypeLiteral typeLiteral) { + Type type = typeLiteral.getType(); + if (!(type instanceof Class)) { + return false; + } + Class clazz = (Class) type; + return typeMatcher.matches(clazz); + } + + @Override + public String toString() { + return typeMatcher.toString(); + } + }, converter); + } + + private static void internalConvertToTypes(InjectorImpl injector, + Matcher> typeMatcher, + TypeConverter converter) { + injector.state.addConverter( + new TypeConverterBinding(SourceProvider.UNKNOWN_SOURCE, typeMatcher, converter)); + } + + private static String capitalize(String s) { + if (s.length() == 0) { + return s; + } + char first = s.charAt(0); + char capitalized = Character.toUpperCase(first); + return (first == capitalized) + ? s + : capitalized + s.substring(1); + } + + @Override + public Boolean visit(TypeConverterBinding command) { + injector.state.addConverter(new TypeConverterBinding( + command.getSource(), command.getTypeMatcher(), command.getTypeConverter())); + return true; + } + +} diff --git a/src/main/java/com/google/inject/internal/UniqueAnnotations.java b/src/main/java/com/google/inject/internal/UniqueAnnotations.java new file mode 100644 index 0000000..bf3be33 --- /dev/null +++ b/src/main/java/com/google/inject/internal/UniqueAnnotations.java @@ -0,0 +1,58 @@ +package com.google.inject.internal; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +public class UniqueAnnotations { + private static final AtomicInteger nextUniqueValue = new AtomicInteger(1); + + private UniqueAnnotations() { + } + + /** + * Returns an annotation instance that is not equal to any other annotation + * instances, for use in creating distinct {@link com.google.inject.Key}s. + */ + public static Annotation create() { + return create(nextUniqueValue.getAndIncrement()); + } + + static Annotation create(final int value) { + return new Internal() { + public int value() { + return value; + } + + public Class annotationType() { + return Internal.class; + } + + @Override + public String toString() { + return "@" + Internal.class.getName() + "(value=" + value + ")"; + } + + @Override + public boolean equals(Object o) { + return o instanceof Internal + && ((Internal) o).value() == value(); + } + + @Override + public int hashCode() { + return (127 * "value".hashCode()) ^ value; + } + }; + } + + @Retention(RUNTIME) + @BindingAnnotation + @interface Internal { + int value(); + } +} diff --git a/src/main/java/com/google/inject/internal/UntargettedBindingImpl.java b/src/main/java/com/google/inject/internal/UntargettedBindingImpl.java new file mode 100644 index 0000000..907e543 --- /dev/null +++ b/src/main/java/com/google/inject/internal/UntargettedBindingImpl.java @@ -0,0 +1,64 @@ +package com.google.inject.internal; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.UntargettedBinding; + +final class UntargettedBindingImpl extends BindingImpl implements UntargettedBinding { + + UntargettedBindingImpl(InjectorImpl injector, Key key, Object source) { + super(injector, key, source, new InternalFactory() { + public T get(Errors errors, InternalContext context, Dependency dependency, boolean linked) { + throw new AssertionError(); + } + }, Scoping.UNSCOPED); + } + + public UntargettedBindingImpl(Object source, Key key, Scoping scoping) { + super(source, key, scoping); + } + + public V acceptTargetVisitor(BindingTargetVisitor visitor) { + return visitor.visit(this); + } + + public BindingImpl withScoping(Scoping scoping) { + return new UntargettedBindingImpl(getSource(), getKey(), scoping); + } + + public BindingImpl withKey(Key key) { + return new UntargettedBindingImpl(getSource(), key, getScoping()); + } + + public void applyTo(Binder binder) { + getScoping().applyTo(binder.withSource(getSource()).bind(getKey())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(UntargettedBinding.class) + .add("key", getKey()) + .add("source", getSource()) + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof UntargettedBindingImpl) { + UntargettedBindingImpl o = (UntargettedBindingImpl) obj; + return getKey().equals(o.getKey()) + && getScoping().equals(o.getScoping()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey(), getScoping()); + } +} diff --git a/src/main/java/com/google/inject/internal/UntargettedBindingProcessor.java b/src/main/java/com/google/inject/internal/UntargettedBindingProcessor.java new file mode 100644 index 0000000..d7c2f19 --- /dev/null +++ b/src/main/java/com/google/inject/internal/UntargettedBindingProcessor.java @@ -0,0 +1,52 @@ +package com.google.inject.internal; + +import com.google.inject.Binding; +import com.google.inject.spi.UntargettedBinding; + +/** + * Processes just UntargettedBindings. + * + */ +class UntargettedBindingProcessor extends AbstractBindingProcessor { + + UntargettedBindingProcessor(Errors errors, ProcessedBindingData bindingData) { + super(errors, bindingData); + } + + @Override + public Boolean visit(Binding binding) { + return binding.acceptTargetVisitor(new Processor((BindingImpl) binding) { + public Boolean visit(UntargettedBinding untargetted) { + prepareBinding(); + + // Error: Missing implementation. + // Example: bind(Date.class).annotatedWith(Red.class); + // We can't assume abstract types aren't injectable. They may have an + // @ImplementedBy annotation or something. + if (key.getAnnotationType() != null) { + errors.missingImplementation(key); + putBinding(invalidBinding(injector, key, source)); + return true; + } + + // This cast is safe after the preceeding check. + try { + BindingImpl binding = injector.createUninitializedBinding( + key, scoping, source, errors, false); + scheduleInitialization(binding); + putBinding(binding); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + putBinding(invalidBinding(injector, key, source)); + } + + return true; + } + + @Override + protected Boolean visitOther(Binding binding) { + return false; + } + }); + } +} diff --git a/src/main/java/com/google/inject/internal/WeakKeySet.java b/src/main/java/com/google/inject/internal/WeakKeySet.java new file mode 100644 index 0000000..a4a48c4 --- /dev/null +++ b/src/main/java/com/google/inject/internal/WeakKeySet.java @@ -0,0 +1,137 @@ +package com.google.inject.internal; + +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalCause; +import com.google.common.cache.RemovalListener; +import com.google.common.cache.RemovalNotification; +import com.google.common.collect.LinkedHashMultiset; +import com.google.common.collect.Maps; +import com.google.common.collect.Multiset; +import com.google.common.collect.Sets; +import com.google.inject.Key; +import com.google.inject.internal.util.SourceProvider; + +import java.util.Map; +import java.util.Set; + +/** + * Minimal set that doesn't hold strong references to the contained keys. + */ +final class WeakKeySet { + + /** + * This is already locked externally on add and getSources but we need it to handle clean up in + * the evictionCache's RemovalListener. + */ + private final Object lock; + private Map, Multiset> backingMap; + /** + * Tracks child injector lifetimes and evicts blacklisted keys/sources after the child injector is + * garbage collected. + */ + private final Cache> evictionCache = CacheBuilder.newBuilder() + .weakKeys() + .removalListener( + new RemovalListener>() { + @Override + public void onRemoval(RemovalNotification> notification) { + Preconditions.checkState(RemovalCause.COLLECTED.equals(notification.getCause())); + + cleanUpForCollectedState(notification.getValue()); + } + }) + .build(); + + WeakKeySet(Object lock) { + this.lock = lock; + } + + /** + * There may be multiple child injectors blacklisting a certain key so only remove the source + * that's relevant. + */ + private void cleanUpForCollectedState(Set keysAndSources) { + synchronized (lock) { + for (KeyAndSource keyAndSource : keysAndSources) { + Multiset set = backingMap.get(keyAndSource.key); + if (set != null) { + set.remove(keyAndSource.source); + if (set.isEmpty()) { + backingMap.remove(keyAndSource.key); + } + } + } + } + } + + public void add(Key key, State state, Object source) { + if (backingMap == null) { + backingMap = Maps.newHashMap(); + } + // if it's an instanceof Class, it was a JIT binding, which we don't + // want to retain. + if (source instanceof Class || source == SourceProvider.UNKNOWN_SOURCE) { + source = null; + } + Multiset sources = backingMap.get(key); + if (sources == null) { + sources = LinkedHashMultiset.create(); + backingMap.put(key, sources); + } + Object convertedSource = Errors.convert(source); + sources.add(convertedSource); + + // Avoid all the extra work if we can. + if (state.parent() != State.NONE) { + Set keyAndSources = evictionCache.getIfPresent(state); + if (keyAndSources == null) { + evictionCache.put(state, keyAndSources = Sets.newHashSet()); + } + keyAndSources.add(new KeyAndSource(key, convertedSource)); + } + } + + public boolean contains(Key key) { + evictionCache.cleanUp(); + return backingMap != null && backingMap.containsKey(key); + } + + public Set getSources(Key key) { + evictionCache.cleanUp(); + Multiset sources = (backingMap == null) ? null : backingMap.get(key); + return (sources == null) ? null : sources.elementSet(); + } + + private static final class KeyAndSource { + final Key key; + final Object source; + + KeyAndSource(Key key, Object source) { + this.key = key; + this.source = source; + } + + @Override + public int hashCode() { + return Objects.hashCode(key, source); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof KeyAndSource)) { + return false; + } + + KeyAndSource other = (KeyAndSource) obj; + return Objects.equal(key, other.key) + && Objects.equal(source, other.source); + } + } +} diff --git a/src/main/java/com/google/inject/internal/util/Classes.java b/src/main/java/com/google/inject/internal/util/Classes.java new file mode 100644 index 0000000..d84c6f0 --- /dev/null +++ b/src/main/java/com/google/inject/internal/util/Classes.java @@ -0,0 +1,64 @@ +package com.google.inject.internal.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Class utilities. + */ +public final class Classes { + + public static boolean isInnerClass(Class clazz) { + return !Modifier.isStatic(clazz.getModifiers()) + && clazz.getEnclosingClass() != null; + } + + public static boolean isConcrete(Class clazz) { + int modifiers = clazz.getModifiers(); + return !clazz.isInterface() && !Modifier.isAbstract(modifiers); + } + + /** + * Formats a member as concise string, such as {@code java.util.ArrayList.size}, + * {@code java.util.ArrayList()} or {@code java.util.List.remove()}. + */ + public static String toString(Member member) { + Class memberType = Classes.memberType(member); + + if (memberType == Method.class) { + return member.getDeclaringClass().getName() + "." + member.getName() + "()"; + } else if (memberType == Field.class) { + return member.getDeclaringClass().getName() + "." + member.getName(); + } else if (memberType == Constructor.class) { + return member.getDeclaringClass().getName() + ".()"; + } else { + throw new AssertionError(); + } + } + + /** + * Returns {@code Field.class}, {@code Method.class} or {@code Constructor.class}. + */ + public static Class memberType(Member member) { + checkNotNull(member, "member"); + + if (member instanceof Field) { + return Field.class; + + } else if (member instanceof Method) { + return Method.class; + + } else if (member instanceof Constructor) { + return Constructor.class; + + } else { + throw new IllegalArgumentException( + "Unsupported implementation class for Member, " + member.getClass()); + } + } +} diff --git a/src/main/java/com/google/inject/internal/util/SourceProvider.java b/src/main/java/com/google/inject/internal/util/SourceProvider.java new file mode 100644 index 0000000..a068ca2 --- /dev/null +++ b/src/main/java/com/google/inject/internal/util/SourceProvider.java @@ -0,0 +1,93 @@ +package com.google.inject.internal.util; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Provides access to the calling line of code. + */ +public final class SourceProvider { + + /** + * Indicates that the source is unknown. + */ + public static final Object UNKNOWN_SOURCE = "[unknown source]"; + public static final SourceProvider DEFAULT_INSTANCE + = new SourceProvider(ImmutableSet.of(SourceProvider.class.getName())); + private final SourceProvider parent; + private final ImmutableSet classNamesToSkip; + + private SourceProvider(Iterable classesToSkip) { + this(null, classesToSkip); + } + + private SourceProvider(SourceProvider parent, Iterable classesToSkip) { + this.parent = parent; + + ImmutableSet.Builder classNamesToSkipBuilder = ImmutableSet.builder(); + for (String classToSkip : classesToSkip) { + if (parent == null || !parent.shouldBeSkipped(classToSkip)) { + classNamesToSkipBuilder.add(classToSkip); + } + } + this.classNamesToSkip = classNamesToSkipBuilder.build(); + } + + /** + * Returns the class names as Strings + */ + private static List asStrings(Class... classes) { + List strings = Lists.newArrayList(); + for (Class c : classes) { + strings.add(c.getName()); + } + return strings; + } + + /** + * Returns a new instance that also skips {@code moreClassesToSkip}. + */ + public SourceProvider plusSkippedClasses(Class... moreClassesToSkip) { + return new SourceProvider(this, asStrings(moreClassesToSkip)); + } + + /** + * Returns true if the className should be skipped. + */ + private boolean shouldBeSkipped(String className) { + return (parent != null && parent.shouldBeSkipped(className)) + || classNamesToSkip.contains(className); + } + + /** + * Returns the calling line of code. The selected line is the nearest to the top of the stack that + * is not skipped. + */ + public StackTraceElement get(StackTraceElement[] stackTraceElements) { + Preconditions.checkNotNull(stackTraceElements, "The stack trace elements cannot be null."); + for (final StackTraceElement element : stackTraceElements) { + String className = element.getClassName(); + + if (!shouldBeSkipped(className)) { + return element; + } + } + throw new AssertionError(); + } + + /** + * Returns the non-skipped module class name. + */ + public Object getFromClassNames(List moduleClassNames) { + Preconditions.checkNotNull(moduleClassNames, "The list of module class names cannot be null."); + for (final String moduleClassName : moduleClassNames) { + if (!shouldBeSkipped(moduleClassName)) { + return new StackTraceElement(moduleClassName, "configure", null, -1); + } + } + return UNKNOWN_SOURCE; + } +} diff --git a/src/main/java/com/google/inject/internal/util/StackTraceElements.java b/src/main/java/com/google/inject/internal/util/StackTraceElements.java new file mode 100644 index 0000000..10183be --- /dev/null +++ b/src/main/java/com/google/inject/internal/util/StackTraceElements.java @@ -0,0 +1,165 @@ +package com.google.inject.internal.util; + +import com.google.common.collect.MapMaker; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Member; +import java.util.Map; + +/** + * Creates stack trace elements for members. + */ +public class StackTraceElements { + + private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; + private static final InMemoryStackTraceElement[] EMPTY_INMEMORY_STACK_TRACE = + new InMemoryStackTraceElement[0]; + private static final String UNKNOWN_SOURCE = "Unknown Source"; + private static Map cache = new MapMaker().makeMap(); + + public static Object forMember(Member member) { + if (member == null) { + return SourceProvider.UNKNOWN_SOURCE; + } + + Class declaringClass = member.getDeclaringClass(); + + String fileName = null; + int lineNumber = -1; + + Class memberType = Classes.memberType(member); + String memberName = memberType == Constructor.class ? "" : member.getName(); + return new StackTraceElement(declaringClass.getName(), memberName, fileName, lineNumber); + } + + public static Object forType(Class implementation) { + String fileName = null; + int lineNumber = -1; + + return new StackTraceElement(implementation.getName(), "class", fileName, lineNumber); + } + + /** + * Clears the internal cache for {@link StackTraceElement StackTraceElements}. + */ + public static void clearCache() { + cache.clear(); + } + + /** + * Returns encoded in-memory version of {@link StackTraceElement StackTraceElements}. + */ + public static InMemoryStackTraceElement[] convertToInMemoryStackTraceElement( + StackTraceElement[] stackTraceElements) { + if (stackTraceElements.length == 0) { + return EMPTY_INMEMORY_STACK_TRACE; + } + InMemoryStackTraceElement[] inMemoryStackTraceElements = + new InMemoryStackTraceElement[stackTraceElements.length]; + for (int i = 0; i < stackTraceElements.length; i++) { + inMemoryStackTraceElements[i] = + weakIntern(new InMemoryStackTraceElement(stackTraceElements[i])); + } + return inMemoryStackTraceElements; + } + + /** + * Decodes in-memory stack trace elements to regular {@link StackTraceElement StackTraceElements}. + */ + public static StackTraceElement[] convertToStackTraceElement( + InMemoryStackTraceElement[] inMemoryStackTraceElements) { + if (inMemoryStackTraceElements.length == 0) { + return EMPTY_STACK_TRACE; + } + StackTraceElement[] stackTraceElements = + new StackTraceElement[inMemoryStackTraceElements.length]; + for (int i = 0; i < inMemoryStackTraceElements.length; i++) { + String declaringClass = inMemoryStackTraceElements[i].getClassName(); + String methodName = inMemoryStackTraceElements[i].getMethodName(); + int lineNumber = inMemoryStackTraceElements[i].getLineNumber(); + stackTraceElements[i] = + new StackTraceElement(declaringClass, methodName, UNKNOWN_SOURCE, lineNumber); + } + return stackTraceElements; + } + + private static InMemoryStackTraceElement weakIntern( + InMemoryStackTraceElement inMemoryStackTraceElement) { + InMemoryStackTraceElement cached = + (InMemoryStackTraceElement) cache.get(inMemoryStackTraceElement); + if (cached != null) { + return cached; + } + inMemoryStackTraceElement = new InMemoryStackTraceElement( + weakIntern(inMemoryStackTraceElement.getClassName()), + weakIntern(inMemoryStackTraceElement.getMethodName()), + inMemoryStackTraceElement.getLineNumber()); + cache.put(inMemoryStackTraceElement, inMemoryStackTraceElement); + return inMemoryStackTraceElement; + } + + private static String weakIntern(String s) { + String cached = (String) cache.get(s); + if (cached != null) { + return cached; + } + cache.put(s, s); + return s; + } + + /** + * In-Memory version of {@link StackTraceElement} that does not store the file name. + */ + public static class InMemoryStackTraceElement { + private String declaringClass; + private String methodName; + private int lineNumber; + + InMemoryStackTraceElement(StackTraceElement ste) { + this(ste.getClassName(), ste.getMethodName(), ste.getLineNumber()); + } + + InMemoryStackTraceElement(String declaringClass, String methodName, int lineNumber) { + this.declaringClass = declaringClass; + this.methodName = methodName; + this.lineNumber = lineNumber; + } + + String getClassName() { + return declaringClass; + } + + String getMethodName() { + return methodName; + } + + int getLineNumber() { + return lineNumber; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof InMemoryStackTraceElement)) { + return false; + } + InMemoryStackTraceElement e = (InMemoryStackTraceElement) obj; + return e.declaringClass.equals(declaringClass) && e.lineNumber == lineNumber && + methodName.equals(e.methodName); + } + + @Override + public int hashCode() { + int result = 31 * declaringClass.hashCode() + methodName.hashCode(); + result = 31 * result + lineNumber; + return result; + } + + @Override + public String toString() { + return declaringClass + "." + methodName + "(" + lineNumber + ")"; + } + } +} diff --git a/src/main/java/com/google/inject/internal/util/Stopwatch.java b/src/main/java/com/google/inject/internal/util/Stopwatch.java new file mode 100644 index 0000000..e8c692a --- /dev/null +++ b/src/main/java/com/google/inject/internal/util/Stopwatch.java @@ -0,0 +1,31 @@ +package com.google.inject.internal.util; + +import java.util.logging.Logger; + +/** + * Enables simple performance monitoring. + */ +public final class Stopwatch { + private static final Logger logger = Logger.getLogger(Stopwatch.class.getName()); + + private long start = System.currentTimeMillis(); + + /** + * Resets and returns elapsed time in milliseconds. + */ + public long reset() { + long now = System.currentTimeMillis(); + try { + return now - start; + } finally { + start = now; + } + } + + /** + * Resets and logs elapsed time in milliseconds. + */ + public void resetAndLog(String label) { + logger.fine(label + ": " + reset() + "ms"); + } +} diff --git a/src/main/java/com/google/inject/matcher/AbstractMatcher.java b/src/main/java/com/google/inject/matcher/AbstractMatcher.java new file mode 100644 index 0000000..2267d67 --- /dev/null +++ b/src/main/java/com/google/inject/matcher/AbstractMatcher.java @@ -0,0 +1,76 @@ +package com.google.inject.matcher; + +/** + * Implements {@code and()} and {@code or()}. + * + */ +public abstract class AbstractMatcher implements Matcher { + + public Matcher and(final Matcher other) { + return new AndMatcher(this, other); + } + + public Matcher or(Matcher other) { + return new OrMatcher(this, other); + } + + private static class AndMatcher extends AbstractMatcher { + private final Matcher a, b; + + public AndMatcher(Matcher a, Matcher b) { + this.a = a; + this.b = b; + } + + public boolean matches(T t) { + return a.matches(t) && b.matches(t); + } + + @Override + public boolean equals(Object other) { + return other instanceof AndMatcher + && ((AndMatcher) other).a.equals(a) + && ((AndMatcher) other).b.equals(b); + } + + @Override + public int hashCode() { + return 41 * (a.hashCode() ^ b.hashCode()); + } + + @Override + public String toString() { + return "and(" + a + ", " + b + ")"; + } + } + + private static class OrMatcher extends AbstractMatcher { + private final Matcher a, b; + + public OrMatcher(Matcher a, Matcher b) { + this.a = a; + this.b = b; + } + + public boolean matches(T t) { + return a.matches(t) || b.matches(t); + } + + @Override + public boolean equals(Object other) { + return other instanceof OrMatcher + && ((OrMatcher) other).a.equals(a) + && ((OrMatcher) other).b.equals(b); + } + + @Override + public int hashCode() { + return 37 * (a.hashCode() ^ b.hashCode()); + } + + @Override + public String toString() { + return "or(" + a + ", " + b + ")"; + } + } +} diff --git a/src/main/java/com/google/inject/matcher/Matcher.java b/src/main/java/com/google/inject/matcher/Matcher.java new file mode 100644 index 0000000..51d73a6 --- /dev/null +++ b/src/main/java/com/google/inject/matcher/Matcher.java @@ -0,0 +1,24 @@ +package com.google.inject.matcher; + +/** + * Returns {@code true} or {@code false} for a given input. + */ +public interface Matcher { + + /** + * Returns {@code true} if this matches {@code t}, {@code false} otherwise. + */ + boolean matches(T t); + + /** + * Returns a new matcher which returns {@code true} if both this and the + * given matcher return {@code true}. + */ + Matcher and(Matcher other); + + /** + * Returns a new matcher which returns {@code true} if either this or the + * given matcher return {@code true}. + */ + Matcher or(Matcher other); +} diff --git a/src/main/java/com/google/inject/matcher/Matchers.java b/src/main/java/com/google/inject/matcher/Matchers.java new file mode 100644 index 0000000..8be3f8a --- /dev/null +++ b/src/main/java/com/google/inject/matcher/Matchers.java @@ -0,0 +1,385 @@ +package com.google.inject.matcher; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Matcher implementations. Supports matching classes and methods. + * + */ +public class Matchers { + private static final Matcher ANY = new Any(); + + private Matchers() { + } + + /** + * Returns a matcher which matches any input. + */ + public static Matcher any() { + return ANY; + } + + /** + * Inverts the given matcher. + */ + public static Matcher not(final Matcher p) { + return new Not(p); + } + + private static void checkForRuntimeRetention( + Class annotationType) { + Retention retention = annotationType.getAnnotation(Retention.class); + checkArgument(retention != null && retention.value() == RetentionPolicy.RUNTIME, + "Annotation %s is missing RUNTIME retention", annotationType.getSimpleName()); + } + + /** + * Returns a matcher which matches elements (methods, classes, etc.) + * with a given annotation. + */ + public static Matcher annotatedWith( + final Class annotationType) { + return new AnnotatedWithType(annotationType); + } + + /** + * Returns a matcher which matches elements (methods, classes, etc.) + * with a given annotation. + */ + public static Matcher annotatedWith( + final Annotation annotation) { + return new AnnotatedWith(annotation); + } + + /** + * Returns a matcher which matches subclasses of the given type (as well as + * the given type). + */ + public static Matcher subclassesOf(final Class superclass) { + return new SubclassesOf(superclass); + } + + /** + * Returns a matcher which matches objects equal to the given object. + */ + public static Matcher only(Object value) { + return new Only(value); + } + + /** + * Returns a matcher which matches only the given object. + */ + public static Matcher identicalTo(final Object value) { + return new IdenticalTo(value); + } + + /** + * Returns a matcher which matches classes in the given package. Packages are specific to their + * classloader, so classes with the same package name may not have the same package at runtime. + */ + public static Matcher inPackage(final Package targetPackage) { + return new InPackage(targetPackage); + } + + /** + * Returns a matcher which matches classes in the given package and its subpackages. Unlike + * {@link #inPackage(Package) inPackage()}, this matches classes from any classloader. + */ + public static Matcher inSubpackage(final String targetPackageName) { + return new InSubpackage(targetPackageName); + } + + /** + * Returns a matcher which matches methods with matching return types. + */ + public static Matcher returns( + final Matcher> returnType) { + return new Returns(returnType); + } + + private static class Any extends AbstractMatcher { + + public boolean matches(Object o) { + return true; + } + + @Override + public String toString() { + return "any()"; + } + + public Object readResolve() { + return any(); + } + } + + private static class Not extends AbstractMatcher { + final Matcher delegate; + + private Not(Matcher delegate) { + this.delegate = checkNotNull(delegate, "delegate"); + } + + public boolean matches(T t) { + return !delegate.matches(t); + } + + @Override + public boolean equals(Object other) { + return other instanceof Not + && ((Not) other).delegate.equals(delegate); + } + + @Override + public int hashCode() { + return -delegate.hashCode(); + } + + @Override + public String toString() { + return "not(" + delegate + ")"; + } + } + + private static class AnnotatedWithType extends AbstractMatcher { + private final Class annotationType; + + public AnnotatedWithType(Class annotationType) { + this.annotationType = checkNotNull(annotationType, "annotation type"); + checkForRuntimeRetention(annotationType); + } + + public boolean matches(AnnotatedElement element) { + return element.isAnnotationPresent(annotationType); + } + + @Override + public boolean equals(Object other) { + return other instanceof AnnotatedWithType + && ((AnnotatedWithType) other).annotationType.equals(annotationType); + } + + @Override + public int hashCode() { + return 37 * annotationType.hashCode(); + } + + @Override + public String toString() { + return "annotatedWith(" + annotationType.getSimpleName() + ".class)"; + } + } + + private static class AnnotatedWith extends AbstractMatcher { + private final Annotation annotation; + + public AnnotatedWith(Annotation annotation) { + this.annotation = checkNotNull(annotation, "annotation"); + checkForRuntimeRetention(annotation.annotationType()); + } + + public boolean matches(AnnotatedElement element) { + Annotation fromElement = element.getAnnotation(annotation.annotationType()); + return fromElement != null && annotation.equals(fromElement); + } + + @Override + public boolean equals(Object other) { + return other instanceof AnnotatedWith + && ((AnnotatedWith) other).annotation.equals(annotation); + } + + @Override + public int hashCode() { + return 37 * annotation.hashCode(); + } + + @Override + public String toString() { + return "annotatedWith(" + annotation + ")"; + } + } + + private static class SubclassesOf extends AbstractMatcher { + private final Class superclass; + + public SubclassesOf(Class superclass) { + this.superclass = checkNotNull(superclass, "superclass"); + } + + public boolean matches(Class subclass) { + return superclass.isAssignableFrom(subclass); + } + + @Override + public boolean equals(Object other) { + return other instanceof SubclassesOf + && ((SubclassesOf) other).superclass.equals(superclass); + } + + @Override + public int hashCode() { + return 37 * superclass.hashCode(); + } + + @Override + public String toString() { + return "subclassesOf(" + superclass.getSimpleName() + ".class)"; + } + } + + private static class Only extends AbstractMatcher { + private final Object value; + + public Only(Object value) { + this.value = checkNotNull(value, "value"); + } + + public boolean matches(Object other) { + return value.equals(other); + } + + @Override + public boolean equals(Object other) { + return other instanceof Only + && ((Only) other).value.equals(value); + } + + @Override + public int hashCode() { + return 37 * value.hashCode(); + } + + @Override + public String toString() { + return "only(" + value + ")"; + } + } + + private static class IdenticalTo extends AbstractMatcher { + private final Object value; + + public IdenticalTo(Object value) { + this.value = checkNotNull(value, "value"); + } + + public boolean matches(Object other) { + return value == other; + } + + @Override + public boolean equals(Object other) { + return other instanceof IdenticalTo + && ((IdenticalTo) other).value == value; + } + + @Override + public int hashCode() { + return 37 * System.identityHashCode(value); + } + + @Override + public String toString() { + return "identicalTo(" + value + ")"; + } + } + + private static class InPackage extends AbstractMatcher { + private final transient Package targetPackage; + private final String packageName; + + public InPackage(Package targetPackage) { + this.targetPackage = checkNotNull(targetPackage, "package"); + this.packageName = targetPackage.getName(); + } + + public boolean matches(Class c) { + return c.getPackage().equals(targetPackage); + } + + @Override + public boolean equals(Object other) { + return other instanceof InPackage + && ((InPackage) other).targetPackage.equals(targetPackage); + } + + @Override + public int hashCode() { + return 37 * targetPackage.hashCode(); + } + + @Override + public String toString() { + return "inPackage(" + targetPackage.getName() + ")"; + } + + public Object readResolve() { + return inPackage(Package.getPackage(packageName)); + } + } + + private static class InSubpackage extends AbstractMatcher { + private final String targetPackageName; + + public InSubpackage(String targetPackageName) { + this.targetPackageName = targetPackageName; + } + + public boolean matches(Class c) { + String classPackageName = c.getPackage().getName(); + return classPackageName.equals(targetPackageName) + || classPackageName.startsWith(targetPackageName + "."); + } + + @Override + public boolean equals(Object other) { + return other instanceof InSubpackage + && ((InSubpackage) other).targetPackageName.equals(targetPackageName); + } + + @Override + public int hashCode() { + return 37 * targetPackageName.hashCode(); + } + + @Override + public String toString() { + return "inSubpackage(" + targetPackageName + ")"; + } + } + + private static class Returns extends AbstractMatcher { + private final Matcher> returnType; + + public Returns(Matcher> returnType) { + this.returnType = checkNotNull(returnType, "return type matcher"); + } + + public boolean matches(Method m) { + return returnType.matches(m.getReturnType()); + } + + @Override + public boolean equals(Object other) { + return other instanceof Returns + && ((Returns) other).returnType.equals(returnType); + } + + @Override + public int hashCode() { + return 37 * returnType.hashCode(); + } + + @Override + public String toString() { + return "returns(" + returnType + ")"; + } + } +} diff --git a/src/main/java/com/google/inject/name/Named.java b/src/main/java/com/google/inject/name/Named.java new file mode 100644 index 0000000..70a6a19 --- /dev/null +++ b/src/main/java/com/google/inject/name/Named.java @@ -0,0 +1,19 @@ +package com.google.inject.name; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates named things. + */ +@Retention(RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@BindingAnnotation +public @interface Named { + String value(); +} diff --git a/src/main/java/com/google/inject/name/NamedImpl.java b/src/main/java/com/google/inject/name/NamedImpl.java new file mode 100644 index 0000000..f90f018 --- /dev/null +++ b/src/main/java/com/google/inject/name/NamedImpl.java @@ -0,0 +1,40 @@ +package com.google.inject.name; + +import java.lang.annotation.Annotation; + +import static com.google.common.base.Preconditions.checkNotNull; + +class NamedImpl implements Named { + + private final String value; + + public NamedImpl(String value) { + this.value = checkNotNull(value, "name"); + } + + public String value() { + return this.value; + } + + public int hashCode() { + // This is specified in java.lang.Annotation. + return (127 * "value".hashCode()) ^ value.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof Named)) { + return false; + } + + Named other = (Named) o; + return value.equals(other.value()); + } + + public String toString() { + return "@" + Named.class.getName() + "(value=" + value + ")"; + } + + public Class annotationType() { + return Named.class; + } +} diff --git a/src/main/java/com/google/inject/name/Names.java b/src/main/java/com/google/inject/name/Names.java new file mode 100644 index 0000000..045a717 --- /dev/null +++ b/src/main/java/com/google/inject/name/Names.java @@ -0,0 +1,53 @@ +package com.google.inject.name; + +import com.google.inject.Binder; +import com.google.inject.Key; + +import java.util.Enumeration; +import java.util.Map; +import java.util.Properties; + +/** + * Utility methods for use with {@code @}{@link Named}. + */ +public class Names { + + private Names() { + } + + /** + * Creates a {@link Named} annotation with {@code name} as the value. + */ + public static Named named(String name) { + return new NamedImpl(name); + } + + /** + * Creates a constant binding to {@code @Named(key)} for each entry in + * {@code properties}. + */ + public static void bindProperties(Binder binder, Map properties) { + binder = binder.skipSources(Names.class); + for (Map.Entry entry : properties.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + binder.bind(Key.get(String.class, new NamedImpl(key))).toInstance(value); + } + } + + /** + * Creates a constant binding to {@code @Named(key)} for each property. This + * method binds all properties including those inherited from + * {@link Properties#defaults defaults}. + */ + public static void bindProperties(Binder binder, Properties properties) { + binder = binder.skipSources(Names.class); + + // use enumeration to include the default properties + for (Enumeration e = properties.propertyNames(); e.hasMoreElements(); ) { + String propertyName = (String) e.nextElement(); + String value = properties.getProperty(propertyName); + binder.bind(Key.get(String.class, new NamedImpl(propertyName))).toInstance(value); + } + } +} diff --git a/src/main/java/com/google/inject/spi/BindingScopingVisitor.java b/src/main/java/com/google/inject/spi/BindingScopingVisitor.java new file mode 100644 index 0000000..d976d53 --- /dev/null +++ b/src/main/java/com/google/inject/spi/BindingScopingVisitor.java @@ -0,0 +1,40 @@ +package com.google.inject.spi; + +import com.google.inject.Scope; + +import java.lang.annotation.Annotation; + +/** + * Visits each of the strategies used to scope an injection. + * + * @param any type to be returned by the visit method. Use {@link Void} with + * {@code return null} if no return type is needed. + */ +public interface BindingScopingVisitor { + + /** + * Visit an eager singleton or single instance. This scope strategy is found on both module and + * injector bindings. + */ + V visitEagerSingleton(); + + /** + * Visit a scope instance. This scope strategy is found on both module and injector bindings. + */ + V visitScope(Scope scope); + + /** + * Visit a scope annotation. This scope strategy is found only on module bindings. The instance + * that implements this scope is registered by {@link com.google.inject.Binder#bindScope(Class, + * Scope) Binder.bindScope()}. + */ + V visitScopeAnnotation(Class scopeAnnotation); + + /** + * Visit an unspecified or unscoped strategy. On a module, this strategy indicates that the + * injector should use scoping annotations to find a scope. On an injector, it indicates that + * no scope is applied to the binding. An unscoped binding will behave like a scoped one when it + * is linked to a scoped binding. + */ + V visitNoScoping(); +} diff --git a/src/main/java/com/google/inject/spi/BindingTargetVisitor.java b/src/main/java/com/google/inject/spi/BindingTargetVisitor.java new file mode 100644 index 0000000..a68b2cb --- /dev/null +++ b/src/main/java/com/google/inject/spi/BindingTargetVisitor.java @@ -0,0 +1,66 @@ +package com.google.inject.spi; + +/** + * Visits each of the strategies used to find an instance to satisfy an injection. + * + * @param any type to be returned by the visit method. Use {@link Void} with + * {@code return null} if no return type is needed. + */ +public interface BindingTargetVisitor { + + /** + * Visit a instance binding. The same instance is returned for every injection. This target is + * found in both module and injector bindings. + */ + V visit(InstanceBinding binding); + + /** + * Visit a provider instance binding. The provider's {@code get} method is invoked to resolve + * injections. This target is found in both module and injector bindings. + */ + V visit(ProviderInstanceBinding binding); + + /** + * Visit a provider key binding. To resolve injections, the provider key is first resolved, then + * that provider's {@code get} method is invoked. This target is found in both module and injector + * bindings. + */ + V visit(ProviderKeyBinding binding); + + /** + * Visit a linked key binding. The other key's binding is used to resolve injections. This + * target is found in both module and injector bindings. + */ + V visit(LinkedKeyBinding binding); + + /** + * Visit a binding to a key exposed from an enclosed private environment. This target is only + * found in injector bindings. + */ + V visit(ExposedBinding binding); + + /** + * Visit an untargetted binding. This target is found only on module bindings. It indicates + * that the injector should use its implicit binding strategies to resolve injections. + */ + V visit(UntargettedBinding binding); + + /** + * Visit a constructor binding. To resolve injections, an instance is instantiated by invoking + * {@code constructor}. This target is found only on injector bindings. + */ + V visit(ConstructorBinding binding); + + /** + * Visit a binding created from converting a bound instance to a new type. The source binding + * has the same binding annotation but a different type. This target is found only on injector + * bindings. + */ + V visit(ConvertedConstantBinding binding); + + /** + * Visit a binding to a {@link com.google.inject.Provider} that delegates to the binding for the + * provided type. This target is found only on injector bindings. + */ + V visit(ProviderBinding binding); +} diff --git a/src/main/java/com/google/inject/spi/ConstructorBinding.java b/src/main/java/com/google/inject/spi/ConstructorBinding.java new file mode 100644 index 0000000..6233923 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ConstructorBinding.java @@ -0,0 +1,28 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; + +import java.util.Set; + +/** + * A binding to the constructor of a concrete clss. To resolve injections, an instance is + * instantiated by invoking the constructor. + * + */ +public interface ConstructorBinding extends Binding, HasDependencies { + + /** + * Gets the constructor this binding injects. + */ + InjectionPoint getConstructor(); + + /** + * Returns all instance method and field injection points on {@code type}. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + */ + Set getInjectableMembers(); + +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/ConvertedConstantBinding.java b/src/main/java/com/google/inject/spi/ConvertedConstantBinding.java new file mode 100644 index 0000000..9a27ec1 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ConvertedConstantBinding.java @@ -0,0 +1,34 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Key; + +import java.util.Set; + +/** + * A binding created from converting a bound instance to a new type. The source binding has the same + * binding annotation but a different type. + */ +public interface ConvertedConstantBinding extends Binding, HasDependencies { + + /** + * Returns the converted value. + */ + T getValue(); + + /** + * Returns the type converter binding used to convert the constant. + */ + TypeConverterBinding getTypeConverterBinding(); + + /** + * Returns the key for the source binding. That binding can be retrieved from an injector using + * {@link com.google.inject.Injector#getBinding(Key) Injector.getBinding(key)}. + */ + Key getSourceKey(); + + /** + * Returns a singleton set containing only the converted key. + */ + Set> getDependencies(); +} diff --git a/src/main/java/com/google/inject/spi/DefaultBindingScopingVisitor.java b/src/main/java/com/google/inject/spi/DefaultBindingScopingVisitor.java new file mode 100644 index 0000000..d5eb38e --- /dev/null +++ b/src/main/java/com/google/inject/spi/DefaultBindingScopingVisitor.java @@ -0,0 +1,38 @@ +package com.google.inject.spi; + +import com.google.inject.Scope; + +import java.lang.annotation.Annotation; + +/** + * No-op visitor for subclassing. All interface methods simply delegate to + * {@link #visitOther()}, returning its result. + * + * @param any type to be returned by the visit method. Use {@link Void} with + * {@code return null} if no return type is needed. + */ +public class DefaultBindingScopingVisitor implements BindingScopingVisitor { + + /** + * Default visit implementation. Returns {@code null}. + */ + protected V visitOther() { + return null; + } + + public V visitEagerSingleton() { + return visitOther(); + } + + public V visitScope(Scope scope) { + return visitOther(); + } + + public V visitScopeAnnotation(Class scopeAnnotation) { + return visitOther(); + } + + public V visitNoScoping() { + return visitOther(); + } +} diff --git a/src/main/java/com/google/inject/spi/DefaultBindingTargetVisitor.java b/src/main/java/com/google/inject/spi/DefaultBindingTargetVisitor.java new file mode 100644 index 0000000..6e351a9 --- /dev/null +++ b/src/main/java/com/google/inject/spi/DefaultBindingTargetVisitor.java @@ -0,0 +1,57 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; + +/** + * No-op visitor for subclassing. All interface methods simply delegate to {@link + * #visitOther(Binding)}, returning its result. + * + * @param any type to be returned by the visit method. Use {@link Void} with + * {@code return null} if no return type is needed. + */ +public abstract class DefaultBindingTargetVisitor implements BindingTargetVisitor { + + /** + * Default visit implementation. Returns {@code null}. + */ + protected V visitOther(Binding binding) { + return null; + } + + public V visit(InstanceBinding instanceBinding) { + return visitOther(instanceBinding); + } + + public V visit(ProviderInstanceBinding providerInstanceBinding) { + return visitOther(providerInstanceBinding); + } + + public V visit(ProviderKeyBinding providerKeyBinding) { + return visitOther(providerKeyBinding); + } + + public V visit(LinkedKeyBinding linkedKeyBinding) { + return visitOther(linkedKeyBinding); + } + + public V visit(ExposedBinding exposedBinding) { + return visitOther(exposedBinding); + } + + public V visit(UntargettedBinding untargettedBinding) { + return visitOther(untargettedBinding); + } + + public V visit(ConstructorBinding constructorBinding) { + return visitOther(constructorBinding); + } + + public V visit(ConvertedConstantBinding convertedConstantBinding) { + return visitOther(convertedConstantBinding); + } + + @SuppressWarnings("unchecked") + public V visit(ProviderBinding providerBinding) { + return visitOther((Binding) (Binding) providerBinding); + } +} diff --git a/src/main/java/com/google/inject/spi/DefaultElementVisitor.java b/src/main/java/com/google/inject/spi/DefaultElementVisitor.java new file mode 100644 index 0000000..6d0effb --- /dev/null +++ b/src/main/java/com/google/inject/spi/DefaultElementVisitor.java @@ -0,0 +1,84 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; + +/** + * No-op visitor for subclassing. All interface methods simply delegate to + * {@link #visitOther(Element)}, returning its result. + * + * @param any type to be returned by the visit method. Use {@link Void} with + * {@code return null} if no return type is needed. + */ +public abstract class DefaultElementVisitor implements ElementVisitor { + + /** + * Default visit implementation. Returns {@code null}. + */ + protected V visitOther(Element element) { + return null; + } + + public V visit(Message message) { + return visitOther(message); + } + + public V visit(Binding binding) { + return visitOther(binding); + } + + public V visit(ScopeBinding scopeBinding) { + return visitOther(scopeBinding); + } + + public V visit(TypeConverterBinding typeConverterBinding) { + return visitOther(typeConverterBinding); + } + + public V visit(ProviderLookup providerLookup) { + return visitOther(providerLookup); + } + + public V visit(InjectionRequest injectionRequest) { + return visitOther(injectionRequest); + } + + public V visit(StaticInjectionRequest staticInjectionRequest) { + return visitOther(staticInjectionRequest); + } + + public V visit(PrivateElements privateElements) { + return visitOther(privateElements); + } + + public V visit(MembersInjectorLookup lookup) { + return visitOther(lookup); + } + + public V visit(TypeListenerBinding binding) { + return visitOther(binding); + } + + public V visit(ProvisionListenerBinding binding) { + return visitOther(binding); + } + + public V visit(DisableCircularProxiesOption option) { + return visitOther(option); + } + + public V visit(RequireExplicitBindingsOption option) { + return visitOther(option); + } + + public V visit(RequireAtInjectOnConstructorsOption option) { + return visitOther(option); + } + + public V visit(RequireExactBindingAnnotationsOption option) { + return visitOther(option); + } + + public V visit(ModuleAnnotatedMethodScannerBinding binding) { + return visitOther(binding); + } +} diff --git a/src/main/java/com/google/inject/spi/Dependency.java b/src/main/java/com/google/inject/spi/Dependency.java new file mode 100644 index 0000000..d76e025 --- /dev/null +++ b/src/main/java/com/google/inject/spi/Dependency.java @@ -0,0 +1,112 @@ +package com.google.inject.spi; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.Key; +import com.google.inject.internal.MoreTypes; + +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A variable that can be resolved by an injector. + * + *

Use {@link #get} to build a freestanding dependency, or {@link InjectionPoint} to build one + * that's attached to a constructor, method or field. + */ +public final class Dependency { + private final InjectionPoint injectionPoint; + private final Key key; + private final boolean nullable; + private final int parameterIndex; + + Dependency(InjectionPoint injectionPoint, Key key, boolean nullable, int parameterIndex) { + this.injectionPoint = injectionPoint; + this.key = checkNotNull(key, "key"); + this.nullable = nullable; + this.parameterIndex = parameterIndex; + } + + /** + * Returns a new dependency that is not attached to an injection point. The returned dependency is + * nullable. + */ + public static Dependency get(Key key) { + return new Dependency(null, MoreTypes.canonicalizeKey(key), true, -1); + } + + /** + * Returns the dependencies from the given injection points. + */ + public static Set> forInjectionPoints(Set injectionPoints) { + List> dependencies = Lists.newArrayList(); + for (InjectionPoint injectionPoint : injectionPoints) { + dependencies.addAll(injectionPoint.getDependencies()); + } + return ImmutableSet.copyOf(dependencies); + } + + /** + * Returns the key to the binding that satisfies this dependency. + */ + public Key getKey() { + return this.key; + } + + /** + * Returns true if null is a legal value for this dependency. + */ + public boolean isNullable() { + return nullable; + } + + /** + * Returns the injection point to which this dependency belongs, or null if this dependency isn't + * attached to a particular injection point. + */ + public InjectionPoint getInjectionPoint() { + return injectionPoint; + } + + /** + * Returns the index of this dependency in the injection point's parameter list, or {@code -1} if + * this dependency does not belong to a parameter list. Only method and constuctor dependencies + * are elements in a parameter list. + */ + public int getParameterIndex() { + return parameterIndex; + } + + @Override + public int hashCode() { + return Objects.hashCode(injectionPoint, parameterIndex, key); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Dependency) { + Dependency dependency = (Dependency) o; + return Objects.equal(injectionPoint, dependency.injectionPoint) + && Objects.equal(parameterIndex, dependency.parameterIndex) + && Objects.equal(key, dependency.key); + } else { + return false; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(key); + if (injectionPoint != null) { + builder.append("@").append(injectionPoint); + if (parameterIndex != -1) { + builder.append("[").append(parameterIndex).append("]"); + } + } + return builder.toString(); + } +} diff --git a/src/main/java/com/google/inject/spi/DependencyAndSource.java b/src/main/java/com/google/inject/spi/DependencyAndSource.java new file mode 100644 index 0000000..eb17f91 --- /dev/null +++ b/src/main/java/com/google/inject/spi/DependencyAndSource.java @@ -0,0 +1,60 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.internal.util.StackTraceElements; + +import java.lang.reflect.Member; + +/** + * A combination of a {@link Dependency} and the {@link Binding#getSource() + * source} where the dependency was bound. + */ +public final class DependencyAndSource { + private final Dependency dependency; + private final Object source; + + public DependencyAndSource(Dependency dependency, Object source) { + this.dependency = dependency; + this.source = source; + } + + /** + * Returns the Dependency, if one exists. For anything that can be referenced + * by {@link Injector#getBinding}, a dependency exists. A dependency will not + * exist (and this will return null) for types initialized with + * {@link Binder#requestInjection} or {@link Injector#injectMembers(Object)}, + * nor will it exist for objects injected into Providers bound with + * LinkedBindingBuilder#toProvider(Provider). + */ + public Dependency getDependency() { + return dependency; + } + + /** + * Returns a string describing where this dependency was bound. If the binding + * was just-in-time, there is no valid binding source, so this describes the + * class in question. + */ + public String getBindingSource() { + if (source instanceof Class) { + return StackTraceElements.forType((Class) source).toString(); + } else if (source instanceof Member) { + return StackTraceElements.forMember((Member) source).toString(); + } else { + return source.toString(); + } + } + + @Override + public String toString() { + Dependency dep = getDependency(); + Object source = getBindingSource(); + if (dep != null) { + return "Dependency: " + dep + ", source: " + source; + } else { + return "Source: " + source; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/DisableCircularProxiesOption.java b/src/main/java/com/google/inject/spi/DisableCircularProxiesOption.java new file mode 100644 index 0000000..d407354 --- /dev/null +++ b/src/main/java/com/google/inject/spi/DisableCircularProxiesOption.java @@ -0,0 +1,28 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A request to disable circular proxies. + */ +public final class DisableCircularProxiesOption implements Element { + private final Object source; + + DisableCircularProxiesOption(Object source) { + this.source = checkNotNull(source, "source"); + } + + public Object getSource() { + return source; + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).disableCircularProxies(); + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/com/google/inject/spi/Element.java b/src/main/java/com/google/inject/spi/Element.java new file mode 100644 index 0000000..db4b706 --- /dev/null +++ b/src/main/java/com/google/inject/spi/Element.java @@ -0,0 +1,45 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; + +/** + * A core component of a module or injector. + * + *

The elements of a module can be inspected, validated and rewritten. Use {@link + * Elements#getElements(com.google.inject.Module[]) Elements.getElements()} to read the elements + * from a module, and {@link Elements#getModule(Iterable) Elements.getModule()} to rewrite them. + * This can be used for static analysis and generation of Guice modules. + * + *

The elements of an injector can be inspected and exercised. Use {@link + * com.google.inject.Injector#getBindings Injector.getBindings()} to reflect on Guice injectors. + * + */ +public interface Element { + + /** + * Returns an arbitrary object containing information about the "place" where this element was + * configured. Used by Guice in the production of descriptive error messages. + * + *

Tools might specially handle types they know about; {@code StackTraceElement} is a good + * example. Tools should simply call {@code toString()} on the source object if the type is + * unfamiliar. + */ + Object getSource(); + + /** + * Accepts an element visitor. Invokes the visitor method specific to this element's type. + * + * @param visitor to call back on + */ + T acceptVisitor(ElementVisitor visitor); + + /** + * Writes this module element to the given binder (optional operation). + * + * @param binder to apply configuration element to + * @throws UnsupportedOperationException if the {@code applyTo} method is not supported by this + * element. + */ + void applyTo(Binder binder); + +} diff --git a/src/main/java/com/google/inject/spi/ElementSource.java b/src/main/java/com/google/inject/spi/ElementSource.java new file mode 100644 index 0000000..91e1640 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ElementSource.java @@ -0,0 +1,168 @@ +package com.google.inject.spi; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.inject.internal.util.StackTraceElements; +import com.google.inject.internal.util.StackTraceElements.InMemoryStackTraceElement; + +import java.util.List; + +/** + * Contains information about where and how an {@link Element element} was + * bound. + *

+ * The {@link #getDeclaringSource() declaring source} refers to a location in + * source code that defines the Guice {@link Element element}. For example, if + * the element is created from a method annotated by {@literal @Provides}, the + * declaring source of element would be the method itself. + *

+ * The {@link #getStackTrace()} refers to the sequence of calls ends at one of + * {@link com.google.inject.Binder} {@code bindXXX()} methods and eventually + * defines the element. Note that {@link #getStackTrace()} lists + * {@link StackTraceElement StackTraceElements} in reverse chronological order. + * The first element (index zero) is the last method call and the last element + * is the first method invocation. By default, the stack trace is not collected. + * The default behavior can be changed by setting the + * {@code guice_include_stack_traces} flag value. The value can be either + * {@code OFF}, {@code ONLY_FOR_DECLARING_SOURCE} or {@code COMPLETE}. Note that + * collecting stack traces for every binding can cause a performance hit when + * the injector is created. + *

+ * The sequence of class names of {@link com.google.inject.Module modules} + * involved in the element creation can be retrieved by + * {@link #getModuleClassNames()}. Similar to {@link #getStackTrace()}, the + * order is reverse chronological. The first module (index 0) is the module that + * installs the {@link Element element}. The last module is the root module. + *

+ * In order to support the cases where a Guice {@link Element element} is + * created from another Guice {@link Element element} (original) (e.g., by + * {@link Element#applyTo}), it also provides a reference to the original + * element source ({@link #getOriginalElementSource()}). + * + */ +public final class ElementSource { + + /** + * The {@link ElementSource source} of element that this element created from (if there is any), + * otherwise {@code null}. + */ + final ElementSource originalElementSource; + + /** + * The {@link ModuleSource source} of module creates the element. + */ + final ModuleSource moduleSource; + + final InMemoryStackTraceElement[] partialCallStack; + + final Object declaringSource; + + /** + * Creates a new {@link ElementSource} from the given parameters. + * + * @param originalSource The source of element that this element created from (if there is + * any), otherwise {@code null}. + * @param declaringSource the source (in)directly declared the element. + * @param moduleSource the moduleSource when the element is bound + * @param partialCallStack the partial call stack from the top module to where the element is + * bound + */ + ElementSource(ElementSource originalSource, Object declaringSource, + ModuleSource moduleSource, StackTraceElement[] partialCallStack) { + Preconditions.checkNotNull(declaringSource, "declaringSource cannot be null."); + Preconditions.checkNotNull(moduleSource, "moduleSource cannot be null."); + Preconditions.checkNotNull(partialCallStack, "partialCallStack cannot be null."); + this.originalElementSource = originalSource; + this.declaringSource = declaringSource; + this.moduleSource = moduleSource; + this.partialCallStack = StackTraceElements.convertToInMemoryStackTraceElement(partialCallStack); + } + + /** + * Returns the {@link ElementSource} of the element this was created or copied from. If this was + * not created or copied from another element, returns {@code null}. + */ + public ElementSource getOriginalElementSource() { + return originalElementSource; + } + + /** + * Returns a single location in source code that defines the element. It can be any object + * such as {@link java.lang.reflect.Constructor}, {@link java.lang.reflect.Method}, + * {@link java.lang.reflect.Field}, {@link StackTraceElement}, etc. For + * example, if the element is created from a method annotated by {@literal @Provides}, the + * declaring source of element would be the method itself. + */ + public Object getDeclaringSource() { + return declaringSource; + } + + /** + * Returns the class names of modules involved in creating this {@link Element}. The first + * element (index 0) is the class name of module that defined the element, and the last element + * is the class name of root module. + */ + public List getModuleClassNames() { + return moduleSource.getModuleClassNames(); + } + + /** + * Returns the position of {@link com.google.inject.Module#configure configure(Binder)} method + * call in the {@link #getStackTrace stack trace} for modules that their classes returned by + * {@link #getModuleClassNames}. For example, if the stack trace looks like the following: + *

+ * {@code + * 0 - Binder.bind(), + * 1 - ModuleTwo.configure(), + * 2 - Binder.install(), + * 3 - ModuleOne.configure(), + * 4 - theRest(). + * } + *

+ * 1 and 3 are returned. + *

+ * In the cases where stack trace is not available (i.e., the stack trace was not collected), + * it returns -1 for all module positions. + */ + public List getModuleConfigurePositionsInStackTrace() { + int size = moduleSource.size(); + Integer[] positions = new Integer[size]; + int chunkSize = partialCallStack.length; + positions[0] = chunkSize - 1; + ModuleSource current = moduleSource; + for (int cursor = 1; cursor < size; cursor++) { + chunkSize = current.getPartialCallStackSize(); + positions[cursor] = positions[cursor - 1] + chunkSize; + current = current.getParent(); + } + return ImmutableList.copyOf(positions); + } + + /** + * Returns the sequence of method calls that ends at one of {@link com.google.inject.Binder} + * {@code bindXXX()} methods and eventually defines the element. Note that + * this method lists {@link StackTraceElement StackTraceElements} in reverse + * chronological order. The first element (index zero) is the last method call and the last + * element is the first method invocation. In the cases where stack trace is not available + * (i.e.,the stack trace was not collected), it returns an empty array. + */ + public StackTraceElement[] getStackTrace() { + int modulesCallStackSize = moduleSource.getStackTraceSize(); + int chunkSize = partialCallStack.length; + int size = moduleSource.getStackTraceSize() + chunkSize; + StackTraceElement[] callStack = new StackTraceElement[size]; + System.arraycopy( + StackTraceElements.convertToStackTraceElement(partialCallStack), 0, callStack, 0, + chunkSize); + System.arraycopy(moduleSource.getStackTrace(), 0, callStack, chunkSize, modulesCallStackSize); + return callStack; + } + + /** + * Returns {@code getDeclaringSource().toString()} value. + */ + @Override + public String toString() { + return getDeclaringSource().toString(); + } +} diff --git a/src/main/java/com/google/inject/spi/ElementVisitor.java b/src/main/java/com/google/inject/spi/ElementVisitor.java new file mode 100644 index 0000000..f12a8c9 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ElementVisitor.java @@ -0,0 +1,96 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Inject; + +/** + * Visit elements. + * + * @param any type to be returned by the visit method. Use {@link Void} with + * {@code return null} if no return type is needed. + */ +public interface ElementVisitor { + + /** + * Visit a mapping from a key (type and optional annotation) to the strategy for getting + * instances of the type. + */ + V visit(Binding binding); + + /** + * Visit a registration of a scope annotation with the scope that implements it. + */ + V visit(ScopeBinding binding); + + /** + * Visit a registration of type converters for matching target types. + */ + V visit(TypeConverterBinding binding); + + /** + * Visit a request to inject the instance fields and methods of an instance. + */ + V visit(InjectionRequest request); + + /** + * Visit a request to inject the static fields and methods of type. + */ + V visit(StaticInjectionRequest request); + + /** + * Visit a lookup of the provider for a type. + */ + V visit(ProviderLookup lookup); + + /** + * Visit a lookup of the members injector. + */ + V visit(MembersInjectorLookup lookup); + + /** + * Visit an error message and the context in which it occured. + */ + V visit(Message message); + + /** + * Visit a collection of configuration elements for a {@linkplain com.google.inject.PrivateBinder + * private binder}. + */ + V visit(PrivateElements elements); + + /** + * Visit an injectable type listener binding. + */ + V visit(TypeListenerBinding binding); + + /** + * Visit a provision listener binding. + */ + V visit(ProvisionListenerBinding binding); + + /** + * Visit a require explicit bindings command. + */ + V visit(RequireExplicitBindingsOption option); + + /** + * Visit a disable circular proxies command. + */ + V visit(DisableCircularProxiesOption option); + + /** + * Visit a require explicit {@literal @}{@link Inject} command. + */ + V visit(RequireAtInjectOnConstructorsOption option); + + /** + * Visit a require exact binding annotations command. + */ + V visit(RequireExactBindingAnnotationsOption option); + + /** + * Visits a {@link Binder#scanModulesForAnnotatedMethods} command. + */ + V visit(ModuleAnnotatedMethodScannerBinding binding); +} diff --git a/src/main/java/com/google/inject/spi/Elements.java b/src/main/java/com/google/inject/spi/Elements.java new file mode 100644 index 0000000..c1c26af --- /dev/null +++ b/src/main/java/com/google/inject/spi/Elements.java @@ -0,0 +1,547 @@ +package com.google.inject.spi; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Module; +import com.google.inject.PrivateBinder; +import com.google.inject.PrivateModule; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.AnnotatedConstantBindingBuilder; +import com.google.inject.binder.AnnotatedElementBuilder; +import com.google.inject.internal.AbstractBindingBuilder; +import com.google.inject.internal.BindingBuilder; +import com.google.inject.internal.ConstantBindingBuilderImpl; +import com.google.inject.internal.Errors; +import com.google.inject.internal.ExposureBuilder; +import com.google.inject.internal.InternalFlags.IncludeStackTraceOption; +import com.google.inject.internal.MoreTypes; +import com.google.inject.internal.PrivateElementsImpl; +import com.google.inject.internal.ProviderMethodsModule; +import com.google.inject.internal.util.SourceProvider; +import com.google.inject.internal.util.StackTraceElements; +import com.google.inject.matcher.Matcher; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.inject.internal.InternalFlags.getIncludeStackTraceOption; + +/** + * Exposes elements of a module so they can be inspected, validated or {@link + * Element#applyTo(Binder) rewritten}. + * + */ +public final class Elements { + + private static final BindingTargetVisitor GET_INSTANCE_VISITOR + = new DefaultBindingTargetVisitor() { + @Override + public Object visit(InstanceBinding binding) { + return binding.getInstance(); + } + + @Override + protected Object visitOther(Binding binding) { + throw new IllegalArgumentException(); + } + }; + + /** + * Records the elements executed by {@code modules}. + */ + public static List getElements(Module... modules) { + return getElements(Stage.DEVELOPMENT, Arrays.asList(modules)); + } + + /** + * Records the elements executed by {@code modules}. + */ + public static List getElements(Stage stage, Module... modules) { + return getElements(stage, Arrays.asList(modules)); + } + + /** + * Records the elements executed by {@code modules}. + */ + public static List getElements(Iterable modules) { + return getElements(Stage.DEVELOPMENT, modules); + } + + /** + * Records the elements executed by {@code modules}. + */ + public static List getElements(Stage stage, Iterable modules) { + RecordingBinder binder = new RecordingBinder(stage); + for (Module module : modules) { + binder.install(module); + } + binder.scanForAnnotatedMethods(); + for (RecordingBinder child : binder.privateBinders) { + child.scanForAnnotatedMethods(); + } + // Free the memory consumed by the stack trace elements cache + StackTraceElements.clearCache(); + return Collections.unmodifiableList(binder.elements); + } + + /** + * Returns the module composed of {@code elements}. + */ + public static Module getModule(final Iterable elements) { + return new ElementsAsModule(elements); + } + + @SuppressWarnings("unchecked") + static BindingTargetVisitor getInstanceVisitor() { + return (BindingTargetVisitor) GET_INSTANCE_VISITOR; + } + + private static class ElementsAsModule implements Module { + private final Iterable elements; + + ElementsAsModule(Iterable elements) { + this.elements = elements; + } + + @Override + public void configure(Binder binder) { + for (Element element : elements) { + element.applyTo(binder); + } + } + } + + private static class ModuleInfo { + private final Binder binder; + private final ModuleSource moduleSource; + private final boolean skipScanning; + + private ModuleInfo(Binder binder, ModuleSource moduleSource, boolean skipScanning) { + this.binder = binder; + this.moduleSource = moduleSource; + this.skipScanning = skipScanning; + } + } + + private static class RecordingBinder implements Binder, PrivateBinder { + private final Stage stage; + private final Map modules; + private final List elements; + private final Object source; + private final SourceProvider sourceProvider; + private final Set scanners; + /** + * The binder where exposed bindings will be created + */ + private final RecordingBinder parent; + private final PrivateElementsImpl privateElements; + /** + * All children private binders, so we can scan through them. + */ + private final List privateBinders; + /** + * The current modules stack + */ + private ModuleSource moduleSource = null; + + private RecordingBinder(Stage stage) { + this.stage = stage; + this.modules = Maps.newLinkedHashMap(); + this.scanners = Sets.newLinkedHashSet(); + this.elements = Lists.newArrayList(); + this.source = null; + this.sourceProvider = SourceProvider.DEFAULT_INSTANCE.plusSkippedClasses( + Elements.class, RecordingBinder.class, AbstractModule.class, + ConstantBindingBuilderImpl.class, AbstractBindingBuilder.class, BindingBuilder.class); + this.parent = null; + this.privateElements = null; + this.privateBinders = Lists.newArrayList(); + } + + /** + * Creates a recording binder that's backed by {@code prototype}. + */ + private RecordingBinder( + RecordingBinder prototype, Object source, SourceProvider sourceProvider) { + checkArgument(source == null ^ sourceProvider == null); + + this.stage = prototype.stage; + this.modules = prototype.modules; + this.elements = prototype.elements; + this.scanners = prototype.scanners; + this.source = source; + this.moduleSource = prototype.moduleSource; + this.sourceProvider = sourceProvider; + this.parent = prototype.parent; + this.privateElements = prototype.privateElements; + this.privateBinders = prototype.privateBinders; + } + + /** + * Creates a private recording binder. + */ + private RecordingBinder(RecordingBinder parent, PrivateElementsImpl privateElements) { + this.stage = parent.stage; + this.modules = Maps.newLinkedHashMap(); + this.scanners = Sets.newLinkedHashSet(parent.scanners); + this.elements = privateElements.getElementsMutable(); + this.source = parent.source; + this.moduleSource = parent.moduleSource; + this.sourceProvider = parent.sourceProvider; + this.parent = parent; + this.privateElements = privateElements; + this.privateBinders = parent.privateBinders; + } + + @Override + public void bindScope(Class annotationType, Scope scope) { + elements.add(new ScopeBinding(getElementSource(), annotationType, scope)); + } + + @Override + @SuppressWarnings("unchecked") // it is safe to use the type literal for the raw type + public void requestInjection(Object instance) { + requestInjection((TypeLiteral) TypeLiteral.get(instance.getClass()), instance); + } + + @Override + public void requestInjection(TypeLiteral type, T instance) { + elements.add(new InjectionRequest(getElementSource(), MoreTypes.canonicalizeForKey(type), + instance)); + } + + @Override + public MembersInjector getMembersInjector(final TypeLiteral typeLiteral) { + final MembersInjectorLookup element = new MembersInjectorLookup(getElementSource(), + MoreTypes.canonicalizeForKey(typeLiteral)); + elements.add(element); + return element.getMembersInjector(); + } + + public MembersInjector getMembersInjector(Class type) { + return getMembersInjector(TypeLiteral.get(type)); + } + + public void bindListener(Matcher> typeMatcher, TypeListener listener) { + elements.add(new TypeListenerBinding(getElementSource(), listener, typeMatcher)); + } + + public void bindListener(Matcher> bindingMatcher, + ProvisionListener... listeners) { + elements.add(new ProvisionListenerBinding(getElementSource(), bindingMatcher, listeners)); + } + + public void requestStaticInjection(Class... types) { + for (Class type : types) { + elements.add(new StaticInjectionRequest(getElementSource(), type)); + } + } + + /** + * Applies all scanners to the modules we've installed. We skip certain + * PrivateModules because store them in more than one Modules map and only + * want to process them through one of the maps. (They're stored in both + * maps to prevent a module from being installed more than once.) + */ + void scanForAnnotatedMethods() { + for (ModuleAnnotatedMethodScanner scanner : scanners) { + // Note: we must iterate over a copy of the modules because calling install(..) + // will mutate modules, otherwise causing a ConcurrentModificationException. + for (Map.Entry entry : Maps.newLinkedHashMap(modules).entrySet()) { + Module module = entry.getKey(); + ModuleInfo info = entry.getValue(); + if (info.skipScanning) { + continue; + } + moduleSource = entry.getValue().moduleSource; + try { + info.binder.install(ProviderMethodsModule.forModule(module, scanner)); + } catch (RuntimeException e) { + Collection messages = Errors.getMessagesFromThrowable(e); + if (!messages.isEmpty()) { + elements.addAll(messages); + } else { + addError(e); + } + } + } + } + moduleSource = null; + } + + public void install(Module module) { + if (!modules.containsKey(module)) { + RecordingBinder binder = this; + boolean unwrapModuleSource = false; + // Update the module source for the new module + if (module instanceof ProviderMethodsModule) { + // There are two reason's we'd want to get the module source in a ProviderMethodsModule. + // ModuleAnnotatedMethodScanner lets users scan their own modules for @Provides-like + // bindings. If they install the module at a top-level, then moduleSource can be null. + // Also, if they pass something other than 'this' to it, we'd have the wrong source. + Object delegate = ((ProviderMethodsModule) module).getDelegateModule(); + if (moduleSource == null + || !moduleSource.getModuleClassName().equals(delegate.getClass().getName())) { + moduleSource = getModuleSource(delegate); + unwrapModuleSource = true; + } + } else { + moduleSource = getModuleSource(module); + unwrapModuleSource = true; + } + boolean skipScanning = false; + if (module instanceof PrivateModule) { + binder = (RecordingBinder) binder.newPrivateBinder(); + // Store the module in the private binder too so we scan for it. + binder.modules.put(module, new ModuleInfo(binder, moduleSource, false)); + skipScanning = true; // don't scan this module in the parent's module set. + } + // Always store this in the parent binder (even if it was a private module) + // so that we know not to process it again, and so that scanners inherit down. + modules.put(module, new ModuleInfo(binder, moduleSource, skipScanning)); + try { + module.configure(binder); + } catch (RuntimeException e) { + Collection messages = Errors.getMessagesFromThrowable(e); + if (!messages.isEmpty()) { + elements.addAll(messages); + } else { + addError(e); + } + } + binder.install(ProviderMethodsModule.forModule(module)); + // We are done with this module, so undo module source change + if (unwrapModuleSource) { + moduleSource = moduleSource.getParent(); + } + } + } + + public Stage currentStage() { + return stage; + } + + public void addError(String message, Object... arguments) { + elements.add(new Message(getElementSource(), Errors.format(message, arguments))); + } + + public void addError(Throwable t) { + String message = "An exception was caught and reported. Message: " + t.getMessage(); + elements.add(new Message(ImmutableList.of((Object) getElementSource()), message, t)); + } + + public void addError(Message message) { + elements.add(message); + } + + public AnnotatedBindingBuilder bind(Key key) { + return new BindingBuilder(this, elements, getElementSource(), MoreTypes.canonicalizeKey(key)); + } + + public AnnotatedBindingBuilder bind(TypeLiteral typeLiteral) { + return bind(Key.get(typeLiteral)); + } + + public AnnotatedBindingBuilder bind(Class type) { + return bind(Key.get(type)); + } + + public AnnotatedConstantBindingBuilder bindConstant() { + return new ConstantBindingBuilderImpl(this, elements, getElementSource()); + } + + public Provider getProvider(final Key key) { + return getProvider(Dependency.get(key)); + } + + public Provider getProvider(final Dependency dependency) { + final ProviderLookup element = new ProviderLookup(getElementSource(), dependency); + elements.add(element); + return element.getProvider(); + } + + public Provider getProvider(Class type) { + return getProvider(Key.get(type)); + } + + public void convertToTypes(Matcher> typeMatcher, + TypeConverter converter) { + elements.add(new TypeConverterBinding(getElementSource(), typeMatcher, converter)); + } + + public RecordingBinder withSource(final Object source) { + return source == this.source ? this : new RecordingBinder(this, source, null); + } + + public RecordingBinder skipSources(Class... classesToSkip) { + // if a source is specified explicitly, we don't need to skip sources + if (source != null) { + return this; + } + + SourceProvider newSourceProvider = sourceProvider.plusSkippedClasses(classesToSkip); + return new RecordingBinder(this, null, newSourceProvider); + } + + @Override + public PrivateBinder newPrivateBinder() { + PrivateElementsImpl privateElements = new PrivateElementsImpl(getElementSource()); + RecordingBinder binder = new RecordingBinder(this, privateElements); + privateBinders.add(binder); + elements.add(privateElements); + return binder; + } + + @Override + public void disableCircularProxies() { + elements.add(new DisableCircularProxiesOption(getElementSource())); + } + + @Override + public void requireExplicitBindings() { + elements.add(new RequireExplicitBindingsOption(getElementSource())); + } + + @Override + public void requireAtInjectOnConstructors() { + elements.add(new RequireAtInjectOnConstructorsOption(getElementSource())); + } + + @Override + public void requireExactBindingAnnotations() { + elements.add(new RequireExactBindingAnnotationsOption(getElementSource())); + } + + @Override + public void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner) { + scanners.add(scanner); + elements.add(new ModuleAnnotatedMethodScannerBinding(getElementSource(), scanner)); + } + + public void expose(Key key) { + exposeInternal(key); + } + + @Override + public AnnotatedElementBuilder expose(Class type) { + return exposeInternal(Key.get(type)); + } + + @Override + public AnnotatedElementBuilder expose(TypeLiteral type) { + return exposeInternal(Key.get(type)); + } + + private AnnotatedElementBuilder exposeInternal(Key key) { + if (privateElements == null) { + addError("Cannot expose %s on a standard binder. " + + "Exposed bindings are only applicable to private binders.", key); + return new AnnotatedElementBuilder() { + @Override + public void annotatedWith(Class annotationType) { + } + + @Override + public void annotatedWith(Annotation annotation) { + } + }; + } + + ExposureBuilder builder = + new ExposureBuilder(this, getElementSource(), MoreTypes.canonicalizeKey(key)); + privateElements.addExposureBuilder(builder); + return builder; + } + + private ModuleSource getModuleSource(Object module) { + StackTraceElement[] partialCallStack; + if (getIncludeStackTraceOption() == IncludeStackTraceOption.COMPLETE) { + partialCallStack = getPartialCallStack(new Throwable().getStackTrace()); + } else { + partialCallStack = new StackTraceElement[0]; + } + if (moduleSource == null) { + return new ModuleSource(module, partialCallStack); + } + return moduleSource.createChild(module, partialCallStack); + } + + private ElementSource getElementSource() { + // Full call stack + StackTraceElement[] callStack = null; + // The call stack starts from current top module configure and ends at this method caller + StackTraceElement[] partialCallStack = new StackTraceElement[0]; + // The element original source + ElementSource originalSource = null; + // The element declaring source + Object declaringSource = source; + if (declaringSource instanceof ElementSource) { + originalSource = (ElementSource) declaringSource; + declaringSource = originalSource.getDeclaringSource(); + } + IncludeStackTraceOption stackTraceOption = getIncludeStackTraceOption(); + if (stackTraceOption == IncludeStackTraceOption.COMPLETE || + (stackTraceOption == IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE + && declaringSource == null)) { + callStack = new Throwable().getStackTrace(); + } + if (stackTraceOption == IncludeStackTraceOption.COMPLETE) { + partialCallStack = getPartialCallStack(callStack); + } + if (declaringSource == null) { + // So 'source' and 'originalSource' are null otherwise declaringSource has some value + if (stackTraceOption == IncludeStackTraceOption.COMPLETE || + stackTraceOption == IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE) { + // With the above conditions and assignments 'callStack' is non-null + declaringSource = sourceProvider.get(callStack); + } else { // or if (stackTraceOption == IncludeStackTraceOptions.OFF) + // As neither 'declaring source' nor 'call stack' is available use 'module source' + declaringSource = sourceProvider.getFromClassNames(moduleSource.getModuleClassNames()); + } + } + // Build the binding call stack + return new ElementSource( + originalSource, declaringSource, moduleSource, partialCallStack); + } + + /** + * Removes the {@link #moduleSource} call stack from the beginning of current call stack. It + * also removes the last two elements in order to make {@link #install(Module)} the last call + * in the call stack. + */ + private StackTraceElement[] getPartialCallStack(StackTraceElement[] callStack) { + int toSkip = 0; + if (moduleSource != null) { + toSkip = moduleSource.getStackTraceSize(); + } + // -1 for skipping 'getModuleSource' and 'getElementSource' calls + int chunkSize = callStack.length - toSkip - 1; + + StackTraceElement[] partialCallStack = new StackTraceElement[chunkSize]; + System.arraycopy(callStack, 1, partialCallStack, 0, chunkSize); + return partialCallStack; + } + + @Override + public String toString() { + return "Binder"; + } + } +} diff --git a/src/main/java/com/google/inject/spi/ExposedBinding.java b/src/main/java/com/google/inject/spi/ExposedBinding.java new file mode 100644 index 0000000..5627b03 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ExposedBinding.java @@ -0,0 +1,21 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.Binding; + +/** + * A binding to a key exposed from an enclosed private environment. + * + */ +public interface ExposedBinding extends Binding, HasDependencies { + + /** + * Returns the enclosed environment that holds the original binding. + */ + PrivateElements getPrivateElements(); + + /** + * Unsupported. Always throws {@link UnsupportedOperationException}. + */ + void applyTo(Binder binder); +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/HasDependencies.java b/src/main/java/com/google/inject/spi/HasDependencies.java new file mode 100644 index 0000000..9ddc16a --- /dev/null +++ b/src/main/java/com/google/inject/spi/HasDependencies.java @@ -0,0 +1,19 @@ +package com.google.inject.spi; + +import java.util.Set; + +/** + * Implemented by {@link com.google.inject.Binding bindings}, {@link com.google.inject.Provider + * providers} and instances that expose their dependencies explicitly. + */ +public interface HasDependencies { + + /** + * Returns the known dependencies for this type. If this has dependencies whose values are not + * known statically, a dependency for the {@link com.google.inject.Injector Injector} will be + * included in the returned set. + * + * @return a possibly empty set + */ + Set> getDependencies(); +} diff --git a/src/main/java/com/google/inject/spi/InjectionListener.java b/src/main/java/com/google/inject/spi/InjectionListener.java new file mode 100644 index 0000000..0b6d608 --- /dev/null +++ b/src/main/java/com/google/inject/spi/InjectionListener.java @@ -0,0 +1,15 @@ +package com.google.inject.spi; + +/** + * Listens for injections into instances of type {@code I}. Useful for performing further + * injections, post-injection initialization, and more. + */ +public interface InjectionListener { + + /** + * Invoked by Guice after it injects the fields and methods of instance. + * + * @param injectee instance that Guice injected dependencies into + */ + void afterInjection(I injectee); +} diff --git a/src/main/java/com/google/inject/spi/InjectionPoint.java b/src/main/java/com/google/inject/spi/InjectionPoint.java new file mode 100644 index 0000000..e3cd5dc --- /dev/null +++ b/src/main/java/com/google/inject/spi/InjectionPoint.java @@ -0,0 +1,856 @@ +package com.google.inject.spi; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.Annotations; +import com.google.inject.internal.Errors; +import com.google.inject.internal.ErrorsException; +import com.google.inject.internal.Nullability; +import com.google.inject.internal.util.Classes; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.inject.internal.MoreTypes.getRawType; + +/** + * A constructor, field or method that can receive injections. Typically this is a member with the + * {@literal @}{@link Inject} annotation. For non-private, no argument constructors, the member may + * omit the annotation. + */ +public final class InjectionPoint { + + private final boolean optional; + private final Member member; + private final TypeLiteral declaringType; + private final ImmutableList> dependencies; + + InjectionPoint(TypeLiteral declaringType, Method method, boolean optional) { + this.member = method; + this.declaringType = declaringType; + this.optional = optional; + this.dependencies = forMember(method, declaringType, method.getParameterAnnotations()); + } + + InjectionPoint(TypeLiteral declaringType, Constructor constructor) { + this.member = constructor; + this.declaringType = declaringType; + this.optional = false; + this.dependencies = forMember( + constructor, declaringType, constructor.getParameterAnnotations()); + } + + InjectionPoint(TypeLiteral declaringType, Field field, boolean optional) { + this.member = field; + this.declaringType = declaringType; + this.optional = optional; + + Annotation[] annotations = field.getAnnotations(); + + Errors errors = new Errors(field); + Key key = null; + try { + key = Annotations.getKey(declaringType.getFieldType(field), field, annotations, errors); + } catch (ConfigurationException e) { + errors.merge(e.getErrorMessages()); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + errors.throwConfigurationExceptionIfErrorsExist(); + + this.dependencies = ImmutableList.>of( + newDependency(key, Nullability.allowsNull(annotations), -1)); + } + + /** + * Returns a new injection point for the specified constructor. If the declaring type of {@code + * constructor} is parameterized (such as {@code List}), prefer the overload that includes a + * type literal. + * + * @param constructor any single constructor present on {@code type}. + */ + public static InjectionPoint forConstructor(Constructor constructor) { + return new InjectionPoint(TypeLiteral.get(constructor.getDeclaringClass()), constructor); + } + + /** + * Returns a new injection point for the specified constructor of {@code type}. + * + * @param constructor any single constructor present on {@code type}. + * @param type the concrete type that defines {@code constructor}. + */ + public static InjectionPoint forConstructor( + Constructor constructor, TypeLiteral type) { + if (type.getRawType() != constructor.getDeclaringClass()) { + new Errors(type) + .constructorNotDefinedByType(constructor, type) + .throwConfigurationExceptionIfErrorsExist(); + } + + return new InjectionPoint(type, constructor); + } + + /** + * Returns a new injection point for the injectable constructor of {@code type}. + * + * @param type a concrete type with exactly one constructor annotated {@literal @}{@link Inject}, + * or a no-arguments constructor that is not private. + * @throws ConfigurationException if there is no injectable constructor, more than one injectable + * constructor, or if parameters of the injectable constructor are malformed, such as + * a + * parameter with multiple binding annotations. + */ + public static InjectionPoint forConstructorOf(TypeLiteral type) { + Class rawType = getRawType(type.getType()); + Errors errors = new Errors(rawType); + + Constructor injectableConstructor = null; + for (Constructor constructor : rawType.getDeclaredConstructors()) { + + boolean optional; + Inject guiceInject = constructor.getAnnotation(Inject.class); + if (guiceInject == null) { + javax.inject.Inject javaxInject = constructor.getAnnotation(javax.inject.Inject.class); + if (javaxInject == null) { + continue; + } + optional = false; + } else { + optional = guiceInject.optional(); + } + + if (optional) { + errors.optionalConstructor(constructor); + } + + if (injectableConstructor != null) { + errors.tooManyConstructors(rawType); + } + + injectableConstructor = constructor; + checkForMisplacedBindingAnnotations(injectableConstructor, errors); + } + + errors.throwConfigurationExceptionIfErrorsExist(); + + if (injectableConstructor != null) { + return new InjectionPoint(type, injectableConstructor); + } + + // If no annotated constructor is found, look for a no-arg constructor instead. + try { + Constructor noArgConstructor = rawType.getDeclaredConstructor(); + + // Disallow private constructors on non-private classes (unless they have @Inject) + if (Modifier.isPrivate(noArgConstructor.getModifiers()) + && !Modifier.isPrivate(rawType.getModifiers())) { + errors.missingConstructor(rawType); + throw new ConfigurationException(errors.getMessages()); + } + + checkForMisplacedBindingAnnotations(noArgConstructor, errors); + return new InjectionPoint(type, noArgConstructor); + } catch (NoSuchMethodException e) { + errors.missingConstructor(rawType); + throw new ConfigurationException(errors.getMessages()); + } + } + + /** + * Returns a new injection point for the injectable constructor of {@code type}. + * + * @param type a concrete type with exactly one constructor annotated {@literal @}{@link Inject}, + * or a no-arguments constructor that is not private. + * @throws ConfigurationException if there is no injectable constructor, more than one injectable + * constructor, or if parameters of the injectable constructor are malformed, such as + * a + * parameter with multiple binding annotations. + */ + public static InjectionPoint forConstructorOf(Class type) { + return forConstructorOf(TypeLiteral.get(type)); + } + + /** + * Returns a new injection point for the specified method of {@code type}. + * This is useful for extensions that need to build dependency graphs from + * arbitrary methods. + * + * @param method any single method present on {@code type}. + * @param type the concrete type that defines {@code method}. + */ + public static InjectionPoint forMethod(Method method, TypeLiteral type) { + return new InjectionPoint(type, method, false); + } + + /** + * Returns all static method and field injection points on {@code type}. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + * @throws ConfigurationException if there is a malformed injection point on {@code type}, such as + * a field with multiple binding annotations. The exception's {@link + * ConfigurationException#getPartialValue() partial value} is a {@code + * Set} + * of the valid injection points. + */ + public static Set forStaticMethodsAndFields(TypeLiteral type) { + Errors errors = new Errors(); + + Set result; + + if (type.getRawType().isInterface()) { + errors.staticInjectionOnInterface(type.getRawType()); + result = null; + } else { + result = getInjectionPoints(type, true, errors); + } + + if (errors.hasErrors()) { + throw new ConfigurationException(errors.getMessages()).withPartialValue(result); + } + return result; + } + + /** + * Returns all static method and field injection points on {@code type}. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + * @throws ConfigurationException if there is a malformed injection point on {@code type}, such as + * a field with multiple binding annotations. The exception's {@link + * ConfigurationException#getPartialValue() partial value} is a {@code + * Set} + * of the valid injection points. + */ + public static Set forStaticMethodsAndFields(Class type) { + return forStaticMethodsAndFields(TypeLiteral.get(type)); + } + + /** + * Returns all instance method and field injection points on {@code type}. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + * @throws ConfigurationException if there is a malformed injection point on {@code type}, such as + * a field with multiple binding annotations. The exception's {@link + * ConfigurationException#getPartialValue() partial value} is a {@code + * Set} + * of the valid injection points. + */ + public static Set forInstanceMethodsAndFields(TypeLiteral type) { + Errors errors = new Errors(); + Set result = getInjectionPoints(type, false, errors); + if (errors.hasErrors()) { + throw new ConfigurationException(errors.getMessages()).withPartialValue(result); + } + return result; + } + + /** + * Returns all instance method and field injection points on {@code type}. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + * @throws ConfigurationException if there is a malformed injection point on {@code type}, such as + * a field with multiple binding annotations. The exception's {@link + * ConfigurationException#getPartialValue() partial value} is a {@code + * Set} + * of the valid injection points. + */ + public static Set forInstanceMethodsAndFields(Class type) { + return forInstanceMethodsAndFields(TypeLiteral.get(type)); + } + + /** + * Returns true if the binding annotation is in the wrong place. + */ + private static boolean checkForMisplacedBindingAnnotations(Member member, Errors errors) { + Annotation misplacedBindingAnnotation = Annotations.findBindingAnnotation( + errors, member, ((AnnotatedElement) member).getAnnotations()); + if (misplacedBindingAnnotation == null) { + return false; + } + + // don't warn about misplaced binding annotations on methods when there's a field with the same + // name. In Scala, fields always get accessor methods (that we need to ignore). See bug 242. + if (member instanceof Method) { + try { + if (member.getDeclaringClass().getDeclaredField(member.getName()) != null) { + return false; + } + } catch (NoSuchFieldException ignore) { + } + } + + errors.misplacedBindingAnnotation(member, misplacedBindingAnnotation); + return true; + } + + static Annotation getAtInject(AnnotatedElement member) { + Annotation a = member.getAnnotation(javax.inject.Inject.class); + return a == null ? member.getAnnotation(Inject.class) : a; + } + + /** + * Returns an ordered, immutable set of injection points for the given type. Members in + * superclasses come before members in subclasses. Within a class, fields come before methods. + * Overridden methods are filtered out. + * + * @param statics true is this method should return static members, false for instance members + * @param errors used to record errors + */ + private static Set getInjectionPoints(final TypeLiteral type, + boolean statics, Errors errors) { + InjectableMembers injectableMembers = new InjectableMembers(); + OverrideIndex overrideIndex = null; + + List> hierarchy = hierarchyFor(type); + int topIndex = hierarchy.size() - 1; + for (int i = topIndex; i >= 0; i--) { + if (overrideIndex != null && i < topIndex) { + // Knowing the position within the hierarchy helps us make optimizations. + if (i == 0) { + overrideIndex.position = Position.BOTTOM; + } else { + overrideIndex.position = Position.MIDDLE; + } + } + + TypeLiteral current = hierarchy.get(i); + + for (Field field : current.getRawType().getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) == statics) { + Annotation atInject = getAtInject(field); + if (atInject != null) { + InjectableField injectableField = new InjectableField(current, field, atInject); + if (injectableField.jsr330 && Modifier.isFinal(field.getModifiers())) { + errors.cannotInjectFinalField(field); + } + injectableMembers.add(injectableField); + } + } + } + + for (Method method : current.getRawType().getDeclaredMethods()) { + if (isEligibleForInjection(method, statics)) { + Annotation atInject = getAtInject(method); + if (atInject != null) { + InjectableMethod injectableMethod = new InjectableMethod( + current, method, atInject); + if (checkForMisplacedBindingAnnotations(method, errors) + || !isValidMethod(injectableMethod, errors)) { + if (overrideIndex != null) { + boolean removed = overrideIndex.removeIfOverriddenBy(method, false, injectableMethod); + if (removed) { + /*logger.log(Level.WARNING, "Method: {0} is not a valid injectable method (" + + "because it either has misplaced binding annotations " + + "or specifies type parameters) but is overriding a method that is valid. " + + "Because it is not valid, the method will not be injected. " + + "To fix this, make the method a valid injectable method.", method);*/ + } + } + continue; + } + if (statics) { + injectableMembers.add(injectableMethod); + } else { + if (overrideIndex == null) { + /* + * Creating the override index lazily means that the first type in the hierarchy + * with injectable methods (not necessarily the top most type) will be treated as + * the TOP position and will enjoy the same optimizations (no checks for overridden + * methods, etc.). + */ + overrideIndex = new OverrideIndex(injectableMembers); + } else { + // Forcibly remove the overriden method, otherwise we'll inject + // it twice. + overrideIndex.removeIfOverriddenBy(method, true, injectableMethod); + } + overrideIndex.add(injectableMethod); + } + } else { + if (overrideIndex != null) { + boolean removed = overrideIndex.removeIfOverriddenBy(method, false, null); + if (removed) { + /*logger.log(Level.WARNING, "Method: {0} is not annotated with @Inject but " + + "is overriding a method that is annotated with @javax.inject.Inject. Because " + + "it is not annotated with @Inject, the method will not be injected. " + + "To fix this, annotate the method with @Inject.", method);*/ + } + } + } + } + } + } + + if (injectableMembers.isEmpty()) { + return Collections.emptySet(); + } + + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (InjectableMember im = injectableMembers.head; im != null; + im = im.next) { + try { + builder.add(im.toInjectionPoint()); + } catch (ConfigurationException ignorable) { + if (!im.optional) { + errors.merge(ignorable.getErrorMessages()); + } + } + } + return builder.build(); + } + + /** + * Returns true if the method is eligible to be injected. This is different than + * {@link #isValidMethod}, because ineligibility will not drop a method + * from being injected if a superclass was eligible & valid. + * Bridge & synthetic methods are excluded from eligibility for two reasons: + * + *

Prior to Java8, javac would generate these methods in subclasses without + * annotations, which means this would accidentally stop injecting a method + * annotated with {@link javax.inject.Inject}, since the spec says to stop + * injecting if a subclass isn't annotated with it. + * + *

Starting at Java8, javac copies the annotations to the generated subclass + * method, except it leaves out the generic types. If this considered it a valid + * injectable method, this would eject the parent's overridden method that had the + * proper generic types, and would use invalid injectable parameters as a result. + * + *

The fix for both is simply to ignore these synthetic bridge methods. + */ + private static boolean isEligibleForInjection(Method method, boolean statics) { + return Modifier.isStatic(method.getModifiers()) == statics + && !method.isBridge() + && !method.isSynthetic(); + } + + private static boolean isValidMethod(InjectableMethod injectableMethod, + Errors errors) { + boolean result = true; + if (injectableMethod.jsr330) { + Method method = injectableMethod.method; + if (Modifier.isAbstract(method.getModifiers())) { + errors.cannotInjectAbstractMethod(method); + result = false; + } + if (method.getTypeParameters().length > 0) { + errors.cannotInjectMethodWithTypeParameters(method); + result = false; + } + } + return result; + } + + private static List> hierarchyFor(TypeLiteral type) { + List> hierarchy = new ArrayList>(); + TypeLiteral current = type; + while (current.getRawType() != Object.class) { + hierarchy.add(current); + current = current.getSupertype(current.getRawType().getSuperclass()); + } + return hierarchy; + } + + /** + * Returns true if a overrides b. Assumes signatures of a and b are the same and a's declaring + * class is a subclass of b's declaring class. + */ + private static boolean overrides(Method a, Method b) { + // See JLS section 8.4.8.1 + int modifiers = b.getModifiers(); + if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) { + return true; + } + if (Modifier.isPrivate(modifiers)) { + return false; + } + // b must be package-private + return a.getDeclaringClass().getPackage().equals(b.getDeclaringClass().getPackage()); + } + + private ImmutableList> forMember(Member member, TypeLiteral type, + Annotation[][] paramterAnnotations) { + Errors errors = new Errors(member); + Iterator annotationsIterator = Arrays.asList(paramterAnnotations).iterator(); + + List> dependencies = Lists.newArrayList(); + int index = 0; + + for (TypeLiteral parameterType : type.getParameterTypes(member)) { + try { + Annotation[] parameterAnnotations = annotationsIterator.next(); + Key key = Annotations.getKey(parameterType, member, parameterAnnotations, errors); + dependencies.add(newDependency(key, Nullability.allowsNull(parameterAnnotations), index)); + index++; + } catch (ConfigurationException e) { + errors.merge(e.getErrorMessages()); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + + errors.throwConfigurationExceptionIfErrorsExist(); + return ImmutableList.copyOf(dependencies); + } + + // This metohd is necessary to create a Dependency with proper generic type information + private Dependency newDependency(Key key, boolean allowsNull, int parameterIndex) { + return new Dependency(this, key, allowsNull, parameterIndex); + } + + /** + * Returns the injected constructor, field, or method. + */ + public Member getMember() { + // TODO: Don't expose the original member (which probably has setAccessible(true)). + return member; + } + + /** + * Returns the dependencies for this injection point. If the injection point is for a method or + * constructor, the dependencies will correspond to that member's parameters. Field injection + * points always have a single dependency for the field itself. + * + * @return a possibly-empty list + */ + public List> getDependencies() { + return dependencies; + } + + /** + * Returns true if this injection point shall be skipped if the injector cannot resolve bindings + * for all required dependencies. Both explicit bindings (as specified in a module), and implicit + * bindings ({@literal @}{@link com.google.inject.ImplementedBy ImplementedBy}, default + * constructors etc.) may be used to satisfy optional injection points. + */ + public boolean isOptional() { + return optional; + } + + /** + * Returns true if the element is annotated with {@literal @}{@link Toolable}. + */ + public boolean isToolable() { + return ((AnnotatedElement) member).isAnnotationPresent(Toolable.class); + } + + /** + * Returns the generic type that defines this injection point. If the member exists on a + * parameterized type, the result will include more type information than the member's {@link + * Member#getDeclaringClass() raw declaring class}. + */ + public TypeLiteral getDeclaringType() { + return declaringType; + } + + @Override + public boolean equals(Object o) { + return o instanceof InjectionPoint + && member.equals(((InjectionPoint) o).member) + && declaringType.equals(((InjectionPoint) o).declaringType); + } + + @Override + public int hashCode() { + return member.hashCode() ^ declaringType.hashCode(); + } + + @Override + public String toString() { + return Classes.toString(member); + } + + /** + * Position in type hierarchy. + */ + enum Position { + TOP, // No need to check for overridden methods + MIDDLE, + BOTTOM // Methods won't be overridden + } + + /** + * Node in the doubly-linked list of injectable members (fields and methods). + */ + static abstract class InjectableMember { + final TypeLiteral declaringType; + final boolean optional; + final boolean jsr330; + InjectableMember previous; + InjectableMember next; + + InjectableMember(TypeLiteral declaringType, Annotation atInject) { + this.declaringType = declaringType; + + if (atInject.annotationType() == javax.inject.Inject.class) { + optional = false; + jsr330 = true; + return; + } + + jsr330 = false; + optional = ((Inject) atInject).optional(); + } + + abstract InjectionPoint toInjectionPoint(); + } + + static class InjectableField extends InjectableMember { + final Field field; + + InjectableField(TypeLiteral declaringType, Field field, + Annotation atInject) { + super(declaringType, atInject); + this.field = field; + } + + @Override + InjectionPoint toInjectionPoint() { + return new InjectionPoint(declaringType, field, optional); + } + } + + static class InjectableMethod extends InjectableMember { + final Method method; + /** + * true if this method overrode a method that was annotated + * with com.google.inject.Inject. used to allow different + * override behavior for guice inject vs javax.inject.Inject + */ + boolean overrodeGuiceInject; + + InjectableMethod(TypeLiteral declaringType, Method method, + Annotation atInject) { + super(declaringType, atInject); + this.method = method; + } + + @Override + InjectionPoint toInjectionPoint() { + return new InjectionPoint(declaringType, method, optional); + } + + public boolean isFinal() { + return Modifier.isFinal(method.getModifiers()); + } + } + + /** + * Linked list of injectable members. + */ + static class InjectableMembers { + InjectableMember head; + InjectableMember tail; + + void add(InjectableMember member) { + if (head == null) { + head = tail = member; + } else { + member.previous = tail; + tail.next = member; + tail = member; + } + } + + void remove(InjectableMember member) { + if (member.previous != null) { + member.previous.next = member.next; + } + if (member.next != null) { + member.next.previous = member.previous; + } + if (head == member) { + head = member.next; + } + if (tail == member) { + tail = member.previous; + } + } + + boolean isEmpty() { + return head == null; + } + } + + /** + * Keeps track of injectable methods so we can remove methods that get overridden in O(1) time. + * Uses our position in the type hierarchy to perform optimizations. + */ + static class OverrideIndex { + final InjectableMembers injectableMembers; + Map> bySignature; + Position position = Position.TOP; + /* Caches the signature for the last method. */ + Method lastMethod; + Signature lastSignature; + OverrideIndex(InjectableMembers injectableMembers) { + this.injectableMembers = injectableMembers; + } + + /** + * Removes a method overridden by the given method, if present. In order to + * remain backwards compatible with prior Guice versions, this will *not* + * remove overridden methods if 'alwaysRemove' is false and the overridden + * signature was annotated with a com.google.inject.Inject. + * + * @param method The method used to determine what is overridden and should be + * removed. + * @param alwaysRemove true if overridden methods should be removed even if they were + * guice @Inject + * @param injectableMethod if this method overrode any guice @Inject methods, + * {@link InjectableMethod#overrodeGuiceInject} is set to true + */ + boolean removeIfOverriddenBy(Method method, boolean alwaysRemove, + InjectableMethod injectableMethod) { + if (position == Position.TOP) { + // If we're at the top of the hierarchy, there's nothing to override. + return false; + } + + if (bySignature == null) { + // We encountered a method in a subclass. Time to index the + // methods in the parent class. + bySignature = new HashMap>(); + for (InjectableMember member = injectableMembers.head; member != null; + member = member.next) { + if (!(member instanceof InjectableMethod)) { + continue; + } + InjectableMethod im = (InjectableMethod) member; + if (im.isFinal()) { + continue; + } + List methods = new ArrayList(); + methods.add(im); + bySignature.put(new Signature(im.method), methods); + } + } + + lastMethod = method; + Signature signature = lastSignature = new Signature(method); + List methods = bySignature.get(signature); + boolean removed = false; + if (methods != null) { + for (Iterator iterator = methods.iterator(); + iterator.hasNext(); ) { + InjectableMethod possiblyOverridden = iterator.next(); + if (overrides(method, possiblyOverridden.method)) { + boolean wasGuiceInject = + !possiblyOverridden.jsr330 || possiblyOverridden.overrodeGuiceInject; + if (injectableMethod != null) { + injectableMethod.overrodeGuiceInject = wasGuiceInject; + } + // Only actually remove the methods if we want to force + // remove or if the signature never specified @com.google.inject.Inject + // somewhere. + if (alwaysRemove || !wasGuiceInject) { + removed = true; + iterator.remove(); + injectableMembers.remove(possiblyOverridden); + } + } + } + } + return removed; + } + + /** + * Adds the given method to the list of injection points. Keeps track of it in this index + * in case it gets overridden. + */ + void add(InjectableMethod injectableMethod) { + injectableMembers.add(injectableMethod); + if (position == Position.BOTTOM + || injectableMethod.isFinal()) { + // This method can't be overridden, so there's no need to index it. + return; + } + if (bySignature != null) { + // Try to reuse the signature we created during removal + Signature signature = injectableMethod.method == lastMethod + ? lastSignature : new Signature(injectableMethod.method); + List methods = bySignature.get(signature); + if (methods == null) { + methods = new ArrayList(); + bySignature.put(signature, methods); + } + methods.add(injectableMethod); + } + } + } + + /** + * A method signature. Used to handle method overridding. + */ + static class Signature { + + final String name; + final Class[] parameterTypes; + final int hash; + + Signature(Method method) { + this.name = method.getName(); + this.parameterTypes = method.getParameterTypes(); + + int h = name.hashCode(); + h = h * 31 + parameterTypes.length; + for (Class parameterType : parameterTypes) { + h = h * 31 + parameterType.hashCode(); + } + this.hash = h; + } + + @Override + public int hashCode() { + return this.hash; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Signature)) { + return false; + } + + Signature other = (Signature) o; + if (!name.equals(other.name)) { + return false; + } + + if (parameterTypes.length != other.parameterTypes.length) { + return false; + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] != other.parameterTypes[i]) { + return false; + } + } + + return true; + } + } +} diff --git a/src/main/java/com/google/inject/spi/InjectionRequest.java b/src/main/java/com/google/inject/spi/InjectionRequest.java new file mode 100644 index 0000000..16aeaa9 --- /dev/null +++ b/src/main/java/com/google/inject/spi/InjectionRequest.java @@ -0,0 +1,68 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.ConfigurationException; +import com.google.inject.TypeLiteral; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A request to inject the instance fields and methods of an instance. Requests are created + * explicitly in a module using {@link com.google.inject.Binder#requestInjection(Object) + * requestInjection()} statements: + *

+ *     requestInjection(serviceInstance);
+ * + */ +public final class InjectionRequest implements Element { + + private final Object source; + private final TypeLiteral type; + private final T instance; + + public InjectionRequest(Object source, TypeLiteral type, T instance) { + this.source = checkNotNull(source, "source"); + this.type = checkNotNull(type, "type"); + this.instance = checkNotNull(instance, "instance"); + } + + public Object getSource() { + return source; + } + + public T getInstance() { + return instance; + } + + public TypeLiteral getType() { + return type; + } + + /** + * Returns the instance methods and fields of {@code instance} that will be injected to fulfill + * this request. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + * @throws ConfigurationException if there is a malformed injection point on the class of {@code + * instance}, such as a field with multiple binding annotations. The exception's + * {@link + * ConfigurationException#getPartialValue() partial value} is a {@code + * Set} + * of the valid injection points. + */ + public Set getInjectionPoints() throws ConfigurationException { + return InjectionPoint.forInstanceMethodsAndFields(instance.getClass()); + } + + public R acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).requestInjection(type, instance); + } +} diff --git a/src/main/java/com/google/inject/spi/InstanceBinding.java b/src/main/java/com/google/inject/spi/InstanceBinding.java new file mode 100644 index 0000000..e3df76e --- /dev/null +++ b/src/main/java/com/google/inject/spi/InstanceBinding.java @@ -0,0 +1,26 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; + +import java.util.Set; + +/** + * A binding to a single instance. The same instance is returned for every injection. + * + */ +public interface InstanceBinding extends Binding, HasDependencies { + + /** + * Returns the user-supplied instance. + */ + T getInstance(); + + /** + * Returns the field and method injection points of the instance, injected at injector-creation + * time only. + * + * @return a possibly empty set + */ + Set getInjectionPoints(); + +} diff --git a/src/main/java/com/google/inject/spi/LinkedKeyBinding.java b/src/main/java/com/google/inject/spi/LinkedKeyBinding.java new file mode 100644 index 0000000..21831c3 --- /dev/null +++ b/src/main/java/com/google/inject/spi/LinkedKeyBinding.java @@ -0,0 +1,17 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Key; + +/** + * A binding to a linked key. The other key's binding is used to resolve injections. + */ +public interface LinkedKeyBinding extends Binding { + + /** + * Returns the linked key used to resolve injections. That binding can be retrieved from an + * injector using {@link com.google.inject.Injector#getBinding(Key) Injector.getBinding(key)}. + */ + Key getLinkedKey(); + +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/MembersInjectorLookup.java b/src/main/java/com/google/inject/spi/MembersInjectorLookup.java new file mode 100644 index 0000000..a43c0e1 --- /dev/null +++ b/src/main/java/com/google/inject/spi/MembersInjectorLookup.java @@ -0,0 +1,86 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.MembersInjector; +import com.google.inject.TypeLiteral; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * A lookup of the members injector for a type. Lookups are created explicitly in a module using + * {@link com.google.inject.Binder#getMembersInjector(Class) getMembersInjector()} statements: + *
+ *     MembersInjector<PaymentService> membersInjector
+ *         = getMembersInjector(PaymentService.class);
+ * + */ +public final class MembersInjectorLookup implements Element { + + private final Object source; + private final TypeLiteral type; + private MembersInjector delegate; + + public MembersInjectorLookup(Object source, TypeLiteral type) { + this.source = checkNotNull(source, "source"); + this.type = checkNotNull(type, "type"); + } + + public Object getSource() { + return source; + } + + /** + * Gets the type containing the members to be injected. + */ + public TypeLiteral getType() { + return type; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + /** + * Sets the actual members injector. + * + * @throws IllegalStateException if the delegate is already set + */ + public void initializeDelegate(MembersInjector delegate) { + checkState(this.delegate == null, "delegate already initialized"); + this.delegate = checkNotNull(delegate, "delegate"); + } + + public void applyTo(Binder binder) { + initializeDelegate(binder.withSource(getSource()).getMembersInjector(type)); + } + + /** + * Returns the delegate members injector, or {@code null} if it has not yet been initialized. + * The delegate will be initialized when this element is processed, or otherwise used to create + * an injector. + */ + public MembersInjector getDelegate() { + return delegate; + } + + /** + * Returns the looked up members injector. The result is not valid until this lookup has been + * initialized, which usually happens when the injector is created. The members injector will + * throw an {@code IllegalStateException} if you try to use it beforehand. + */ + public MembersInjector getMembersInjector() { + return new MembersInjector() { + public void injectMembers(T instance) { + checkState(delegate != null, + "This MembersInjector cannot be used until the Injector has been created."); + delegate.injectMembers(instance); + } + + @Override + public String toString() { + return "MembersInjector<" + type + ">"; + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/Message.java b/src/main/java/com/google/inject/spi/Message.java new file mode 100644 index 0000000..70f3af8 --- /dev/null +++ b/src/main/java/com/google/inject/spi/Message.java @@ -0,0 +1,100 @@ +package com.google.inject.spi; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.internal.Errors; +import com.google.inject.internal.util.SourceProvider; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * An error message and the context in which it occured. Messages are usually created internally by + * Guice and its extensions. Messages can be created explicitly in a module using {@link + * com.google.inject.Binder#addError(Throwable) addError()} statements: + *
+ *     try {
+ *       bindPropertiesFromFile();
+ *     } catch (IOException e) {
+ *       addError(e);
+ *     }
+ * + */ +public final class Message implements Element { + private final String message; + private final Throwable cause; + private final List sources; + + public Message(List sources, String message, Throwable cause) { + this.sources = ImmutableList.copyOf(sources); + this.message = checkNotNull(message, "message"); + this.cause = cause; + } + + public Message(String message, Throwable cause) { + this(ImmutableList.of(), message, cause); + } + + public Message(Object source, String message) { + this(ImmutableList.of(source), message, null); + } + + public Message(String message) { + this(ImmutableList.of(), message, null); + } + + public String getSource() { + return sources.isEmpty() + ? SourceProvider.UNKNOWN_SOURCE.toString() + : Errors.convert(sources.get(sources.size() - 1)).toString(); + } + + public List getSources() { + return sources; + } + + /** + * Gets the error message text. + */ + public String getMessage() { + return message; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + /** + * Returns the throwable that caused this message, or {@code null} if this + * message was not caused by a throwable. + */ + public Throwable getCause() { + return cause; + } + + @Override + public String toString() { + return message; + } + + @Override + public int hashCode() { + return message.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Message)) { + return false; + } + Message e = (Message) o; + return message.equals(e.message) && Objects.equal(cause, e.cause) && sources.equals(e.sources); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).addError(this); + } + +} diff --git a/src/main/java/com/google/inject/spi/ModuleAnnotatedMethodScanner.java b/src/main/java/com/google/inject/spi/ModuleAnnotatedMethodScanner.java new file mode 100644 index 0000000..4b2ff40 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ModuleAnnotatedMethodScanner.java @@ -0,0 +1,37 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.Key; + +import java.lang.annotation.Annotation; +import java.util.Set; + +/** + * Allows extensions to scan modules for annotated methods and bind those methods + * as providers, similar to {@code @Provides} methods. + */ +public abstract class ModuleAnnotatedMethodScanner { + + /** + * Returns the annotations this should scan for. Every method in the module that has one of these + * annotations will create a Provider binding, with the return value of the binding being what's + * provided and the parameters of the method being dependencies of the provider. + */ + public abstract Set> annotationClasses(); + + /** + * Prepares a method for binding. This {@code key} parameter is the key discovered from looking at + * the binding annotation and return value of the method. Implementations can modify the key to + * instead bind to another key. For example, Multibinder may want to change + * {@code @SetProvides String provideFoo()} to bind into a unique Key within the multibinder + * instead of binding {@code String}. + * + *

The injection point and annotation are provided in case the implementation wants to set the + * key based on the property of the annotation or if any additional preparation is needed for any + * of the dependencies. The annotation is guaranteed to be an instance of one the classes returned + * by {@link #annotationClasses}. + */ + public abstract Key prepareMethod(Binder binder, Annotation annotation, Key key, + InjectionPoint injectionPoint); + +} diff --git a/src/main/java/com/google/inject/spi/ModuleAnnotatedMethodScannerBinding.java b/src/main/java/com/google/inject/spi/ModuleAnnotatedMethodScannerBinding.java new file mode 100644 index 0000000..8ff988a --- /dev/null +++ b/src/main/java/com/google/inject/spi/ModuleAnnotatedMethodScannerBinding.java @@ -0,0 +1,41 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.internal.Errors; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Represents a call to {@link Binder#scanModulesForAnnotatedMethods} in a module. + */ +public final class ModuleAnnotatedMethodScannerBinding implements Element { + private final Object source; + private final ModuleAnnotatedMethodScanner scanner; + + public ModuleAnnotatedMethodScannerBinding(Object source, ModuleAnnotatedMethodScanner scanner) { + this.source = checkNotNull(source, "source"); + this.scanner = checkNotNull(scanner, "scanner"); + } + + public Object getSource() { + return source; + } + + public ModuleAnnotatedMethodScanner getScanner() { + return scanner; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).scanModulesForAnnotatedMethods(scanner); + } + + @Override + public String toString() { + return scanner + " which scans for " + scanner.annotationClasses() + + " (bound at " + Errors.convert(source) + ")"; + } +} diff --git a/src/main/java/com/google/inject/spi/ModuleSource.java b/src/main/java/com/google/inject/spi/ModuleSource.java new file mode 100644 index 0000000..9ca8a77 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ModuleSource.java @@ -0,0 +1,170 @@ +package com.google.inject.spi; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.internal.util.StackTraceElements; +import com.google.inject.internal.util.StackTraceElements.InMemoryStackTraceElement; + +import java.util.List; + +/** + * Associated to a {@link Module module}, provides the module class name, the parent module {@link + * ModuleSource source}, and the call stack that ends just before the module {@link + * Module#configure(Binder)} method invocation. + */ +final class ModuleSource { + + /** + * The class name of module that this {@link ModuleSource} associated to. + */ + private final String moduleClassName; + + /** + * The parent {@link ModuleSource module source}. + */ + private final ModuleSource parent; + + /** + * The chunk of call stack that starts from the parent module {@link Module#configure(Binder) + * configure(Binder)} call and ends just before the module {@link Module#configure(Binder) + * configure(Binder)} method invocation. For a module without a parent module the chunk starts + * from the bottom of call stack. The array is non-empty if stack trace collection is on. + */ + private final InMemoryStackTraceElement[] partialCallStack; + + /** + * Creates a new {@link ModuleSource} with a {@literal null} parent. + * + * @param module the corresponding module + * @param partialCallStack the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * Module#configure(Binder) configure(Binder)} method invocation + */ + ModuleSource(Object module, StackTraceElement[] partialCallStack) { + this(null, module, partialCallStack); + } + + /** + * Creates a new {@link ModuleSource} Object. + * + * @param parent the parent module {@link ModuleSource source} + * @param module the corresponding module + * @param partialCallStack the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * Module#configure(Binder) configure(Binder)} method invocation + */ + private ModuleSource( + /* @Nullable */ ModuleSource parent, Object module, StackTraceElement[] partialCallStack) { + Preconditions.checkNotNull(module, "module cannot be null."); + Preconditions.checkNotNull(partialCallStack, "partialCallStack cannot be null."); + this.parent = parent; + this.moduleClassName = module.getClass().getName(); + this.partialCallStack = StackTraceElements.convertToInMemoryStackTraceElement(partialCallStack); + } + + /** + * Returns the corresponding module class name. + * + * @see Class#getName() + */ + String getModuleClassName() { + return moduleClassName; + } + + /** + * Returns the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * Module#configure(Binder) configure(Binder)} method invocation. The return array is non-empty + * only if stack trace collection is on. + */ + StackTraceElement[] getPartialCallStack() { + return StackTraceElements.convertToStackTraceElement(partialCallStack); + } + + /** + * Returns the size of partial call stack if stack trace collection is on otherwise zero. + */ + int getPartialCallStackSize() { + return partialCallStack.length; + } + + /** + * Creates and returns a child {@link ModuleSource} corresponding to the {@link Module module}. + * + * @param module the corresponding module + * @param partialCallStack the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * Module#configure(Binder) configure(Binder)} method invocation + */ + ModuleSource createChild(Object module, StackTraceElement[] partialCallStack) { + return new ModuleSource(this, module, partialCallStack); + } + + /** + * Returns the parent module {@link ModuleSource source}. + */ + ModuleSource getParent() { + return parent; + } + + /** + * Returns the class names of modules in this module source. The first element (index 0) is filled + * by this object {@link #getModuleClassName()}. The second element is filled by the parent's + * {@link #getModuleClassName()} and so on. + */ + List getModuleClassNames() { + ImmutableList.Builder classNames = ImmutableList.builder(); + ModuleSource current = this; + while (current != null) { + String className = current.moduleClassName; + classNames.add(className); + current = current.parent; + } + return classNames.build(); + } + + /** + * Returns the size of {@link ModuleSource ModuleSources} chain (all parents) that ends at this + * object. + */ + int size() { + if (parent == null) { + return 1; + } + return parent.size() + 1; + } + + /** + * Returns the size of call stack that ends just before the module {@link Module#configure(Binder) + * configure(Binder)} method invocation (see {@link #getStackTrace()}). + */ + int getStackTraceSize() { + if (parent == null) { + return partialCallStack.length; + } + return parent.getStackTraceSize() + partialCallStack.length; + } + + /** + * Returns the full call stack that ends just before the module {@link Module#configure(Binder) + * configure(Binder)} method invocation. The return array is non-empty if stack trace collection + * on. + */ + StackTraceElement[] getStackTrace() { + int stackTraceSize = getStackTraceSize(); + StackTraceElement[] callStack = new StackTraceElement[stackTraceSize]; + int cursor = 0; + ModuleSource current = this; + while (current != null) { + StackTraceElement[] chunk = + StackTraceElements.convertToStackTraceElement(current.partialCallStack); + int chunkSize = chunk.length; + System.arraycopy(chunk, 0, callStack, cursor, chunkSize); + current = current.parent; + cursor = cursor + chunkSize; + } + return callStack; + } +} diff --git a/src/main/java/com/google/inject/spi/PrivateElements.java b/src/main/java/com/google/inject/spi/PrivateElements.java new file mode 100644 index 0000000..9bf5c9d --- /dev/null +++ b/src/main/java/com/google/inject/spi/PrivateElements.java @@ -0,0 +1,42 @@ +package com.google.inject.spi; + +import com.google.inject.Injector; +import com.google.inject.Key; + +import java.util.List; +import java.util.Set; + +/** + * A private collection of elements that are hidden from the enclosing injector or module by + * default. See {@link com.google.inject.PrivateModule PrivateModule} for details. + */ +public interface PrivateElements extends Element { + + /** + * Returns the configuration information in this private environment. + */ + List getElements(); + + /** + * Returns the child injector that hosts these private elements, or null if the elements haven't + * been used to create an injector. + */ + Injector getInjector(); + + /** + * Returns the unique exposed keys for these private elements. + */ + Set> getExposedKeys(); + + /** + * Returns an arbitrary object containing information about the "place" where this key was + * exposed. Used by Guice in the production of descriptive error messages. + * + *

Tools might specially handle types they know about; {@code StackTraceElement} is a good + * example. Tools should simply call {@code toString()} on the source object if the type is + * unfamiliar. + * + * @param key one of the keys exposed by this module. + */ + Object getExposedSource(Key key); +} diff --git a/src/main/java/com/google/inject/spi/ProviderBinding.java b/src/main/java/com/google/inject/spi/ProviderBinding.java new file mode 100644 index 0000000..69fd748 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProviderBinding.java @@ -0,0 +1,19 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Provider; + +/** + * A binding to a {@link Provider} that delegates to the binding for the provided type. This binding + * is used whenever a {@code Provider} is injected (as opposed to injecting {@code T} directly). + */ +public interface ProviderBinding> extends Binding { + + /** + * Returns the key whose binding is used to {@link Provider#get provide instances}. That binding + * can be retrieved from an injector using {@link com.google.inject.Injector#getBinding(Key) + * Injector.getBinding(providedKey)} + */ + Key getProvidedKey(); +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/ProviderInstanceBinding.java b/src/main/java/com/google/inject/spi/ProviderInstanceBinding.java new file mode 100644 index 0000000..371a7f1 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProviderInstanceBinding.java @@ -0,0 +1,26 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; + +import java.util.Set; + +/** + * A binding to a provider instance. The provider's {@code get} method is invoked to resolve + * injections. + */ +public interface ProviderInstanceBinding extends Binding, HasDependencies { + + /** + * Returns the user-supplied, unscoped provider. + */ + javax.inject.Provider getUserSuppliedProvider(); + + /** + * Returns the field and method injection points of the provider, injected at injector-creation + * time only. + * + * @return a possibly empty set + */ + Set getInjectionPoints(); + +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/ProviderKeyBinding.java b/src/main/java/com/google/inject/spi/ProviderKeyBinding.java new file mode 100644 index 0000000..af74647 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProviderKeyBinding.java @@ -0,0 +1,19 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Key; + +/** + * A binding to a provider key. To resolve injections, the provider key is first resolved, then that + * provider's {@code get} method is invoked. + */ +public interface ProviderKeyBinding extends Binding { + + /** + * Returns the key used to resolve the provider's binding. That binding can be retrieved from an + * injector using {@link com.google.inject.Injector#getBinding(Key) + * Injector.getBinding(providerKey)} + */ + Key> getProviderKey(); + +} \ No newline at end of file diff --git a/src/main/java/com/google/inject/spi/ProviderLookup.java b/src/main/java/com/google/inject/spi/ProviderLookup.java new file mode 100644 index 0000000..911d72f --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProviderLookup.java @@ -0,0 +1,100 @@ +package com.google.inject.spi; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.util.Types; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * A lookup of the provider for a type. Lookups are created explicitly in a module using + * {@link com.google.inject.Binder#getProvider(Class) getProvider()} statements: + *

+ *     Provider<PaymentService> paymentServiceProvider
+ *         = getProvider(PaymentService.class);
+ * + */ +public final class ProviderLookup implements Element { + private final Object source; + private final Dependency dependency; + private Provider delegate; + + public ProviderLookup(Object source, Key key) { + this(source, Dependency.get(checkNotNull(key, "key"))); + } + + public ProviderLookup(Object source, Dependency dependency) { + this.source = checkNotNull(source, "source"); + this.dependency = checkNotNull(dependency, "dependency"); + } + + public Object getSource() { + return source; + } + + public Key getKey() { + return dependency.getKey(); + } + + public Dependency getDependency() { + return dependency; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + /** + * Sets the actual provider. + * + * @throws IllegalStateException if the delegate is already set + */ + public void initializeDelegate(Provider delegate) { + checkState(this.delegate == null, "delegate already initialized"); + this.delegate = checkNotNull(delegate, "delegate"); + } + + public void applyTo(Binder binder) { + initializeDelegate(binder.withSource(getSource()).getProvider(dependency)); + } + + /** + * Returns the delegate provider, or {@code null} if it has not yet been initialized. The delegate + * will be initialized when this element is processed, or otherwise used to create an injector. + */ + public Provider getDelegate() { + return delegate; + } + + /** + * Returns the looked up provider. The result is not valid until this lookup has been initialized, + * which usually happens when the injector is created. The provider will throw an {@code + * IllegalStateException} if you try to use it beforehand. + */ + public Provider getProvider() { + return new ProviderWithDependencies() { + public T get() { + checkState(delegate != null, + "This Provider cannot be used until the Injector has been created."); + return delegate.get(); + } + + public Set> getDependencies() { + // We depend on Provider, not T directly. This is an important distinction + // for dependency analysis tools that short-circuit on providers. + Key providerKey = getKey().ofType(Types.providerOf(getKey().getTypeLiteral().getType())); + return ImmutableSet.>of(Dependency.get(providerKey)); + } + + @Override + public String toString() { + return "Provider<" + getKey().getTypeLiteral() + ">"; + } + }; + } +} diff --git a/src/main/java/com/google/inject/spi/ProviderWithDependencies.java b/src/main/java/com/google/inject/spi/ProviderWithDependencies.java new file mode 100644 index 0000000..e77a7b0 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProviderWithDependencies.java @@ -0,0 +1,10 @@ +package com.google.inject.spi; + +import com.google.inject.Provider; + +/** + * A provider with dependencies on other injected types. If a {@link Provider} has dependencies that + * aren't specified in injections, this interface should be used to expose all dependencies. + */ +public interface ProviderWithDependencies extends Provider, HasDependencies { +} diff --git a/src/main/java/com/google/inject/spi/ProviderWithExtensionVisitor.java b/src/main/java/com/google/inject/spi/ProviderWithExtensionVisitor.java new file mode 100644 index 0000000..e62d1e5 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProviderWithExtensionVisitor.java @@ -0,0 +1,44 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Provider; + +/** + * A Provider that is part of an extension which supports a custom + * BindingTargetVisitor. + *

+ * When an extension binds a provider instance, the provider can implement this + * interface to allow users using the + * {@link Binding#acceptTargetVisitor(BindingTargetVisitor)} method to visit a + * custom visitor designed for that extension. A typical implementation within + * the extension would look like + *

+ * <V, B> V acceptExtensionVisitor(BindingTargetVisitor<B, V> visitor, ProviderInstanceBinding<? extends B>
+ * binding) {
+ *   if(visitor instanceof MyCustomExtensionVisitor) {
+ *     return ((MyCustomExtensionVisitor<B, V>)visitor).visitCustomExtension(customProperties, binding);
+ *   } else {
+ *     return visitor.visit(binding);
+ *   }
+ * }
+ * 'MyCustomExtensionVisitor' in the example above would be an interface the + * extension provides that users can implement in order to be notified of custom + * extension information. These visitor interfaces must extend from + * BindingTargetVisitor. + * + */ +public interface ProviderWithExtensionVisitor extends Provider { + + /** + * Instructs the extension determine if the visitor is an instance of a custom + * extension visitor, and if so, visit it using that method. If the visitor is + * not an instance of the custom extension visitor, this method MUST + * call visitor.visit(binding). + *

+ * Due to issues with generics, the type parameters of this method do not + * relate to the type of the provider. In practice, the 'B' type will always + * be a supertype of 'T'. + */ + V acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding); +} diff --git a/src/main/java/com/google/inject/spi/ProvidesMethodBinding.java b/src/main/java/com/google/inject/spi/ProvidesMethodBinding.java new file mode 100644 index 0000000..60f25a3 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProvidesMethodBinding.java @@ -0,0 +1,38 @@ +package com.google.inject.spi; + +import com.google.inject.Key; +import com.google.inject.Provides; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * An {@literal @}{@link Provides} binding or binding produced by a + * {@link ModuleAnnotatedMethodScanner}. + * + */ +public interface ProvidesMethodBinding extends HasDependencies { + + /** + * Returns the method this binding uses. + */ + Method getMethod(); + + /** + * Returns the instance of the object the method is defined in. + */ + Object getEnclosingInstance(); + + /** + * Returns the key of the binding. + */ + Key getKey(); + + /** + * Returns the annotation that caused this binding to be created. For {@code @Provides} methods, + * this is an instance of the {@code @Provides} annotation. For bindings from + * {@link ModuleAnnotatedMethodScanner}, this is the annotation that caused the scanner to produce + * the binding. + */ + Annotation getAnnotation(); +} diff --git a/src/main/java/com/google/inject/spi/ProvidesMethodTargetVisitor.java b/src/main/java/com/google/inject/spi/ProvidesMethodTargetVisitor.java new file mode 100644 index 0000000..5d0b8bf --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProvidesMethodTargetVisitor.java @@ -0,0 +1,17 @@ +package com.google.inject.spi; + +import com.google.inject.Provides; + +/** + * A visitor for the {@literal @}{@link Provides} bindings. + *

+ * If your {@link BindingTargetVisitor} implements this interface, bindings created by using + * {@code @Provides} will be visited through this interface. + */ +public interface ProvidesMethodTargetVisitor extends BindingTargetVisitor { + + /** + * Visits an {@link ProvidesMethodBinding} created with an {@literal @}{@link Provides} method. + */ + V visit(ProvidesMethodBinding providesMethodBinding); +} diff --git a/src/main/java/com/google/inject/spi/ProvisionListener.java b/src/main/java/com/google/inject/spi/ProvisionListener.java new file mode 100644 index 0000000..463dbff --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProvisionListener.java @@ -0,0 +1,58 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Provider; +import com.google.inject.Scope; + +import java.util.List; + +/** + * Listens for provisioning of objects. Useful for gathering timing information + * about provisioning, post-provision initialization, and more. + * + */ +public interface ProvisionListener { + + /** + * Invoked by Guice when an object requires provisioning. Provisioning occurs + * when Guice locates and injects the dependencies for a binding. For types + * bound to a Provider, provisioning encapsulates the {@link Provider#get} + * method. For toInstance or constant bindings, provisioning encapsulates + * the injecting of {@literal @}{@code Inject}ed fields or methods. + * For other types, provisioning encapsulates the construction of the + * object. If a type is bound within a {@link Scope}, provisioning depends on + * the scope. Types bound in Singleton scope will only be provisioned once. + * Types bound in no scope will be provisioned every time they are injected. + * Other scopes define their own behavior for provisioning. + *

+ * To perform the provision, call {@link ProvisionInvocation#provision()}. + * If you do not explicitly call provision, it will be automatically done after + * this method returns. It is an error to call provision more than once. + */ + void onProvision(ProvisionInvocation provision); + + /** + * Encapsulates a single act of provisioning. + */ + public abstract static class ProvisionInvocation { + + /** + * Returns the Binding this is provisioning. + *

+ * You must not call {@link Provider#get()} on the provider returned by + * {@link Binding#getProvider}, otherwise you will get confusing error messages. + */ + public abstract Binding getBinding(); + + /** + * Performs the provision, returning the object provisioned. + */ + public abstract T provision(); + + /** + * Returns the dependency chain that led to this object being provisioned. + */ + public abstract List getDependencyChain(); + + } +} diff --git a/src/main/java/com/google/inject/spi/ProvisionListenerBinding.java b/src/main/java/com/google/inject/spi/ProvisionListenerBinding.java new file mode 100644 index 0000000..8d8b719 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ProvisionListenerBinding.java @@ -0,0 +1,54 @@ +package com.google.inject.spi; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.matcher.Matcher; + +import java.util.List; + +/** + * Binds keys (picked using a Matcher) to a provision listener. Listeners are created explicitly in + * a module using {@link Binder#bindListener(Matcher, ProvisionListener...)} statements: + */ +public final class ProvisionListenerBinding implements Element { + + private final Object source; + private final Matcher> bindingMatcher; + private final List listeners; + + ProvisionListenerBinding(Object source, + Matcher> bindingMatcher, + ProvisionListener[] listeners) { + this.source = source; + this.bindingMatcher = bindingMatcher; + this.listeners = ImmutableList.copyOf(listeners); + } + + /** + * Returns the registered listeners. + */ + public List getListeners() { + return listeners; + } + + /** + * Returns the binding matcher which chooses which bindings the listener should be notified of. + */ + public Matcher> getBindingMatcher() { + return bindingMatcher; + } + + public Object getSource() { + return source; + } + + public R acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).bindListener(bindingMatcher, + listeners.toArray(new ProvisionListener[listeners.size()])); + } +} diff --git a/src/main/java/com/google/inject/spi/RequireAtInjectOnConstructorsOption.java b/src/main/java/com/google/inject/spi/RequireAtInjectOnConstructorsOption.java new file mode 100644 index 0000000..d17db13 --- /dev/null +++ b/src/main/java/com/google/inject/spi/RequireAtInjectOnConstructorsOption.java @@ -0,0 +1,29 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A request to require explicit {@literal @}{@link Inject} annotations on constructors. + */ +public final class RequireAtInjectOnConstructorsOption implements Element { + private final Object source; + + RequireAtInjectOnConstructorsOption(Object source) { + this.source = checkNotNull(source, "source"); + } + + public Object getSource() { + return source; + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).requireAtInjectOnConstructors(); + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/com/google/inject/spi/RequireExactBindingAnnotationsOption.java b/src/main/java/com/google/inject/spi/RequireExactBindingAnnotationsOption.java new file mode 100644 index 0000000..41fb488 --- /dev/null +++ b/src/main/java/com/google/inject/spi/RequireExactBindingAnnotationsOption.java @@ -0,0 +1,28 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A request to require exact binding annotations. + */ +public final class RequireExactBindingAnnotationsOption implements Element { + private final Object source; + + RequireExactBindingAnnotationsOption(Object source) { + this.source = checkNotNull(source, "source"); + } + + public Object getSource() { + return source; + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).requireExactBindingAnnotations(); + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/com/google/inject/spi/RequireExplicitBindingsOption.java b/src/main/java/com/google/inject/spi/RequireExplicitBindingsOption.java new file mode 100644 index 0000000..194c0a1 --- /dev/null +++ b/src/main/java/com/google/inject/spi/RequireExplicitBindingsOption.java @@ -0,0 +1,28 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A request to require explicit bindings. + */ +public final class RequireExplicitBindingsOption implements Element { + private final Object source; + + RequireExplicitBindingsOption(Object source) { + this.source = checkNotNull(source, "source"); + } + + public Object getSource() { + return source; + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).requireExplicitBindings(); + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/com/google/inject/spi/ScopeBinding.java b/src/main/java/com/google/inject/spi/ScopeBinding.java new file mode 100644 index 0000000..b338b84 --- /dev/null +++ b/src/main/java/com/google/inject/spi/ScopeBinding.java @@ -0,0 +1,49 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.Scope; + +import java.lang.annotation.Annotation; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Registration of a scope annotation with the scope that implements it. Instances are created + * explicitly in a module using {@link com.google.inject.Binder#bindScope(Class, Scope) bindScope()} + * statements: + *

+ *     Scope recordScope = new RecordScope();
+ *     bindScope(RecordScoped.class, new RecordScope());
+ * + */ +public final class ScopeBinding implements Element { + private final Object source; + private final Class annotationType; + private final Scope scope; + + ScopeBinding(Object source, Class annotationType, Scope scope) { + this.source = checkNotNull(source, "source"); + this.annotationType = checkNotNull(annotationType, "annotationType"); + this.scope = checkNotNull(scope, "scope"); + } + + public Object getSource() { + return source; + } + + public Class getAnnotationType() { + return annotationType; + } + + public Scope getScope() { + return scope; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).bindScope(annotationType, scope); + } +} diff --git a/src/main/java/com/google/inject/spi/StaticInjectionRequest.java b/src/main/java/com/google/inject/spi/StaticInjectionRequest.java new file mode 100644 index 0000000..56f8e29 --- /dev/null +++ b/src/main/java/com/google/inject/spi/StaticInjectionRequest.java @@ -0,0 +1,59 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.ConfigurationException; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A request to inject the static fields and methods of a type. Requests are created + * explicitly in a module using {@link com.google.inject.Binder#requestStaticInjection(Class[]) + * requestStaticInjection()} statements: + *
+ *     requestStaticInjection(MyLegacyService.class);
+ * + */ +public final class StaticInjectionRequest implements Element { + private final Object source; + private final Class type; + + StaticInjectionRequest(Object source, Class type) { + this.source = checkNotNull(source, "source"); + this.type = checkNotNull(type, "type"); + } + + public Object getSource() { + return source; + } + + public Class getType() { + return type; + } + + /** + * Returns the static methods and fields of {@code type} that will be injected to fulfill this + * request. + * + * @return a possibly empty set of injection points. The set has a specified iteration order. All + * fields are returned and then all methods. Within the fields, supertype fields are returned + * before subtype fields. Similarly, supertype methods are returned before subtype methods. + * @throws ConfigurationException if there is a malformed injection point on {@code type}, such as + * a field with multiple binding annotations. The exception's {@link + * ConfigurationException#getPartialValue() partial value} is a {@code + * Set} + * of the valid injection points. + */ + public Set getInjectionPoints() throws ConfigurationException { + return InjectionPoint.forStaticMethodsAndFields(type); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).requestStaticInjection(type); + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/com/google/inject/spi/Toolable.java b/src/main/java/com/google/inject/spi/Toolable.java new file mode 100644 index 0000000..6bcbffb --- /dev/null +++ b/src/main/java/com/google/inject/spi/Toolable.java @@ -0,0 +1,27 @@ +package com.google.inject.spi; + +import com.google.inject.Injector; +import com.google.inject.Stage; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Instructs an {@link Injector} running in {@link Stage#TOOL} that a method should be injected. + * This is typically useful for for extensions to Guice that perform additional validation in an + * injected method or field. This only applies to objects that are already constructed when + * bindings are created (ie., something bound using {@link + * com.google.inject.binder.LinkedBindingBuilder#toProvider toProvider}, {@link + * com.google.inject.binder.LinkedBindingBuilder#toInstance toInstance}, or {@link + * com.google.inject.Binder#requestInjection requestInjection}. + * + */ +@Target({METHOD}) +@Retention(RUNTIME) +@Documented +public @interface Toolable { +} diff --git a/src/main/java/com/google/inject/spi/TypeConverter.java b/src/main/java/com/google/inject/spi/TypeConverter.java new file mode 100644 index 0000000..d0a0bce --- /dev/null +++ b/src/main/java/com/google/inject/spi/TypeConverter.java @@ -0,0 +1,14 @@ +package com.google.inject.spi; + +import com.google.inject.TypeLiteral; + +/** + * Converts constant string values to a different type. + */ +public interface TypeConverter { + + /** + * Converts a string value. Throws an exception if a conversion error occurs. + */ + Object convert(String value, TypeLiteral toType); +} diff --git a/src/main/java/com/google/inject/spi/TypeConverterBinding.java b/src/main/java/com/google/inject/spi/TypeConverterBinding.java new file mode 100644 index 0000000..f6e35f5 --- /dev/null +++ b/src/main/java/com/google/inject/spi/TypeConverterBinding.java @@ -0,0 +1,55 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.Errors; +import com.google.inject.matcher.Matcher; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Registration of type converters for matching target types. Instances are created + * explicitly in a module using {@link com.google.inject.Binder#convertToTypes(Matcher, + * TypeConverter) convertToTypes()} statements: + *
+ *     convertToTypes(Matchers.only(TypeLiteral.get(DateTime.class)), new DateTimeConverter());
+ * + */ +public final class TypeConverterBinding implements Element { + private final Object source; + private final Matcher> typeMatcher; + private final TypeConverter typeConverter; + + public TypeConverterBinding(Object source, Matcher> typeMatcher, + TypeConverter typeConverter) { + this.source = checkNotNull(source, "source"); + this.typeMatcher = checkNotNull(typeMatcher, "typeMatcher"); + this.typeConverter = checkNotNull(typeConverter, "typeConverter"); + } + + public Object getSource() { + return source; + } + + public Matcher> getTypeMatcher() { + return typeMatcher; + } + + public TypeConverter getTypeConverter() { + return typeConverter; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).convertToTypes(typeMatcher, typeConverter); + } + + @Override + public String toString() { + return typeConverter + " which matches " + typeMatcher + + " (bound at " + Errors.convert(source) + ")"; + } +} diff --git a/src/main/java/com/google/inject/spi/TypeEncounter.java b/src/main/java/com/google/inject/spi/TypeEncounter.java new file mode 100644 index 0000000..6d96ae5 --- /dev/null +++ b/src/main/java/com/google/inject/spi/TypeEncounter.java @@ -0,0 +1,84 @@ +package com.google.inject.spi; + +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; + +/** + * Context of an injectable type encounter. Enables reporting errors, registering injection + * listeners and binding method interceptors for injectable type {@code I}. It is an error to use + * an encounter after the {@link TypeListener#hear(TypeLiteral, TypeEncounter) hear()} method has + * returned. + * + * @param the injectable type encountered + */ +public interface TypeEncounter { + + /** + * Records an error message for type {@code I} which will be presented to the user at a later + * time. Unlike throwing an exception, this enable us to continue configuring the Injector and + * discover more errors. Uses {@link String#format(String, Object[])} to insert the arguments + * into the message. + */ + void addError(String message, Object... arguments); + + /** + * Records an exception for type {@code I}, the full details of which will be logged, and the + * message of which will be presented to the user at a later time. If your type listener calls + * something that you worry may fail, you should catch the exception and pass it to this method. + */ + void addError(Throwable t); + + /** + * Records an error message to be presented to the user at a later time. + */ + void addError(Message message); + + /** + * Returns the provider used to obtain instances for the given injection key. The returned + * provider will not be valid until the injector has been created. The provider will throw an + * {@code IllegalStateException} if you try to use it beforehand. + */ + Provider getProvider(Key key); + + /** + * Returns the provider used to obtain instances for the given injection type. The returned + * provider will not be valid until the injector has been created. The provider will throw an + * {@code IllegalStateException} if you try to use it beforehand. + */ + Provider getProvider(Class type); + + /** + * Returns the members injector used to inject dependencies into methods and fields on instances + * of the given type {@code T}. The returned members injector will not be valid until the main + * injector has been created. The members injector will throw an {@code IllegalStateException} + * if you try to use it beforehand. + * + * @param typeLiteral type to get members injector for + */ + MembersInjector getMembersInjector(TypeLiteral typeLiteral); + + /** + * Returns the members injector used to inject dependencies into methods and fields on instances + * of the given type {@code T}. The returned members injector will not be valid until the main + * injector has been created. The members injector will throw an {@code IllegalStateException} + * if you try to use it beforehand. + * + * @param type type to get members injector for + */ + MembersInjector getMembersInjector(Class type); + + /** + * Registers a members injector for type {@code I}. Guice will use the members injector after its + * performed its own injections on an instance of {@code I}. + */ + void register(MembersInjector membersInjector); + + /** + * Registers an injection listener for type {@code I}. Guice will notify the listener after all + * injections have been performed on an instance of {@code I}. + */ + void register(InjectionListener listener); + +} diff --git a/src/main/java/com/google/inject/spi/TypeListener.java b/src/main/java/com/google/inject/spi/TypeListener.java new file mode 100644 index 0000000..726f56d --- /dev/null +++ b/src/main/java/com/google/inject/spi/TypeListener.java @@ -0,0 +1,24 @@ +package com.google.inject.spi; + +import com.google.inject.TypeLiteral; + +/** + * Listens for Guice to encounter injectable types. If a given type has its constructor injected in + * one situation but only its methods and fields injected in another, Guice will notify this + * listener once. + */ +public interface TypeListener { + + /** + * Invoked when Guice encounters a new type eligible for constructor or members injection. + * Called during injector creation (or afterwords if Guice encounters a type at run time and + * creates a JIT binding). + * + * @param type encountered by Guice + * @param encounter context of this encounter, enables reporting errors, registering injection + * listeners and binding method interceptors for {@code type}. + * @param the injectable type + */ + void hear(TypeLiteral type, TypeEncounter encounter); + +} diff --git a/src/main/java/com/google/inject/spi/TypeListenerBinding.java b/src/main/java/com/google/inject/spi/TypeListenerBinding.java new file mode 100644 index 0000000..22a2ce6 --- /dev/null +++ b/src/main/java/com/google/inject/spi/TypeListenerBinding.java @@ -0,0 +1,53 @@ +package com.google.inject.spi; + +import com.google.inject.Binder; +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matcher; + +/** + * Binds types (picked using a Matcher) to an type listener. Registrations are created explicitly in + * a module using {@link com.google.inject.Binder#bindListener(Matcher, TypeListener)} statements: + * + *
+ *     register(only(new TypeLiteral<PaymentService<CreditCard>>() {}), listener);
+ * + */ +public final class TypeListenerBinding implements Element { + + private final Object source; + private final Matcher> typeMatcher; + private final TypeListener listener; + + TypeListenerBinding(Object source, TypeListener listener, + Matcher> typeMatcher) { + this.source = source; + this.listener = listener; + this.typeMatcher = typeMatcher; + } + + /** + * Returns the registered listener. + */ + public TypeListener getListener() { + return listener; + } + + /** + * Returns the type matcher which chooses which types the listener should be notified of. + */ + public Matcher> getTypeMatcher() { + return typeMatcher; + } + + public Object getSource() { + return source; + } + + public T acceptVisitor(ElementVisitor visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).bindListener(typeMatcher, listener); + } +} diff --git a/src/main/java/com/google/inject/spi/UntargettedBinding.java b/src/main/java/com/google/inject/spi/UntargettedBinding.java new file mode 100644 index 0000000..dc8aefd --- /dev/null +++ b/src/main/java/com/google/inject/spi/UntargettedBinding.java @@ -0,0 +1,11 @@ +package com.google.inject.spi; + +import com.google.inject.Binding; + +/** + * An untargetted binding. This binding indicates that the injector should use its implicit binding + * strategies to resolve injections. + * + */ +public interface UntargettedBinding extends Binding { +} diff --git a/src/main/java/com/google/inject/util/Modules.java b/src/main/java/com/google/inject/util/Modules.java new file mode 100644 index 0000000..463e01a --- /dev/null +++ b/src/main/java/com/google/inject/util/Modules.java @@ -0,0 +1,355 @@ +package com.google.inject.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.PrivateBinder; +import com.google.inject.PrivateModule; +import com.google.inject.Scope; +import com.google.inject.internal.Errors; +import com.google.inject.spi.DefaultBindingScopingVisitor; +import com.google.inject.spi.DefaultElementVisitor; +import com.google.inject.spi.Element; +import com.google.inject.spi.ElementVisitor; +import com.google.inject.spi.Elements; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; +import com.google.inject.spi.PrivateElements; +import com.google.inject.spi.ScopeBinding; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Static utility methods for creating and working with instances of {@link Module}. + * + */ +public final class Modules { + public static final Module EMPTY_MODULE = new EmptyModule(); + + private Modules() { + } + + /** + * Returns a builder that creates a module that overlays override modules over the given + * modules. If a key is bound in both sets of modules, only the binding from the override modules + * is kept. If a single {@link PrivateModule} is supplied or all elements are from + * a single {@link PrivateBinder}, then this will overwrite the private bindings. + * Otherwise, private bindings will not be overwritten unless they are exposed. + * This can be used to replace the bindings of a production module with test bindings: + *
+     * Module functionalTestModule
+     *     = Modules.override(new ProductionModule()).with(new TestModule());
+     * 
+ * + *

Prefer to write smaller modules that can be reused and tested without overrides. + * + * @param modules the modules whose bindings are open to be overridden + */ + public static OverriddenModuleBuilder override(Module... modules) { + return new RealOverriddenModuleBuilder(Arrays.asList(modules)); + } + + /** + * Returns a builder that creates a module that overlays override modules over the given + * modules. If a key is bound in both sets of modules, only the binding from the override modules + * is kept. If a single {@link PrivateModule} is supplied or all elements are from + * a single {@link PrivateBinder}, then this will overwrite the private bindings. + * Otherwise, private bindings will not be overwritten unless they are exposed. + * This can be used to replace the bindings of a production module with test bindings: + *

+     * Module functionalTestModule
+     *     = Modules.override(getProductionModules()).with(getTestModules());
+     * 
+ * + *

Prefer to write smaller modules that can be reused and tested without overrides. + * + * @param modules the modules whose bindings are open to be overridden + */ + public static OverriddenModuleBuilder override(Iterable modules) { + return new RealOverriddenModuleBuilder(modules); + } + + /** + * Returns a new module that installs all of {@code modules}. + */ + public static Module combine(Module... modules) { + return combine(ImmutableSet.copyOf(modules)); + } + + /** + * Returns a new module that installs all of {@code modules}. + */ + public static Module combine(Iterable modules) { + return new CombinedModule(modules); + } + + private static Module extractScanners(Iterable elements) { + final List scanners = Lists.newArrayList(); + ElementVisitor visitor = new DefaultElementVisitor() { + @Override + public Void visit(ModuleAnnotatedMethodScannerBinding binding) { + scanners.add(binding); + return null; + } + }; + for (Element element : elements) { + element.acceptVisitor(visitor); + } + return new AbstractModule() { + @Override + protected void configure() { + for (ModuleAnnotatedMethodScannerBinding scanner : scanners) { + scanner.applyTo(binder()); + } + } + }; + } + + /** + * See the EDSL example at {@link Modules#override(Module[]) override()}. + */ + public interface OverriddenModuleBuilder { + + /** + * See the EDSL example at {@link Modules#override(Module[]) override()}. + */ + Module with(Module... overrides); + + /** + * See the EDSL example at {@link Modules#override(Module[]) override()}. + */ + Module with(Iterable overrides); + } + + private static class EmptyModule implements Module { + public void configure(Binder binder) { + } + } + + private static class CombinedModule implements Module { + final Set modulesSet; + + CombinedModule(Iterable modules) { + this.modulesSet = ImmutableSet.copyOf(modules); + } + + public void configure(Binder binder) { + binder = binder.skipSources(getClass()); + for (Module module : modulesSet) { + binder.install(module); + } + } + } + + private static final class RealOverriddenModuleBuilder implements OverriddenModuleBuilder { + private final ImmutableSet baseModules; + + private RealOverriddenModuleBuilder(Iterable baseModules) { + this.baseModules = ImmutableSet.copyOf(baseModules); + } + + public Module with(Module... overrides) { + return with(Arrays.asList(overrides)); + } + + public Module with(Iterable overrides) { + return new OverrideModule(overrides, baseModules); + } + } + + static class OverrideModule extends AbstractModule { + private final ImmutableSet overrides; + private final ImmutableSet baseModules; + + OverrideModule(Iterable overrides, ImmutableSet baseModules) { + this.overrides = ImmutableSet.copyOf(overrides); + this.baseModules = baseModules; + } + + @Override + public void configure() { + Binder baseBinder = binder(); + List baseElements = Elements.getElements(currentStage(), baseModules); + + // If the sole element was a PrivateElements, we want to override + // the private elements within that -- so refocus our elements + // and binder. + if (baseElements.size() == 1) { + Element element = Iterables.getOnlyElement(baseElements); + if (element instanceof PrivateElements) { + PrivateElements privateElements = (PrivateElements) element; + PrivateBinder privateBinder = baseBinder.newPrivateBinder().withSource(privateElements.getSource()); + for (Key exposed : privateElements.getExposedKeys()) { + privateBinder.withSource(privateElements.getExposedSource(exposed)).expose(exposed); + } + baseBinder = privateBinder; + baseElements = privateElements.getElements(); + } + } + + final Binder binder = baseBinder.skipSources(this.getClass()); + final LinkedHashSet elements = new LinkedHashSet(baseElements); + final Module scannersModule = extractScanners(elements); + final List overrideElements = Elements.getElements(currentStage(), + ImmutableList.builder().addAll(overrides).add(scannersModule).build()); + + final Set> overriddenKeys = Sets.newHashSet(); + final Map, ScopeBinding> overridesScopeAnnotations = + Maps.newHashMap(); + + // execute the overrides module, keeping track of which keys and scopes are bound + new ModuleWriter(binder) { + @Override + public Void visit(Binding binding) { + overriddenKeys.add(binding.getKey()); + return super.visit(binding); + } + + @Override + public Void visit(ScopeBinding scopeBinding) { + overridesScopeAnnotations.put(scopeBinding.getAnnotationType(), scopeBinding); + return super.visit(scopeBinding); + } + + @Override + public Void visit(PrivateElements privateElements) { + overriddenKeys.addAll(privateElements.getExposedKeys()); + return super.visit(privateElements); + } + }.writeAll(overrideElements); + + // execute the original module, skipping all scopes and overridden keys. We only skip each + // overridden binding once so things still blow up if the module binds the same thing + // multiple times. + final Map> scopeInstancesInUse = Maps.newHashMap(); + final List scopeBindings = Lists.newArrayList(); + new ModuleWriter(binder) { + @Override + public Void visit(Binding binding) { + if (!overriddenKeys.remove(binding.getKey())) { + super.visit(binding); + + // Record when a scope instance is used in a binding + Scope scope = getScopeInstanceOrNull(binding); + if (scope != null) { + List existing = scopeInstancesInUse.get(scope); + if (existing == null) { + existing = Lists.newArrayList(); + scopeInstancesInUse.put(scope, existing); + } + existing.add(binding.getSource()); + } + } + + return null; + } + + void rewrite(Binder binder, PrivateElements privateElements, Set> keysToSkip) { + PrivateBinder privateBinder = binder.withSource(privateElements.getSource()) + .newPrivateBinder(); + + Set> skippedExposes = Sets.newHashSet(); + + for (Key key : privateElements.getExposedKeys()) { + if (keysToSkip.remove(key)) { + skippedExposes.add(key); + } else { + privateBinder.withSource(privateElements.getExposedSource(key)).expose(key); + } + } + + for (Element element : privateElements.getElements()) { + if (element instanceof Binding + && skippedExposes.remove(((Binding) element).getKey())) { + continue; + } + if (element instanceof PrivateElements) { + rewrite(privateBinder, (PrivateElements) element, skippedExposes); + continue; + } + element.applyTo(privateBinder); + } + } + + @Override + public Void visit(PrivateElements privateElements) { + rewrite(binder, privateElements, overriddenKeys); + return null; + } + + @Override + public Void visit(ScopeBinding scopeBinding) { + scopeBindings.add(scopeBinding); + return null; + } + }.writeAll(elements); + + // execute the scope bindings, skipping scopes that have been overridden. Any scope that + // is overridden and in active use will prompt an error + new ModuleWriter(binder) { + @Override + public Void visit(ScopeBinding scopeBinding) { + ScopeBinding overideBinding = + overridesScopeAnnotations.remove(scopeBinding.getAnnotationType()); + if (overideBinding == null) { + super.visit(scopeBinding); + } else { + List usedSources = scopeInstancesInUse.get(scopeBinding.getScope()); + if (usedSources != null) { + StringBuilder sb = new StringBuilder( + "The scope for @%s is bound directly and cannot be overridden."); + sb.append("%n original binding at ").append(Errors.convert(scopeBinding.getSource())); + for (Object usedSource : usedSources) { + sb.append("%n bound directly at ").append(Errors.convert(usedSource)).append(""); + } + binder.withSource(overideBinding.getSource()) + .addError(sb.toString(), scopeBinding.getAnnotationType().getSimpleName()); + } + } + return null; + } + }.writeAll(scopeBindings); + } + + private Scope getScopeInstanceOrNull(Binding binding) { + return binding.acceptScopingVisitor(new DefaultBindingScopingVisitor() { + @Override + public Scope visitScope(Scope scope) { + return scope; + } + }); + } + } + + private static class ModuleWriter extends DefaultElementVisitor { + protected final Binder binder; + + ModuleWriter(Binder binder) { + this.binder = binder.skipSources(this.getClass()); + } + + @Override + protected Void visitOther(Element element) { + element.applyTo(binder); + return null; + } + + void writeAll(Iterable elements) { + for (Element element : elements) { + element.acceptVisitor(this); + } + } + } +} diff --git a/src/main/java/com/google/inject/util/Providers.java b/src/main/java/com/google/inject/util/Providers.java new file mode 100644 index 0000000..99e2792 --- /dev/null +++ b/src/main/java/com/google/inject/util/Providers.java @@ -0,0 +1,56 @@ +package com.google.inject.util; + +import com.google.common.base.Objects; +import com.google.inject.Provider; + +/** + * Static utility methods for creating and working with instances of + * {@link Provider}. + */ +public final class Providers { + + private Providers() { + } + + /** + * Returns a provider which always provides {@code instance}. This should not + * be necessary to use in your application, but is helpful for several types + * of unit tests. + * + * @param instance the instance that should always be provided. This is also + * permitted to be null, to enable aggressive testing, although in real + * life a Guice-supplied Provider will never return null. + */ + public static Provider of(final T instance) { + return new ConstantProvider(instance); + } + + private static final class ConstantProvider implements Provider { + private final T instance; + + private ConstantProvider(T instance) { + this.instance = instance; + } + + public T get() { + return instance; + } + + @Override + public String toString() { + return "of(" + instance + ")"; + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof ConstantProvider) + && Objects.equal(instance, ((ConstantProvider) obj).instance); + } + + @Override + public int hashCode() { + return Objects.hashCode(instance); + } + } + +} diff --git a/src/main/java/com/google/inject/util/Types.java b/src/main/java/com/google/inject/util/Types.java new file mode 100644 index 0000000..4cc390b --- /dev/null +++ b/src/main/java/com/google/inject/util/Types.java @@ -0,0 +1,116 @@ +package com.google.inject.util; + +import com.google.inject.Provider; +import com.google.inject.internal.MoreTypes; +import com.google.inject.internal.MoreTypes.GenericArrayTypeImpl; +import com.google.inject.internal.MoreTypes.ParameterizedTypeImpl; +import com.google.inject.internal.MoreTypes.WildcardTypeImpl; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Static methods for working with types. + * + */ +public final class Types { + private Types() { + } + + /** + * Returns a new parameterized type, applying {@code typeArguments} to + * {@code rawType}. The returned type does not have an owner type. + * + * @return a parameterized type. + */ + public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) { + return newParameterizedTypeWithOwner(null, rawType, typeArguments); + } + + /** + * Returns a new parameterized type, applying {@code typeArguments} to + * {@code rawType} and enclosed by {@code ownerType}. + * + * @return a parameterized type. + */ + public static ParameterizedType newParameterizedTypeWithOwner( + Type ownerType, Type rawType, Type... typeArguments) { + return new ParameterizedTypeImpl(ownerType, rawType, typeArguments); + } + + /** + * Returns an array type whose elements are all instances of + * {@code componentType}. + * + * @return a generic array type. + */ + public static GenericArrayType arrayOf(Type componentType) { + return new GenericArrayTypeImpl(componentType); + } + + /** + * Returns a type that represents an unknown type that extends {@code bound}. + * For example, if {@code bound} is {@code CharSequence.class}, this returns + * {@code ? extends CharSequence}. If {@code bound} is {@code Object.class}, + * this returns {@code ?}, which is shorthand for {@code ? extends Object}. + */ + public static WildcardType subtypeOf(Type bound) { + return new WildcardTypeImpl(new Type[]{bound}, MoreTypes.EMPTY_TYPE_ARRAY); + } + + /** + * Returns a type that represents an unknown supertype of {@code bound}. For + * example, if {@code bound} is {@code String.class}, this returns {@code ? + * super String}. + */ + public static WildcardType supertypeOf(Type bound) { + return new WildcardTypeImpl(new Type[]{Object.class}, new Type[]{bound}); + } + + /** + * Returns a type modelling a {@link List} whose elements are of type + * {@code elementType}. + * + * @return a parameterized type. + */ + public static ParameterizedType listOf(Type elementType) { + return newParameterizedType(List.class, elementType); + } + + /** + * Returns a type modelling a {@link Set} whose elements are of type + * {@code elementType}. + * + * @return a parameterized type. + */ + public static ParameterizedType setOf(Type elementType) { + return newParameterizedType(Set.class, elementType); + } + + /** + * Returns a type modelling a {@link Map} whose keys are of type + * {@code keyType} and whose values are of type {@code valueType}. + * + * @return a parameterized type. + */ + public static ParameterizedType mapOf(Type keyType, Type valueType) { + return newParameterizedType(Map.class, keyType, valueType); + } + + // for other custom collections types, use newParameterizedType() + + /** + * Returns a type modelling a {@link Provider} that provides elements of type + * {@code elementType}. + * + * @return a parameterized type. + */ + public static ParameterizedType providerOf(Type providedType) { + return newParameterizedType(Provider.class, providedType); + } +} \ No newline at end of file diff --git a/src/test/java/com/google/inject/Asserts.java b/src/test/java/com/google/inject/Asserts.java new file mode 100644 index 0000000..165172c --- /dev/null +++ b/src/test/java/com/google/inject/Asserts.java @@ -0,0 +1,191 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.inject; + +import static com.google.inject.internal.InternalFlags.IncludeStackTraceOption; +import static com.google.inject.internal.InternalFlags.getIncludeStackTraceOption; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.testing.GcFinalization; + +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class Asserts { + private Asserts() {} + + /** + * Returns the String that would appear in an error message for this chain of classes + * as modules. + */ + public static String asModuleChain(Class... classes) { + return Joiner.on(" -> ").appendTo(new StringBuilder(" (via modules: "), + Iterables.transform(ImmutableList.copyOf(classes), new Function() { + @Override + public String apply(Class input) { + return input.getName(); + } + })).append(")").toString(); + } + + /** + * Returns the source file appears in error messages + */ + public static String getDeclaringSourcePart(Class clazz) { + if (getIncludeStackTraceOption() == IncludeStackTraceOption.OFF) { + return ".configure(Unknown Source"; + } + return ".configure(" + clazz.getSimpleName() + ".java:"; + } + + /** + * Returns true if {@link com.google.inject.internal.InternalFlags#getIncludeStackTraceOption()} returns {@link + * IncludeStackTraceOption#OFF}. + */ + public static boolean isIncludeStackTraceOff() { + return getIncludeStackTraceOption() == IncludeStackTraceOption.OFF; + } + + /** + * Returns true if {@link com.google.inject.internal.InternalFlags#getIncludeStackTraceOption()} returns {@link + * IncludeStackTraceOption#COMPLETE}. + */ + public static boolean isIncludeStackTraceComplete() { + return getIncludeStackTraceOption() == IncludeStackTraceOption.COMPLETE; + } + + /** + * Fails unless {@code expected.equals(actual)}, {@code + * actual.equals(expected)} and their hash codes are equal. This is useful + * for testing the equals method itself. + */ + public static void assertEqualsBothWays(Object expected, Object actual) { + assertNotNull(expected); + assertNotNull(actual); + assertEquals("expected.equals(actual)", actual, expected); + assertEquals("actual.equals(expected)", expected, actual); + assertEquals("hashCode", expected.hashCode(), actual.hashCode()); + } + + /** + * Fails unless {@code text} includes all {@code substrings}, in order. + */ + public static void assertContains(String text, String... substrings) { + + int startingFrom = 0; + for (String substring : substrings) { + int index = text.indexOf(substring, startingFrom); + assertTrue(String.format("Expected \"%s\" to contain substring \"%s\"", text, substring), + index >= startingFrom); + startingFrom = index + substring.length(); + } + + String lastSubstring = substrings[substrings.length - 1]; + assertTrue(String.format("Expected \"%s\" to contain substring \"%s\" only once),", + text, lastSubstring), text.indexOf(lastSubstring, startingFrom) == -1); + } + + /** + * Fails unless {@code object} doesn't equal itself when reserialized. + */ + public static void assertEqualWhenReserialized(Object object) + throws IOException { + //Object reserialized = reserialize(object); + //assertEquals(object, reserialized); + //assertEquals(object.hashCode(), reserialized.hashCode()); + } + + /** + * Fails unless {@code object} has the same toString value when reserialized. + */ + public static void assertSimilarWhenReserialized(Object object) throws IOException { + //Object reserialized = reserialize(object); + //assertEquals(object.toString(), reserialized.toString()); + } + + /*public static E reserialize(E original) throws IOException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new ObjectOutputStream(out).writeObject(original); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + @SuppressWarnings("unchecked") // the reserialized type is assignable + E reserialized = (E) new ObjectInputStream(in).readObject(); + return reserialized; + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }*/ + + public static void assertNotSerializable(Object object) throws IOException { + //try { + // reserialize(object); + // Assert.fail(); + //} catch (NotSerializableException expected) { + //} + } + + public static void awaitFullGc() { + // GcFinalization *should* do it, but doesn't work well in practice... + // so we put a second latch and wait for a ReferenceQueue to tell us. + ReferenceQueue queue = new ReferenceQueue(); + WeakReference ref = new WeakReference(new Object(), queue); + GcFinalization.awaitFullGc(); + try { + assertSame("queue didn't return ref in time", ref, queue.remove(5000)); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void awaitClear(WeakReference ref) { + // GcFinalization *should* do it, but doesn't work well in practice... + // so we put a second latch and wait for a ReferenceQueue to tell us. + Object data = ref.get(); + ReferenceQueue queue = null; + WeakReference extraRef = null; + if (data != null) { + queue = new ReferenceQueue(); + extraRef = new WeakReference(data, queue); + data = null; + } + GcFinalization.awaitClear(ref); + if (queue != null) { + try { + assertSame("queue didn't return ref in time", extraRef, queue.remove(5000)); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/test/java/com/google/inject/BinderTest.java b/src/test/java/com/google/inject/BinderTest.java new file mode 100644 index 0000000..c62c155 --- /dev/null +++ b/src/test/java/com/google/inject/BinderTest.java @@ -0,0 +1,633 @@ +/** + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.assertNotSerializable; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static com.google.inject.Asserts.isIncludeStackTraceOff; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.Message; +import com.google.inject.util.Providers; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.logging.Handler; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class BinderTest extends TestCase { + + //private final Logger loggerToWatch = Logger.getLogger(Guice.class.getName()); + + /*private final List logRecords = Lists.newArrayList(); + private final Handler fakeHandler = new Handler() { + @Override + public void publish(LogRecord logRecord) { + logRecords.add(logRecord); + } + @Override + public void flush() {} + @Override + public void close() throws SecurityException {} + };*/ + + Provider fooProvider; + + @Override protected void setUp() throws Exception { + super.setUp(); + //loggerToWatch.addHandler(fakeHandler); + } + + @Override protected void tearDown() throws Exception { + //loggerToWatch.removeHandler(fakeHandler); + super.tearDown(); + } + + public void testProviderFromBinder() { + Guice.createInjector(new Module() { + public void configure(Binder binder) { + fooProvider = binder.getProvider(Foo.class); + + try { + fooProvider.get(); + } catch (IllegalStateException e) { /* expected */ } + } + }); + + assertNotNull(fooProvider.get()); + } + + static class Foo {} + + public void testMissingBindings() { + try { + Guice.createInjector(new AbstractModule() { + @Override + public void configure() { + getProvider(Runnable.class); + bind(Comparator.class); + requireBinding(Key.get(new TypeLiteral>() {})); + bind(Date.class).annotatedWith(Names.named("date")); + } + }); + } catch (CreationException e) { + assertEquals(4, e.getErrorMessages().size()); + String segment1 = "No implementation for " + Comparator.class.getName() + " was bound."; + String segment2 = "No implementation for java.util.Date annotated with @" + + Named.class.getName() + "(value=date) was bound."; + String segment3 = "No implementation for java.lang.Runnable was bound."; + String segment4 = " No implementation for java.util.concurrent.Callable was" + + " bound."; + String atSegment = "at " + getClass().getName(); + String sourceFileName = getDeclaringSourcePart(getClass()); + if (isIncludeStackTraceOff()) { + assertContains(e.getMessage(), + segment1, atSegment, sourceFileName, + segment2, atSegment, sourceFileName, + segment3, atSegment, sourceFileName, + segment4, atSegment, sourceFileName); + } else { + assertContains(e.getMessage(), + segment3, atSegment, sourceFileName, + segment1, atSegment, sourceFileName, + segment4, atSegment, sourceFileName, + segment2, atSegment, sourceFileName); + } + } + } + + public void testMissingDependency() { + try { + Guice.createInjector(new AbstractModule() { + @Override + public void configure() { + bind(NeedsRunnable.class); + } + }); + } catch (CreationException e) { + assertEquals(1, e.getErrorMessages().size()); + assertContains(e.getMessage(), + "No implementation for java.lang.Runnable was bound.", + "for field at " + NeedsRunnable.class.getName(), ".runnable(", + "at " + getClass().getName(), getDeclaringSourcePart(getClass())); + } + } + + public static class NeedsRunnable { + @Inject Runnable runnable; + } + + public void testDanglingConstantBinding() { + try { + Guice.createInjector(new AbstractModule() { + @Override public void configure() { + bindConstant(); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Missing constant value. Please call to(...).", + "at " + getClass().getName()); + } + } + + public void testRecursiveBinding() { + try { + Guice.createInjector(new AbstractModule() { + @Override public void configure() { + bind(Runnable.class).to(Runnable.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Binding points to itself.", + "at " + getClass().getName(), getDeclaringSourcePart(getClass())); + } + } + + public void testBindingNullConstant() { + try { + Guice.createInjector(new AbstractModule() { + @Override public void configure() { + String none = null; + bindConstant().annotatedWith(Names.named("nullOne")).to(none); + bind(String.class).annotatedWith(Names.named("nullTwo")).toInstance(none); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Binding to null instances is not allowed. Use toProvider(Providers.of(null))", + "2) Binding to null instances is not allowed. Use toProvider(Providers.of(null))"); + } + } + + public void testToStringOnBinderApi() { + try { + Guice.createInjector(new AbstractModule() { + @Override public void configure() { + assertEquals("Binder", binder().toString()); + assertEquals("Provider", getProvider(Integer.class).toString()); + assertEquals("Provider>", + getProvider(Key.get(new TypeLiteral>() {})).toString()); + + assertEquals("BindingBuilder", + bind(Integer.class).toString()); + assertEquals("BindingBuilder", + bind(Integer.class).annotatedWith(Names.named("a")).toString()); + assertEquals("ConstantBindingBuilder", bindConstant().toString()); + assertEquals("ConstantBindingBuilder", + bindConstant().annotatedWith(Names.named("b")).toString()); + assertEquals("AnnotatedElementBuilder", + binder().newPrivateBinder().expose(Integer.class).toString()); + } + }); + fail(); + } catch (CreationException ignored) { + } + } + + public void testNothingIsSerializableInBinderApi() { + try { + Guice.createInjector(new AbstractModule() { + @Override public void configure() { + try { + assertNotSerializable(binder()); + assertNotSerializable(getProvider(Integer.class)); + assertNotSerializable(getProvider(Key.get(new TypeLiteral>() {}))); + assertNotSerializable(bind(Integer.class)); + assertNotSerializable(bind(Integer.class).annotatedWith(Names.named("a"))); + assertNotSerializable(bindConstant()); + assertNotSerializable(bindConstant().annotatedWith(Names.named("b"))); + } catch (IOException e) { + fail(e.getMessage()); + } + } + }); + fail(); + } catch (CreationException ignored) { + } + } + + /** + * Although {@code String[].class} isn't equal to {@code new + * GenericArrayTypeImpl(String.class)}, Guice should treat these two types + * interchangeably. + */ + public void testArrayTypeCanonicalization() { + final String[] strings = new String[] { "A" }; + final Integer[] integers = new Integer[] { 1 }; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String[].class).toInstance(strings); + bind(new TypeLiteral() {}).toInstance(integers); + } + }); + + assertSame(integers, injector.getInstance(Key.get(new TypeLiteral() {}))); + assertSame(integers, injector.getInstance(new Key() {})); + assertSame(integers, injector.getInstance(Integer[].class)); + assertSame(strings, injector.getInstance(Key.get(new TypeLiteral() {}))); + assertSame(strings, injector.getInstance(new Key() {})); + assertSame(strings, injector.getInstance(String[].class)); + + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String[].class).toInstance(new String[] { "A" }); + bind(new TypeLiteral() {}).toInstance(new String[] { "B" }); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) A binding to java.lang.String[] was already configured at " + getClass().getName(), + "at " + getClass().getName(), getDeclaringSourcePart(getClass())); + assertContains(expected.getMessage(), "1 error"); + } + + // passes because duplicates are ignored + injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String[].class).toInstance(strings); + bind(new TypeLiteral() {}).toInstance(strings); + } + }); + assertSame(strings, injector.getInstance(Key.get(new TypeLiteral() {}))); + assertSame(strings, injector.getInstance(new Key() {})); + assertSame(strings, injector.getInstance(String[].class)); + } + + static class ParentModule extends AbstractModule { + @Override protected void configure() { + install(new FooModule()); + install(new BarModule()); + } + } + static class FooModule extends AbstractModule { + @Override protected void configure() { + install(new ConstantModule("foo")); + } + } + static class BarModule extends AbstractModule { + @Override protected void configure() { + install(new ConstantModule("bar")); + } + } + static class ConstantModule extends AbstractModule { + private final String constant; + ConstantModule(String constant) { + this.constant = constant; + } + @Override protected void configure() { + bind(String.class).toInstance(constant); + } + } + + /** + * Binding something to two different things should give an error. + */ + public void testSettingBindingTwice() { + try { + Guice.createInjector(new ParentModule()); + fail(); + } catch(CreationException expected) { + assertContains(expected.getMessage(), + "1) A binding to java.lang.String was already configured at " + ConstantModule.class.getName(), + asModuleChain(ParentModule.class, FooModule.class, ConstantModule.class), + "at " + ConstantModule.class.getName(), getDeclaringSourcePart(getClass()), + asModuleChain(ParentModule.class, BarModule.class, ConstantModule.class)); + assertContains(expected.getMessage(), "1 error"); + } + } + + /** + * Binding an @ImplementedBy thing to something else should also fail. + */ + public void testSettingAtImplementedByTwice() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(HasImplementedBy1.class); + bind(HasImplementedBy1.class).toInstance(new HasImplementedBy1() {}); + } + }); + fail(); + } catch(CreationException expected) { + expected.printStackTrace(); + assertContains(expected.getMessage(), + "1) A binding to " + HasImplementedBy1.class.getName() + + " was already configured at " + getClass().getName(), + "at " + getClass().getName(), getDeclaringSourcePart(getClass())); + assertContains(expected.getMessage(), "1 error"); + } + } + + /** + * See issue 614, Problem One + * https://github.com/google/guice/issues/614 + */ + public void testJitDependencyDoesntBlockOtherExplicitBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(HasImplementedByThatNeedsAnotherImplementedBy.class); + bind(HasImplementedBy1.class).toInstance(new HasImplementedBy1() {}); + } + }); + injector.getAllBindings(); // just validate it doesn't throw. + // Also validate that we're using the explicit (and not @ImplementedBy) implementation + assertFalse(injector.getInstance(HasImplementedBy1.class) instanceof ImplementsHasImplementedBy1); + } + + /** + * See issue 614, Problem Two + * https://github.com/google/guice/issues/id=614 + */ + public void testJitDependencyCanUseExplicitDependencies() { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(HasImplementedByThatWantsExplicit.class); + bind(JustAnInterface.class).toInstance(new JustAnInterface() {}); + } + }); + } + + /** + * Untargetted bindings should follow @ImplementedBy and @ProvidedBy + * annotations if they exist. Otherwise the class should be constructed + * directly. + */ + public void testUntargettedBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(HasProvidedBy1.class); + bind(HasImplementedBy1.class); + bind(HasProvidedBy2.class); + bind(HasImplementedBy2.class); + bind(JustAClass.class); + } + }); + + assertNotNull(injector.getInstance(HasProvidedBy1.class)); + assertNotNull(injector.getInstance(HasImplementedBy1.class)); + assertNotSame(HasProvidedBy2.class, + injector.getInstance(HasProvidedBy2.class).getClass()); + assertSame(ExtendsHasImplementedBy2.class, + injector.getInstance(HasImplementedBy2.class).getClass()); + assertSame(JustAClass.class, injector.getInstance(JustAClass.class).getClass()); + } + + public void testPartialInjectorGetInstance() { + Injector injector = Guice.createInjector(); + try { + injector.getInstance(MissingParameter.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) Could not find a suitable constructor in " + NoInjectConstructor.class.getName(), + "at " + MissingParameter.class.getName() + ".("); + } + } + + public void testUserReportedError() { + final Message message = new Message(getClass(), "Whoops!"); + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + addError(message); + } + }); + fail(); + } catch (CreationException expected) { + assertSame(message, Iterables.getOnlyElement(expected.getErrorMessages())); + } + } + + public void testUserReportedErrorsAreAlsoLogged() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + addError(new Message("Whoops!", new IllegalArgumentException())); + } + }); + fail(); + } catch (CreationException expected) { + } + + /*LogRecord logRecord = Iterables.getOnlyElement(this.logRecords); + assertContains(logRecord.getMessage(), + "An exception was caught and reported. Message: java.lang.IllegalArgumentException"); + */ + } + + public void testBindingToProvider() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>() {}).toInstance(Providers.of("A")); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Binding to Provider is not allowed.", + "at " + BinderTest.class.getName(), getDeclaringSourcePart(getClass())); + } + } + + static class OuterCoreModule extends AbstractModule { + @Override protected void configure() { + install(new InnerCoreModule()); + } + } + static class InnerCoreModule extends AbstractModule { + final Named red = Names.named("red"); + + @Override protected void configure() { + bind(AbstractModule.class).annotatedWith(red) + .toProvider(Providers.of(null)); + bind(Binder.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Binding.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Injector.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Key.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Module.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Provider.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Scope.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(Stage.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(TypeLiteral.class).annotatedWith(red).toProvider(Providers.of(null)); + bind(new TypeLiteral>() {}).toProvider(Providers.>of(null)); + } + } + public void testCannotBindToGuiceTypes() { + try { + Guice.createInjector(new OuterCoreModule()); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Binding to core guice framework type is not allowed: AbstractModule.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Binder.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Binding.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Injector.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Key.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Module.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to Provider is not allowed.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Scope.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Stage.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: TypeLiteral.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class), + + "Binding to core guice framework type is not allowed: Key.", + "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterCoreModule.class, InnerCoreModule.class)); + } + } + + static class MissingParameter { + @Inject MissingParameter(NoInjectConstructor noInjectConstructor) {} + } + + static class NoInjectConstructor { + private NoInjectConstructor() {} + } + + @ProvidedBy(HasProvidedBy1Provider.class) + interface HasProvidedBy1 {} + + static class HasProvidedBy1Provider implements Provider { + public HasProvidedBy1 get() { + return new HasProvidedBy1() {}; + } + } + + @ImplementedBy(ImplementsHasImplementedBy1.class) + interface HasImplementedBy1 {} + + static class ImplementsHasImplementedBy1 implements HasImplementedBy1 {} + + @ProvidedBy(HasProvidedBy2Provider.class) + static class HasProvidedBy2 {} + + static class HasProvidedBy2Provider implements Provider { + public HasProvidedBy2 get() { + return new HasProvidedBy2() {}; + } + } + + @ImplementedBy(ExtendsHasImplementedBy2.class) + static class HasImplementedBy2 {} + + static class ExtendsHasImplementedBy2 extends HasImplementedBy2 {} + + static class JustAClass {} + + @ImplementedBy(ImplementsHasImplementedByThatNeedsAnotherImplementedBy.class) + static interface HasImplementedByThatNeedsAnotherImplementedBy { + } + + static class ImplementsHasImplementedByThatNeedsAnotherImplementedBy + implements HasImplementedByThatNeedsAnotherImplementedBy { + @Inject + ImplementsHasImplementedByThatNeedsAnotherImplementedBy( + HasImplementedBy1 h1n1) {} + } + + @ImplementedBy(ImplementsHasImplementedByThatWantsExplicit.class) + static interface HasImplementedByThatWantsExplicit { + } + + static class ImplementsHasImplementedByThatWantsExplicit + implements HasImplementedByThatWantsExplicit { + @Inject ImplementsHasImplementedByThatWantsExplicit(JustAnInterface jai) {} + } + + static interface JustAnInterface {} + + +// public void testBindInterfaceWithoutImplementation() { +// Guice.createInjector(new AbstractModule() { +// protected void configure() { +// bind(Runnable.class); +// } +// }).getInstance(Runnable.class); +// } + + enum Roshambo { ROCK, SCISSORS, PAPER } + + public void testInjectRawProvider() { + try { + Guice.createInjector().getInstance(Provider.class); + fail(); + } catch (ConfigurationException expected) { + Asserts.assertContains(expected.getMessage(), + "1) Cannot inject a Provider that has no type parameter", + "while locating " + Provider.class.getName()); + } + } +} diff --git a/src/test/java/com/google/inject/BinderTestSuite.java b/src/test/java/com/google/inject/BinderTestSuite.java new file mode 100644 index 0000000..75f11a0 --- /dev/null +++ b/src/test/java/com/google/inject/BinderTestSuite.java @@ -0,0 +1,798 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.ScopedBindingBuilder; +import com.google.inject.name.Named; +import com.google.inject.util.Providers; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class BinderTestSuite extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite(); + + new Builder() + .name("bind A") + .module(new AbstractModule() { + protected void configure() { + bind(A.class); + } + }) + .creationException("No implementation for %s was bound", A.class.getName()) + .addToSuite(suite); + + new Builder() + .name("bind PlainA named apple") + .module(new AbstractModule() { + protected void configure() { + bind(PlainA.class).annotatedWith(named("apple")); + } + }) + .creationException("No implementation for %s annotated with %s was bound", + PlainA.class.getName(), named("apple")) + .addToSuite(suite); + + new Builder() + .name("bind A to new PlainA(1)") + .module(new AbstractModule() { + protected void configure() { + bind(A.class).toInstance(new PlainA(1)); + } + }) + .creationTime(CreationTime.NONE) + .expectedValues(new PlainA(1), new PlainA(1), new PlainA(1)) + .addToSuite(suite); + + new Builder() + .name("no binding, AWithProvidedBy") + .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class) + .addToSuite(suite); + + new Builder() + .name("no binding, AWithImplementedBy") + .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class) + .addToSuite(suite); + + new Builder() + .name("no binding, ScopedA") + .key(Key.get(ScopedA.class), InjectsScopedA.class) + .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)) + .addToSuite(suite); + + new Builder() + .name("no binding, AWithProvidedBy named apple") + .key(Key.get(AWithProvidedBy.class, named("apple")), + InjectsAWithProvidedByNamedApple.class) + .configurationException("No implementation for %s annotated with %s was bound", + AWithProvidedBy.class.getName(), named("apple")) + .addToSuite(suite); + + new Builder() + .name("no binding, AWithImplementedBy named apple") + .key(Key.get(AWithImplementedBy.class, named("apple")), + InjectsAWithImplementedByNamedApple.class) + .configurationException("No implementation for %s annotated with %s was bound", + AWithImplementedBy.class.getName(), named("apple")) + .addToSuite(suite); + + new Builder() + .name("no binding, ScopedA named apple") + .key(Key.get(ScopedA.class, named("apple")), InjectsScopedANamedApple.class) + .configurationException("No implementation for %s annotated with %s was bound", + ScopedA.class.getName(), named("apple")) + .addToSuite(suite); + + for (final Scoper scoper : Scoper.values()) { + new Builder() + .name("bind PlainA") + .key(Key.get(PlainA.class), InjectsPlainA.class) + .module(new AbstractModule() { + protected void configure() { + AnnotatedBindingBuilder abb = bind(PlainA.class); + scoper.configure(abb); + } + }) + .scoper(scoper) + .addToSuite(suite); + + new Builder() + .name("bind A to PlainA") + .module(new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(A.class).to(PlainA.class); + scoper.configure(sbb); + } + }) + .scoper(scoper) + .addToSuite(suite); + + new Builder() + .name("bind A to PlainAProvider.class") + .module(new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(A.class).toProvider(PlainAProvider.class); + scoper.configure(sbb); + } + }) + .scoper(scoper) + .addToSuite(suite); + + new Builder() + .name("bind A to new PlainAProvider()") + .module(new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(A.class).toProvider(new PlainAProvider()); + scoper.configure(sbb); + } + }) + .scoper(scoper) + .addToSuite(suite); + + new Builder() + .name("bind AWithProvidedBy") + .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class) + .module(new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(AWithProvidedBy.class); + scoper.configure(sbb); + } + }) + .scoper(scoper) + .addToSuite(suite); + + new Builder() + .name("bind AWithImplementedBy") + .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class) + .module(new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(AWithImplementedBy.class); + scoper.configure(sbb); + } + }) + .scoper(scoper) + .addToSuite(suite); + + new Builder() + .name("bind ScopedA") + .key(Key.get(ScopedA.class), InjectsScopedA.class) + .module(new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(ScopedA.class); + scoper.configure(sbb); + } + }) + .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)) + .scoper(scoper) + .addToSuite(suite); + + + new Builder() + .name("bind AWithProvidedBy named apple") + .module(new AbstractModule() { + protected void configure() { + scoper.configure(bind(AWithProvidedBy.class).annotatedWith(named("apple"))); + } + }) + .creationException("No implementation for %s annotated with %s was bound", + AWithProvidedBy.class.getName(), named("apple")) + .addToSuite(suite); + + new Builder() + .name("bind AWithImplementedBy named apple") + .module(new AbstractModule() { + protected void configure() { + scoper.configure(bind(AWithImplementedBy.class).annotatedWith(named("apple"))); + } + }) + .creationException("No implementation for %s annotated with %s was bound", + AWithImplementedBy.class.getName(), named("apple")) + .addToSuite(suite); + + new Builder() + .name("bind ScopedA named apple") + .module(new AbstractModule() { + protected void configure() { + scoper.configure(bind(ScopedA.class).annotatedWith(named("apple"))); + } + }) + .creationException("No implementation for %s annotated with %s was bound", + ScopedA.class.getName(), named("apple")) + .addToSuite(suite); + + } + + return suite; + } + + enum Scoper { + UNSCOPED { + void configure(ScopedBindingBuilder sbb) {} + void apply(Builder builder) {} + }, + + EAGER_SINGLETON { + void configure(ScopedBindingBuilder sbb) { + sbb.asEagerSingleton(); + } + void apply(Builder builder) { + builder.expectedValues(new PlainA(101), new PlainA(101), new PlainA(101)); + builder.creationTime(CreationTime.EAGER); + } + }, + + SCOPES_SINGLETON { + void configure(ScopedBindingBuilder sbb) { + sbb.in(Scopes.SINGLETON); + } + void apply(Builder builder) { + builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201)); + } + }, + + SINGLETON_DOT_CLASS { + void configure(ScopedBindingBuilder sbb) { + sbb.in(Singleton.class); + } + void apply(Builder builder) { + builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201)); + } + }, + + TWO_AT_A_TIME_SCOPED_DOT_CLASS { + void configure(ScopedBindingBuilder sbb) { + sbb.in(TwoAtATimeScoped.class); + } + void apply(Builder builder) { + builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)); + } + }, + + TWO_AT_A_TIME_SCOPE { + void configure(ScopedBindingBuilder sbb) { + sbb.in(new TwoAtATimeScope()); + } + void apply(Builder builder) { + builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)); + } + }; + + abstract void configure(ScopedBindingBuilder sbb); + abstract void apply(Builder builder); + } + + /** When Guice creates a value, directly or via a provider */ + enum CreationTime { + NONE, EAGER, LAZY + } + + public static class Builder { + private String name = "test"; + private Key key = Key.get(A.class); + private Class injectsKey = InjectsA.class; + private List modules = Lists.newArrayList(new AbstractModule() { + protected void configure() { + bindScope(TwoAtATimeScoped.class, new TwoAtATimeScope()); + } + }); + private List expectedValues = Lists.newArrayList( + new PlainA(201), new PlainA(202), new PlainA(203)); + private CreationTime creationTime = CreationTime.LAZY; + private String creationException; + private String configurationException; + + public Builder module(Module module) { + this.modules.add(module); + return this; + } + + public Builder creationTime(CreationTime creationTime) { + this.creationTime = creationTime; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder key(Key key, Class injectsKey) { + this.key = key; + this.injectsKey = injectsKey; + return this; + } + + private Builder creationException(String message, Object... args) { + this.creationException = String.format(message, args); + return this; + } + + private Builder configurationException(String message, Object... args) { + configurationException = String.format(message, args); + return this; + } + + private Builder scoper(Scoper scoper) { + name(name + " in " + scoper); + scoper.apply(this); + return this; + } + + private Builder expectedValues(T... values) { + this.expectedValues.clear(); + Collections.addAll(this.expectedValues, values); + return this; + } + + public void addToSuite(TestSuite suite) { + if (creationException != null) { + suite.addTest(new CreationExceptionTest().set(this)); + + } else if (configurationException != null) { + suite.addTest(new ConfigurationExceptionTest().set(this)); + + } else { + suite.addTest(new SuccessTest().set(this)); + //if (creationTime != CreationTime.NONE) { + // suite.addTest(new UserExceptionsTest().set(this)); + //} + } + } + } + + public static class SuccessTest extends TestCase { + String name; + Key key; + Class injectsKey; + ImmutableList modules; + ImmutableList expectedValues; + + public SuccessTest() { + super("test"); + } + + public SuccessTest set(Builder builder) { + name = builder.name; + key = builder.key; + injectsKey = builder.injectsKey; + modules = ImmutableList.copyOf(builder.modules); + expectedValues = ImmutableList.copyOf(builder.expectedValues); + return this; + } + + public String getName() { + return name; + } + + Injector newInjector() { + nextId.set(101); + return Guice.createInjector(modules); + } + + public void test() throws IllegalAccessException, InstantiationException { + if (expectedValues == null) { + return; + } + Injector injector = newInjector(); + nextId.set(201); + for (Object value : expectedValues) { + assertEquals(value, injector.getInstance(key)); + } + + Provider provider = newInjector().getProvider(key); + nextId.set(201); + for (Object value : expectedValues) { + assertEquals(value, provider.get()); + } + + Provider bindingProvider = newInjector().getBinding(key).getProvider(); + nextId.set(201); + for (Object value : expectedValues) { + assertEquals(value, bindingProvider.get()); + } + + injector = newInjector(); + nextId.set(201); + for (Object value : expectedValues) { + Injectable instance = injector.getInstance(injectsKey); + assertEquals(value, instance.value); + } + + injector = newInjector(); + nextId.set(201); + for (Object value : expectedValues) { + Injectable injectable = injectsKey.newInstance(); + injector.injectMembers(injectable); + assertEquals(value, injectable.value); + } + + Injector injector1 = newInjector(); + nextId.set(201); + Injectable hasProvider = injector1.getInstance(injectsKey); + hasProvider.provider.get(); + nextId.set(201); + for (Object value : expectedValues) { + assertEquals(value, hasProvider.provider.get()); + } + } + } + + public static class CreationExceptionTest extends TestCase { + String name; + Key key; + ImmutableList modules; + String creationException; + + public CreationExceptionTest() { + super("test"); + } + + public CreationExceptionTest set(Builder builder) { + name = builder.name; + key = builder.key; + modules = ImmutableList.copyOf(builder.modules); + creationException = builder.creationException; + return this; + } + + public String getName() { + return "creation errors:" + name; + } + + public void test() { + if (modules == null) { + return; + } + try { + Guice.createInjector(modules); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), creationException); + } + } + } + + public static class ConfigurationExceptionTest extends TestCase { + String name; + Key key; + Class injectsKey; + ImmutableList modules; + String configurationException; + + public ConfigurationExceptionTest() { + super("test"); + } + + public ConfigurationExceptionTest set(Builder builder) { + name = builder.name; + key = builder.key; + injectsKey = builder.injectsKey; + modules = ImmutableList.copyOf(builder.modules); + configurationException = builder.configurationException; + return this; + } + + public String getName() { + return "provision errors:" + name; + } + + Injector newInjector() { + return Guice.createInjector(modules); + } + + public void test() throws IllegalAccessException, InstantiationException { + if (key == null) { + return; + } + + try { + newInjector().getProvider(key); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), configurationException); + } + + try { + newInjector().getBinding(key).getProvider(); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), configurationException); + } + + try { + newInjector().getInstance(key); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), configurationException); + } + + try { + newInjector().getInstance(injectsKey); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + configurationException, injectsKey.getName() + ".inject", + configurationException, injectsKey.getName() + ".inject", + "2 errors"); + } + + try { + Injectable injectable = injectsKey.newInstance(); + newInjector().injectMembers(injectable); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + configurationException, injectsKey.getName() + ".inject", + configurationException, injectsKey.getName() + ".inject", + "2 errors"); + } + } + } + + public static class UserExceptionsTest extends TestCase { + String name; + Key key; + Class injectsKey; + ImmutableList modules; + ImmutableList expectedValues; + CreationTime creationTime; + + public UserExceptionsTest() { + super("test"); + } + + public UserExceptionsTest set(Builder builder) { + name = builder.name; + key = builder.key; + injectsKey = builder.injectsKey; + modules = ImmutableList.copyOf(builder.modules); + expectedValues = ImmutableList.copyOf(builder.expectedValues); + creationTime = builder.creationTime; + return this; + } + + public String getName() { + return "provision errors:" + name; + } + + Injector newInjector() { + return Guice.createInjector(modules); + } + + public void test() throws IllegalAccessException, InstantiationException { + if (creationTime == null) { + return; + } + nextId.set(-1); + try { + newInjector(); + assertEquals(CreationTime.LAZY, creationTime); + } catch (CreationException expected) { + assertEquals(CreationTime.EAGER, creationTime); + assertContains(expected.getMessage(), "Illegal value: -1"); + return; + } + + Provider provider = newInjector().getProvider(key); + Provider bindingProvider = newInjector().getBinding(key).getProvider(); + + nextId.set(-1); + try { + newInjector().getInstance(key); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), "Illegal value: -1"); + } + + nextId.set(-1); + try { + provider.get(); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), "Illegal value: -1"); + } + + nextId.set(-1); + try { + bindingProvider.get(); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), "Illegal value: -1"); + } + + try { + nextId.set(-1); + newInjector().getInstance(injectsKey); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), "Illegal value: -1", + "for parameter 0 at " + injectsKey.getName() + ".inject"); + } + + nextId.set(201); + Injectable injectable = injectsKey.newInstance(); + try { + nextId.set(-1); + newInjector().injectMembers(injectable); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), "Illegal value: -1", + "for parameter 0 at " + injectsKey.getName() + ".inject"); + } + + nextId.set(201); + Injectable hasProvider = newInjector().getInstance(injectsKey); + hasProvider.provider.get(); + try { + nextId.set(-1); + hasProvider.provider.get(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), "Illegal value: -1"); + } + } + } + + /** negative to throw, 101... for eager singletons, 201... for everything else */ + static final AtomicInteger nextId = new AtomicInteger(); + + @ProvidedBy(PlainAProvider.class) + interface AWithProvidedBy {} + + static class InjectsAWithProvidedBy extends Injectable { + @Inject public void inject(AWithProvidedBy aWithProvidedBy, + Provider aWithProvidedByProvider) { + this.value = aWithProvidedBy; + this.provider = aWithProvidedByProvider; + } + } + + static class InjectsAWithProvidedByNamedApple extends Injectable { + @Inject public void inject(@Named("apple") AWithProvidedBy aWithProvidedBy, + @Named("apple") Provider aWithProvidedByProvider) { + this.value = aWithProvidedBy; + this.provider = aWithProvidedByProvider; + } + } + + @ImplementedBy(PlainA.class) + interface AWithImplementedBy {} + + static class InjectsAWithImplementedBy extends Injectable { + @Inject public void inject(AWithImplementedBy aWithImplementedBy, + Provider aWithImplementedByProvider) { + this.value = aWithImplementedBy; + this.provider = aWithImplementedByProvider; + } + } + + static class InjectsAWithImplementedByNamedApple extends Injectable { + @Inject public void inject(@Named("apple") AWithImplementedBy aWithImplementedBy, + @Named("apple") Provider aWithImplementedByProvider) { + this.value = aWithImplementedBy; + this.provider = aWithImplementedByProvider; + } + } + + interface A extends AWithProvidedBy, AWithImplementedBy {} + + static class InjectsA extends Injectable { + @Inject public void inject(A a, Provider aProvider) { + this.value = a; + this.provider = aProvider; + } + } + + static class PlainA implements A { + final int value; + PlainA() { + value = nextId.getAndIncrement(); + if (value < 0) { + throw new RuntimeException("Illegal value: " + value); + } + } + PlainA(int value) { + this.value = value; + } + public boolean equals(Object obj) { + return obj instanceof PlainA + && value == ((PlainA) obj).value; + } + public int hashCode() { + return value; + } + public String toString() { + return "PlainA#" + value; + } + } + + static class PlainAProvider implements Provider { + public A get() { + return new PlainA(); + } + } + + static class InjectsPlainA extends Injectable { + @Inject public void inject(PlainA plainA, Provider plainAProvider) { + this.value = plainA; + this.provider = plainAProvider; + } + } + + /** This scope hands out each value exactly twice */ + static class TwoAtATimeScope implements Scope { + public Provider scope(Key key, final Provider unscoped) { + return new Provider() { + T instance; + public T get() { + if (instance == null) { + instance = unscoped.get(); + return instance; + } else { + T result = instance; + instance = null; + return result; + } + } + }; + } + } + + @Target({ TYPE, METHOD }) @Retention(RUNTIME) @ScopeAnnotation + public @interface TwoAtATimeScoped {} + + @TwoAtATimeScoped + static class ScopedA extends PlainA {} + + static class InjectsScopedA extends Injectable { + @Inject public void inject(ScopedA scopedA, Provider scopedAProvider) { + this.value = scopedA; + this.provider = scopedAProvider; + } + } + + static class InjectsScopedANamedApple extends Injectable { + @Inject public void inject(@Named("apple") ScopedA scopedA, + @Named("apple") Provider scopedAProvider) { + this.value = scopedA; + this.provider = scopedAProvider; + } + } + + static class Injectable { + Object value = new Object(); + Provider provider = Providers.of(new Object()); + } +} diff --git a/src/test/java/com/google/inject/BindingAnnotationTest.java b/src/test/java/com/google/inject/BindingAnnotationTest.java new file mode 100644 index 0000000..b119953 --- /dev/null +++ b/src/test/java/com/google/inject/BindingAnnotationTest.java @@ -0,0 +1,169 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class BindingAnnotationTest extends TestCase { + + public void testAnnotationWithValueMatchesKeyWithTypeOnly() throws CreationException { + Injector c = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(Blue.class).to("foo"); + bind(BlueFoo.class); + } + }); + + BlueFoo foo = c.getInstance(BlueFoo.class); + + assertEquals("foo", foo.s); + } + + public void testRequireExactAnnotationsDisablesFallback() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExactBindingAnnotations(); + bindConstant().annotatedWith(Blue.class).to("foo"); + bind(BlueFoo.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "No implementation for java.lang.String annotated with", + "BindingAnnotationTest$Blue(value=5) was bound", + "at " + BindingAnnotationTest.class.getName(), + getDeclaringSourcePart(getClass())); + } + } + + public void testRequireExactAnnotationsDoesntBreakIfDefaultsExist() { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExactBindingAnnotations(); + bindConstant().annotatedWith(Red.class).to("foo"); + bind(RedFoo.class); + } + }).getInstance(RedFoo.class); + } + + public void testRequireExactAnnotationsRequireAllOptionals() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExactBindingAnnotations(); + bindConstant().annotatedWith(Color.class).to("foo"); + bind(ColorFoo.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "No implementation for java.lang.String annotated with", + "BindingAnnotationTest$Color", + "at " + BindingAnnotationTest.class.getName(), + getDeclaringSourcePart(getClass())); + } + } + + public void testAnnotationWithValueThatDoesntMatch() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(createBlue(6)).to("six"); + bind(String.class).toInstance("bar"); + bind(BlueFoo.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "No implementation for java.lang.String annotated with", + "BindingAnnotationTest$Blue(value=5) was bound", + "at " + BindingAnnotationTest.class.getName(), + getDeclaringSourcePart(getClass())); + } + } + + static class BlueFoo { + @Inject @Blue(5) String s; + } + + static class RedFoo { + @Inject @Red String s; + } + + static class ColorFoo { + @Inject @Color(b=2) String s; + } + + @Retention(RUNTIME) + @BindingAnnotation + @interface Blue { + int value(); + } + + @Retention(RUNTIME) + @BindingAnnotation + @interface Red { + int r() default 42; + int g() default 42; + int b() default 42; + } + + @Retention(RUNTIME) + @BindingAnnotation + @interface Color { + int r() default 0; + int g() default 0; + int b(); + } + + public Blue createBlue(final int value) { + return new Blue() { + public int value() { + return value; + } + + public Class annotationType() { + return Blue.class; + } + + @Override public boolean equals(Object o) { + return o instanceof Blue + && ((Blue) o).value() == value; + } + + @Override public int hashCode() { + return 127 * "value".hashCode() ^ value; + } + }; + } +} diff --git a/src/test/java/com/google/inject/BindingOrderTest.java b/src/test/java/com/google/inject/BindingOrderTest.java new file mode 100644 index 0000000..6754cad --- /dev/null +++ b/src/test/java/com/google/inject/BindingOrderTest.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + + +import junit.framework.TestCase; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ + +public class BindingOrderTest extends TestCase { + + public void testBindingOutOfOrder() { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(BoundFirst.class); + bind(BoundSecond.class).to(BoundSecondImpl.class); + } + }); + } + + public static class BoundFirst { + @Inject public BoundFirst(BoundSecond boundSecond) { } + } + + interface BoundSecond { } + static class BoundSecondImpl implements BoundSecond { } + + public void testBindingOrderAndScopes() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class); + bind(B.class).asEagerSingleton(); + } + }); + + assertSame(injector.getInstance(A.class).b, injector.getInstance(A.class).b); + } + + public void testBindingWithExtraThreads() throws InterruptedException { + final CountDownLatch ready = new CountDownLatch(1); + final CountDownLatch done = new CountDownLatch(1); + final AtomicReference ref = new AtomicReference(); + + final Object createsAThread = new Object() { + @Inject void createAnotherThread(final Injector injector) { + new Thread() { + public void run() { + ready.countDown(); + A a = injector.getInstance(A.class); + ref.set(a.b); + done.countDown(); + } + }.start(); + + // to encourage collisions, we make sure the other thread is running before returning + try { + ready.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + + Guice.createInjector(new AbstractModule() { + protected void configure() { + requestInjection(createsAThread); + bind(A.class).toInstance(new A()); + } + }); + + done.await(); + assertNotNull(ref.get()); + } + + static class A { + @Inject B b; + } + + static class B { } +} diff --git a/src/test/java/com/google/inject/BindingTest.java b/src/test/java/com/google/inject/BindingTest.java new file mode 100644 index 0000000..e574b90 --- /dev/null +++ b/src/test/java/com/google/inject/BindingTest.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.name.Names.named; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.Runnables; +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; + +import junit.framework.TestCase; + +import java.lang.reflect.Constructor; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class BindingTest extends TestCase { + + static class Dependent { + @Inject A a; + @Inject Dependent(A a, B b) {} + @Inject void injectBob(Bob bob) {} + } + + public void testExplicitCyclicDependency() { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class); + bind(B.class); + } + }).getInstance(A.class); + } + + static class A { @Inject B b; } + static class B { @Inject A a; } + + static class Bob {} + + static class MyModule extends AbstractModule { + + protected void configure() { + // Linked. + bind(Object.class).to(Runnable.class).in(Scopes.SINGLETON); + + // Instance. + bind(Runnable.class).toInstance(Runnables.doNothing()); + + // Provider instance. + bind(Foo.class).toProvider(new Provider() { + public Foo get() { + return new Foo(); + } + }).in(Scopes.SINGLETON); + + // Provider. + bind(Foo.class) + .annotatedWith(named("provider")) + .toProvider(FooProvider.class); + + // Class. + bind(Bar.class).in(Scopes.SINGLETON); + + // Constant. + bindConstant().annotatedWith(named("name")).to("Bob"); + } + } + + static class Foo {} + + public static class FooProvider implements Provider { + public Foo get() { + throw new UnsupportedOperationException(); + } + } + + public static class Bar {} + + public void testBindToUnboundLinkedBinding() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Collection.class).to(List.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "No implementation for java.util.List was bound."); + } + } + + /** + * This test ensures that the asEagerSingleton() scoping applies to the key, + * not to what the key is linked to. + */ + public void testScopeIsAppliedToKeyNotTarget() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Integer.class).toProvider(Counter.class).asEagerSingleton(); + bind(Number.class).toProvider(Counter.class).asEagerSingleton(); + } + }); + + assertNotSame(injector.getInstance(Integer.class), injector.getInstance(Number.class)); + } + + static class Counter implements Provider { + static AtomicInteger next = new AtomicInteger(1); + public Integer get() { + return next.getAndIncrement(); + } + } + + public void testAnnotatedNoArgConstructor() { + assertBindingSucceeds(PublicNoArgAnnotated.class); + assertBindingSucceeds(ProtectedNoArgAnnotated.class); + assertBindingSucceeds(PackagePrivateNoArgAnnotated.class); + assertBindingSucceeds(PrivateNoArgAnnotated.class); + } + + static class PublicNoArgAnnotated { + @Inject public PublicNoArgAnnotated() { } + } + + static class ProtectedNoArgAnnotated { + @Inject protected ProtectedNoArgAnnotated() { } + } + + static class PackagePrivateNoArgAnnotated { + @Inject PackagePrivateNoArgAnnotated() { } + } + + static class PrivateNoArgAnnotated { + @Inject private PrivateNoArgAnnotated() { } + } + + public void testUnannotatedNoArgConstructor() throws Exception{ + assertBindingSucceeds(PublicNoArg.class); + assertBindingSucceeds(ProtectedNoArg.class); + assertBindingSucceeds(PackagePrivateNoArg.class); + assertBindingSucceeds(PrivateNoArgInPrivateClass.class); + assertBindingFails(PrivateNoArg.class); + } + + static class PublicNoArg { + public PublicNoArg() { } + } + + static class ProtectedNoArg { + protected ProtectedNoArg() { } + } + + static class PackagePrivateNoArg { + PackagePrivateNoArg() { } + } + + private static class PrivateNoArgInPrivateClass { + PrivateNoArgInPrivateClass() { } + } + + static class PrivateNoArg { + private PrivateNoArg() { } + } + + private void assertBindingSucceeds(final Class clazz) { + assertNotNull(Guice.createInjector().getInstance(clazz)); + } + + private void assertBindingFails(final Class clazz) throws NoSuchMethodException { + try { + Guice.createInjector().getInstance(clazz); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "Could not find a suitable constructor in " + PrivateNoArg.class.getName(), + "at " + PrivateNoArg.class.getName() + ".class("); + } + } + + public void testTooManyConstructors() { + try { + Guice.createInjector().getInstance(TooManyConstructors.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + TooManyConstructors.class.getName() + " has more than one constructor annotated with " + + "@Inject. Classes must have either one (and only one) constructor", + "at " + TooManyConstructors.class.getName() + ".class("); + } + } + + static class TooManyConstructors { + @Inject TooManyConstructors(Injector i) {} + @Inject TooManyConstructors() {} + } + + public void testToConstructorBinding() throws NoSuchMethodException { + final Constructor constructor = D.class.getConstructor(Stage.class); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Object.class).toConstructor(constructor); + } + }); + + D d = (D) injector.getInstance(Object.class); + assertEquals(Stage.DEVELOPMENT, d.stage); + } + + public void testToConstructorBindingsOnParameterizedTypes() throws NoSuchMethodException { + final Constructor constructor = C.class.getConstructor(Stage.class, Object.class); + final Key s = new Key(named("s")) {}; + final Key i = new Key(named("i")) {}; + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(s).toConstructor(constructor, new TypeLiteral>() {}); + bind(i).toConstructor(constructor, new TypeLiteral>() {}); + } + }); + + C one = (C) injector.getInstance(s); + assertEquals(Stage.DEVELOPMENT, one.stage); + assertEquals(Stage.DEVELOPMENT, one.t); + assertEquals(Stage.DEVELOPMENT, one.anotherT); + + C two = (C) injector.getInstance(i); + assertEquals(Stage.DEVELOPMENT, two.stage); + assertEquals(injector, two.t); + assertEquals(injector, two.anotherT); + } + + public void testToConstructorBindingsFailsOnRawTypes() throws NoSuchMethodException { + final Constructor constructor = C.class.getConstructor(Stage.class, Object.class); + + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Object.class).toConstructor(constructor); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) T cannot be used as a key; It is not fully specified.", + "at " + C.class.getName() + ".(", + "2) T cannot be used as a key; It is not fully specified.", + "at " + C.class.getName() + ".anotherT("); + } + } + + public void testInaccessibleConstructor() throws NoSuchMethodException { + final Constructor constructor = E.class.getDeclaredConstructor(Stage.class); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(E.class).toConstructor(constructor); + } + }); + + E e = injector.getInstance(E.class); + assertEquals(Stage.DEVELOPMENT, e.stage); + } + + public void testToConstructorAndScopes() throws NoSuchMethodException { + final Constructor constructor = F.class.getConstructor(Stage.class); + + final Key d = Key.get(Object.class, named("D")); // default scoping + final Key s = Key.get(Object.class, named("S")); // singleton + final Key n = Key.get(Object.class, named("N")); // "N" instances + final Key r = Key.get(Object.class, named("R")); // a regular binding + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(d).toConstructor(constructor); + bind(s).toConstructor(constructor).in(Singleton.class); + bind(n).toConstructor(constructor).in(Scopes.NO_SCOPE); + bind(r).to(F.class); + } + }); + + assertDistinct(injector, 1, d, d, d, d); + assertDistinct(injector, 1, s, s, s, s); + assertDistinct(injector, 4, n, n, n, n); + assertDistinct(injector, 1, r, r, r, r); + assertDistinct(injector, 4, d, d, r, r, s, s, n); + } + + public void assertDistinct(Injector injector, int expectedCount, Key... keys) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Key k : keys) { + builder.add(injector.getInstance(k)); + } + assertEquals(expectedCount, builder.build().size()); + } + + public void testToConstructorSpiData() throws NoSuchMethodException { + final Set> heardTypes = Sets.newHashSet(); + + final Constructor constructor = D.class.getConstructor(Stage.class); + final TypeListener listener = new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + if (!heardTypes.add(type)) { + fail("Heard " + type + " multiple times!"); + } + } + }; + + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Object.class).toConstructor(constructor); + bind(D.class).toConstructor(constructor); + bindListener(Matchers.any(), listener); + } + }); + + assertEquals(ImmutableSet.of(TypeLiteral.get(D.class)), heardTypes); + } + + public void testInterfaceToImplementationConstructor() throws NoSuchMethodException { + final Constructor constructor = CFoo.class.getDeclaredConstructor(); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(IFoo.class).toConstructor(constructor); + } + }); + + injector.getInstance(IFoo.class); + } + + public static interface IFoo {} + public static class CFoo implements IFoo {} + + public void testGetAllBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(D.class).toInstance(new D(Stage.PRODUCTION)); + bind(Object.class).to(D.class); + getProvider(new Key>() {}); + } + }); + + Map,Binding> bindings = injector.getAllBindings(); + assertEquals(ImmutableSet.of(Key.get(Injector.class), Key.get(Stage.class), Key.get(D.class), + Key.get(Object.class), new Key>() {}), + bindings.keySet()); + + // add a JIT binding + injector.getInstance(F.class); + + Map,Binding> bindings2 = injector.getAllBindings(); + assertEquals(ImmutableSet.of(Key.get(Injector.class), Key.get(Stage.class), Key.get(D.class), + Key.get(Object.class), new Key>() {}, Key.get(F.class)), + bindings2.keySet()); + + // the original map shouldn't have changed + assertEquals(ImmutableSet.of(Key.get(Injector.class), Key.get(Stage.class), Key.get(D.class), + Key.get(Object.class), new Key>() {}), + bindings.keySet()); + + // check the bindings' values + assertEquals(injector, bindings.get(Key.get(Injector.class)).getProvider().get()); + } + + public void testGetAllServletBindings() throws Exception { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(F.class); // an explicit binding that uses a JIT binding for a constructor + } + }); + injector.getAllBindings(); + } + + public static class C { + private Stage stage; + private T t; + @Inject T anotherT; + + public C(Stage stage, T t) { + this.stage = stage; + this.t = t; + } + + @Inject C() {} + } + + public static class D { + Stage stage; + public D(Stage stage) { + this.stage = stage; + } + } + + private static class E { + Stage stage; + private E(Stage stage) { + this.stage = stage; + } + } + + @Singleton + public static class F { + Stage stage; + @Inject public F(Stage stage) { + this.stage = stage; + } + } + + public void testTurkeyBaconProblemUsingToConstuctor() { + Injector injector = Guice.createInjector(new AbstractModule() { + @SuppressWarnings("unchecked") + @Override + public void configure() { + bind(Bacon.class).to(UncookedBacon.class); + bind(Bacon.class).annotatedWith(named("Turkey")).to(TurkeyBacon.class); + bind(Bacon.class).annotatedWith(named("Cooked")).toConstructor( + (Constructor)InjectionPoint.forConstructorOf(Bacon.class).getMember()); + } + }); + Bacon bacon = injector.getInstance(Bacon.class); + assertEquals(Food.PORK, bacon.getMaterial()); + assertFalse(bacon.isCooked()); + + Bacon turkeyBacon = injector.getInstance(Key.get(Bacon.class, named("Turkey"))); + assertEquals(Food.TURKEY, turkeyBacon.getMaterial()); + assertTrue(turkeyBacon.isCooked()); + + Bacon cookedBacon = injector.getInstance(Key.get(Bacon.class, named("Cooked"))); + assertEquals(Food.PORK, cookedBacon.getMaterial()); + assertTrue(cookedBacon.isCooked()); + } + + enum Food { TURKEY, PORK } + + private static class Bacon { + public Food getMaterial() { return Food.PORK; } + public boolean isCooked() { return true; } + } + + private static class TurkeyBacon extends Bacon { + public Food getMaterial() { return Food.TURKEY; } + } + + private static class UncookedBacon extends Bacon { + public boolean isCooked() { return false; } + } +} diff --git a/src/test/java/com/google/inject/BoundInstanceInjectionTest.java b/src/test/java/com/google/inject/BoundInstanceInjectionTest.java new file mode 100644 index 0000000..59fe8b2 --- /dev/null +++ b/src/test/java/com/google/inject/BoundInstanceInjectionTest.java @@ -0,0 +1,119 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.name.Named; + +import junit.framework.TestCase; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class BoundInstanceInjectionTest extends TestCase { + + public void testInstancesAreInjected() throws CreationException { + final O o = new O(); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(O.class).toInstance(o); + bind(int.class).toInstance(5); + } + }); + + assertEquals(5, o.fromMethod); + } + + static class O { + int fromMethod; + @Inject + void setInt(int i) { + this.fromMethod = i; + } + } + + public void testProvidersAreInjected() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(O.class).toProvider(new Provider() { + @Inject int i; + public O get() { + O o = new O(); + o.setInt(i); + return o; + } + }); + bind(int.class).toInstance(5); + } + }); + + assertEquals(5, injector.getInstance(O.class).fromMethod); + } + + public void testMalformedInstance() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Object.class).toInstance(new MalformedInjectable()); + } + }); + fail(); + } catch (CreationException expected) { + Asserts.assertContains(expected.getMessage(), MalformedInjectable.class.getName(), + ".doublyAnnotated() has more than one ", "annotation annotated with @BindingAnnotation: ", + Named.class.getName() + " and " + Another.class.getName()); + } + } + + public void testMalformedProvider() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toProvider(new MalformedProvider()); + } + }); + fail(); + } catch (CreationException expected) { + Asserts.assertContains(expected.getMessage(), MalformedProvider.class.getName(), + ".doublyAnnotated() has more than one ", "annotation annotated with @BindingAnnotation: ", + Named.class.getName() + " and " + Another.class.getName()); + } + } + + static class MalformedInjectable { + @Inject void doublyAnnotated(@Named("a") @Another String unused) {} + } + + static class MalformedProvider implements Provider { + @Inject void doublyAnnotated(@Named("a") @Another String s) {} + + public String get() { + return "a"; + } + } + + @BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME) + public @interface Another {} +} diff --git a/src/test/java/com/google/inject/BoundProviderTest.java b/src/test/java/com/google/inject/BoundProviderTest.java new file mode 100644 index 0000000..5fc46f4 --- /dev/null +++ b/src/test/java/com/google/inject/BoundProviderTest.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import junit.framework.TestCase; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class BoundProviderTest extends TestCase { + + public void testFooProvider() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toProvider(FooProvider.class); + } + }); + + Foo a = injector.getInstance(Foo.class); + Foo b = injector.getInstance(Foo.class); + + assertEquals(0, a.i); + assertEquals(0, b.i); + assertNotNull(a.bar); + assertNotNull(b.bar); + assertNotSame(a.bar, b.bar); + } + + public void testSingletonFooProvider() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toProvider(SingletonFooProvider.class); + } + }); + + Foo a = injector.getInstance(Foo.class); + Foo b = injector.getInstance(Foo.class); + + assertEquals(0, a.i); + assertEquals(1, b.i); + assertNotNull(a.bar); + assertNotNull(b.bar); + assertSame(a.bar, b.bar); + } + + static class Bar {} + + static class Foo { + final Bar bar; + final int i; + + Foo(Bar bar, int i) { + this.bar = bar; + this.i = i; + } + } + + static class FooProvider implements Provider { + + final Bar bar; + int count = 0; + + @Inject + public FooProvider(Bar bar) { + this.bar = bar; + } + + public Foo get() { + return new Foo(this.bar, count++); + } + } + + @Singleton + static class SingletonFooProvider implements Provider { + + final Bar bar; + int count = 0; + + @Inject + public SingletonFooProvider(Bar bar) { + this.bar = bar; + } + + public Foo get() { + return new Foo(this.bar, count++); + } + } +} diff --git a/src/test/java/com/google/inject/CircularDependencyTest.java b/src/test/java/com/google/inject/CircularDependencyTest.java new file mode 100644 index 0000000..2b67f7b --- /dev/null +++ b/src/test/java/com/google/inject/CircularDependencyTest.java @@ -0,0 +1,598 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import junit.framework.TestCase; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author crazybob@google.com (Bob Lee) + * @author sameb@google.com (Sam Berlin) + */ +public class CircularDependencyTest extends TestCase { + + @Override + protected void setUp() throws Exception { + AImpl.nextId = 0; + BImpl.nextId = 0; + } + + public void testCircularlyDependentConstructors() + throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class).to(AImpl.class); + bind(B.class).to(BImpl.class); + } + }); + assertCircularDependencies(injector); + } + + public void testCircularlyDependentConstructorsWithProviderMethods() + throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() {} + + @Provides @Singleton A a(B b) { return new AImpl(b); } + @Provides B b(A a) { return new BImpl(a); } + }); + assertCircularDependencies(injector); + } + + public void testCircularlyDependentConstructorsWithProviderInstances() + throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class).toProvider(new Provider() { + @Inject Provider bp; + public A get() { + return new AImpl(bp.get()); + } + }).in(Singleton.class); + bind(B.class).toProvider(new Provider() { + @Inject Provider ap; + public B get() { + return new BImpl(ap.get()); + } + }); + } + }); + assertCircularDependencies(injector); + } + + public void testCircularlyDependentConstructorsWithProviderKeys() + throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class).toProvider(AP.class).in(Singleton.class); + bind(B.class).toProvider(BP.class); + } + }); + assertCircularDependencies(injector); + } + + public void testCircularlyDependentConstructorsWithProvidedBy() + throws CreationException { + Injector injector = Guice.createInjector(); + assertCircularDependencies(injector); + } + + private void assertCircularDependencies(Injector injector) { + A a = injector.getInstance(A.class); + assertNotNull(a.getB().getA()); + assertEquals(0, a.id()); + assertEquals(a.id(), a.getB().getA().id()); + assertEquals(0, a.getB().id()); + assertEquals(1, AImpl.nextId); + assertEquals(1, BImpl.nextId); + assertSame(a, injector.getInstance(A.class)); + } + + @ProvidedBy(AutoAP.class) + public interface A { + B getB(); + int id(); + } + + @Singleton + static class AImpl implements A { + static int nextId; + int id = nextId++; + + final B b; + @Inject public AImpl(B b) { + this.b = b; + } + public int id() { + return id; + } + public B getB() { + return b; + } + } + + static class AP implements Provider { + @Inject Provider bp; + public A get() { + return new AImpl(bp.get()); + } + } + + @Singleton + static class AutoAP implements Provider { + @Inject Provider bp; + A a; + + public A get() { + if (a == null) { + a = new AImpl(bp.get()); + } + return a; + } + } + + @ProvidedBy(BP.class) + public interface B { + A getA(); + int id(); + } + + static class BImpl implements B { + static int nextId; + int id = nextId++; + + final A a; + @Inject public BImpl(A a) { + this.a = a; + } + public int id() { + return id; + } + public A getA() { + return a; + } + } + + static class BP implements Provider { + Provider ap; + @Inject BP(Provider ap) { + this.ap = ap; + } + public B get() { + return new BImpl(ap.get()); + } + } + + public void testUnresolvableCircularDependency() { + try { + Guice.createInjector().getInstance(C.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C.class.getName() + " to support a circular dependency, ", + "but it is not an interface."); + } + } + + public void testUnresolvableCircularDependenciesWithProviderInstances() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() {} + @Provides C c(D d) { return null; } + @Provides D d(C c) { return null; } + }).getInstance(C.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C.class.getName() + " to support a circular dependency, ", + "but it is not an interface."); + } + } + + public void testUnresolvableCircularDependenciesWithProviderKeys() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(C2.class).toProvider(C2P.class); + bind(D2.class).toProvider(D2P.class); + } + }).getInstance(C2.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C2.class.getName() + " to support a circular dependency, ", + "but it is not an interface."); + } + } + + public void testUnresolvableCircularDependenciesWithProvidedBy() { + try { + Guice.createInjector().getInstance(C2.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C2.class.getName() + " to support a circular dependency, ", + "but it is not an interface."); + } + } + + static class C { + @Inject C(D d) {} + } + static class D { + @Inject D(C c) {} + } + + static class C2P implements Provider { + @Inject Provider dp; + public C2 get() { + dp.get(); + return null; + } + } + static class D2P implements Provider { + @Inject Provider cp; + public D2 get() { + cp.get(); + return null; + } + } + @ProvidedBy(C2P.class) + static class C2 { + @Inject C2(D2 d) {} + } + @ProvidedBy(D2P.class) + static class D2 { + @Inject D2(C2 c) {} + } + + public void testDisabledCircularDependency() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().disableCircularProxies(); + } + }).getInstance(C.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C.class.getName() + " to support a circular dependency, ", + "but circular proxies are disabled."); + } + } + + public void testDisabledCircularDependenciesWithProviderInstances() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().disableCircularProxies(); + } + @Provides C c(D d) { return null; } + @Provides D d(C c) { return null; } + }).getInstance(C.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C.class.getName() + " to support a circular dependency, ", + "but circular proxies are disabled."); + } + } + + public void testDisabledCircularDependenciesWithProviderKeys() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().disableCircularProxies(); + bind(C2.class).toProvider(C2P.class); + bind(D2.class).toProvider(D2P.class); + } + }).getInstance(C2.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C2.class.getName() + " to support a circular dependency, ", + "but circular proxies are disabled."); + } + } + + public void testDisabledCircularDependenciesWithProvidedBy() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().disableCircularProxies(); + } + }).getInstance(C2.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + C2.class.getName() + " to support a circular dependency, ", + "but circular proxies are disabled."); + } + } + + /** + * As reported by issue 349, we give a lousy trace when a class is circularly + * dependent on itself in multiple ways. + */ + public void testCircularlyDependentMultipleWays() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + binder.bind(A.class).to(E.class); + binder.bind(B.class).to(E.class); + } + }); + injector.getInstance(A.class); + } + + public void testDisablingCircularProxies() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + binder().disableCircularProxies(); + binder.bind(A.class).to(E.class); + binder.bind(B.class).to(E.class); + } + }); + + try { + injector.getInstance(A.class); + fail("expected exception"); + } catch(ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + A.class.getName() + " to support a circular dependency, but circular proxies are disabled", + "Tried proxying " + B.class.getName() + " to support a circular dependency, but circular proxies are disabled"); + } + } + + @Singleton + static class E implements A, B { + @Inject + public E(A a, B b) {} + + public B getB() { + return this; + } + + public A getA() { + return this; + } + + public int id() { + return 0; + } + } + + + public void testCircularDependencyProxyDelegateNeverInitialized() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(F.class).to(RealF.class); + bind(G.class).to(RealG.class); + } + }); + F f = injector.getInstance(F.class); + assertEquals("F", f.g().f().toString()); + assertEquals("G", f.g().f().g().toString()); + + } + + public interface F { + G g(); + } + + @Singleton + public static class RealF implements F { + private final G g; + @Inject RealF(G g) { + this.g = g; + } + + public G g() { + return g; + } + + @Override public String toString() { + return "F"; + } + } + + public interface G { + F f(); + } + + @Singleton + public static class RealG implements G { + private final F f; + @Inject RealG(F f) { + this.f = f; + } + + public F f() { + return f; + } + + @Override public String toString() { + return "G"; + } + } + + /** + * Tests that ProviderInternalFactory can detect circular dependencies + * before it gets to Scopes.SINGLETON. This is especially important + * because the failure in Scopes.SINGLETON doesn't have enough context to + * provide a decent error message. + */ + public void testCircularDependenciesDetectedEarlyWhenDependenciesHaveDifferentTypes() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Number.class).to(Integer.class); + } + + @Provides @Singleton Integer provideInteger(List list) { + return new Integer(2); + } + + @Provides List provideList(Integer integer) { + return new ArrayList(); + } + }); + try { + injector.getInstance(Number.class); + fail(); + } catch(ProvisionException expected) { + assertContains(expected.getMessage(), + "Tried proxying " + Integer.class.getName() + " to support a circular dependency, ", + "but it is not an interface."); + } + } + + public void testPrivateModulesDontTriggerCircularErrorsInProviders() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new PrivateModule() { + @Override + protected void configure() { + bind(Foo.class); + expose(Foo.class); + } + @Provides String provideString(Bar bar) { + return new String("private 1, " + bar.string); + } + }); + install(new PrivateModule() { + @Override + protected void configure() { + bind(Bar.class); + expose(Bar.class); + } + @Provides String provideString() { + return new String("private 2"); + } + }); + } + }); + Foo foo = injector.getInstance(Foo.class); + assertEquals("private 1, private 2", foo.string); + } + static class Foo { + @Inject String string; + } + static class Bar { + @Inject String string; + } + + /** + * When Scope Providers call their unscoped Provider's get() methods are + * called, it's possible that the result is a circular proxy designed for one + * specific parameter (not for all possible parameters). But custom scopes + * typically cache the results without checking to see if the result is a + * proxy. This leads to caching a result that is unsuitable for reuse for + * other parameters. + * + * This means that custom proxies have to do an + * {@code if(Scopes.isCircularProxy(..))} + * in order to avoid exceptions. + */ + public void testCustomScopeCircularProxies() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindScope(SimpleSingleton.class, new BasicSingleton()); + bind(H.class).to(HImpl.class); + bind(I.class).to(IImpl.class); + bind(J.class).to(JImpl.class); + } + }); + + // The reason this happens is because the Scope gets these requests, in order: + // entry: Key (1 - from getInstance call) + // entry: Key + // entry: Key (2 - circular dependency from HImpl) + // result of 2nd Key - a com.google.inject.$Proxy, because it's a circular proxy + // result of Key - an HImpl + // entry: Key + // entry: Key (3 - another circular dependency, this time from JImpl) + // At this point, if the first Key result was cached, our cache would have + // Key caching to an instanceof of I, but not an an instanceof of IImpl. + // If returned this, it would result in cglib giving a ClassCastException or + // java reflection giving an IllegalArgumentException when filling in parameters + // for the constructor, because JImpl wants an IImpl, not an I. + + try { + injector.getInstance(IImpl.class); + fail(); + } catch(ProvisionException pe) { + assertContains(Iterables.getOnlyElement(pe.getErrorMessages()).getMessage(), + "Tried proxying " + IImpl.class.getName() + + " to support a circular dependency, but it is not an interface."); + } + } + + interface H {} + interface I {} + interface J {} + @SimpleSingleton + static class HImpl implements H { + @Inject HImpl(I i) {} + } + @SimpleSingleton + static class IImpl implements I { + @Inject IImpl(HImpl i, J j) {} + } + @SimpleSingleton + static class JImpl implements J { + @Inject JImpl(IImpl i) {} + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RUNTIME) + @ScopeAnnotation + public @interface SimpleSingleton {} + public static class BasicSingleton implements Scope { + private static Map cache = Maps.newHashMap(); + public Provider scope(final Key key, final Provider unscoped) { + return new Provider() { + @SuppressWarnings("unchecked") + public T get() { + if (!cache.containsKey(key)) { + T t = unscoped.get(); + if (Scopes.isCircularProxy(t)) { + return t; + } + cache.put(key, t); + } + return (T)cache.get(key); + } + }; + } + } +} diff --git a/src/test/java/com/google/inject/DuplicateBindingsTest.java b/src/test/java/com/google/inject/DuplicateBindingsTest.java new file mode 100644 index 0000000..27a2fd1 --- /dev/null +++ b/src/test/java/com/google/inject/DuplicateBindingsTest.java @@ -0,0 +1,576 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.*; +import static com.google.inject.name.Names.named; + +import com.google.common.base.Objects; +import com.google.common.collect.Lists; +import com.google.inject.name.Named; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.google.inject.util.Providers; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.logging.Logger; + +/** + * A suite of tests for duplicate bindings. + * + * @author sameb@google.com (Sam Berlin) + */ +public class DuplicateBindingsTest extends TestCase { + + private FooImpl foo = new FooImpl(); + private Provider pFoo = Providers.of(new FooImpl()); + private Class> pclFoo = FooProvider.class; + private Class clFoo = FooImpl.class; + private Constructor cFoo = FooImpl.cxtor(); + + public void testDuplicateBindingsAreIgnored() { + Injector injector = Guice.createInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo), + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo) + ); + List> bindings = Lists.newArrayList(injector.getAllBindings().keySet()); + removeBasicBindings(bindings); + + // Ensure only one binding existed for each type. + assertTrue(bindings.remove(Key.get(Foo.class, named("instance")))); + assertTrue(bindings.remove(Key.get(Foo.class, named("pInstance")))); + assertTrue(bindings.remove(Key.get(Foo.class, named("pKey")))); + assertTrue(bindings.remove(Key.get(Foo.class, named("linkedKey")))); + assertTrue(bindings.remove(Key.get(FooImpl.class))); + assertTrue(bindings.remove(Key.get(Foo.class, named("constructor")))); + assertTrue(bindings.remove(Key.get(FooProvider.class))); // JIT binding + assertTrue(bindings.remove(Key.get(Foo.class, named("providerMethod")))); + + assertEquals(bindings.toString(), 0, bindings.size()); + } + + public void testElementsDeduplicate() { + List elements = Elements.getElements( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo), + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo) + ); + assertEquals(14, elements.size()); + assertEquals(7, new LinkedHashSet(elements).size()); + } + + public void testProviderMethodsFailIfInstancesDiffer() { + try { + Guice.createInjector(new FailingProviderModule(), new FailingProviderModule()); + fail("should have failed"); + } catch(CreationException ce) { + assertContains(ce.getMessage(), + "A binding to " + Foo.class.getName() + " was already configured " + + "at " + FailingProviderModule.class.getName(), + "at " + FailingProviderModule.class.getName() + ); + } + } + + public void testSameScopeInstanceIgnored() { + Guice.createInjector( + new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo), + new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo) + ); + + Guice.createInjector( + new ScopedModule(Scopes.NO_SCOPE, foo, pFoo, pclFoo, clFoo, cFoo), + new ScopedModule(Scopes.NO_SCOPE, foo, pFoo, pclFoo, clFoo, cFoo) + ); + } + + public void testSameScopeAnnotationIgnored() { + Guice.createInjector( + new AnnotatedScopeModule(Singleton.class, foo, pFoo, pclFoo, clFoo, cFoo), + new AnnotatedScopeModule(Singleton.class, foo, pFoo, pclFoo, clFoo, cFoo) + ); + } + + public void testMixedAnnotationAndScopeForSingletonIgnored() { + Guice.createInjector( + new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo), + new AnnotatedScopeModule(Singleton.class, foo, pFoo, pclFoo, clFoo, cFoo) + ); + } + + public void testMixedScopeAndUnscopedIgnored() { + Guice.createInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo), + new ScopedModule(Scopes.NO_SCOPE, foo, pFoo, pclFoo, clFoo, cFoo) + ); + } + + public void testMixedScopeFails() { + try { + Guice.createInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo), + new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo) + ); + fail("expected exception"); + } catch(CreationException ce) { + String segment1 = "A binding to " + Foo.class.getName() + " annotated with " + + named("pInstance") + " was already configured at " + SimpleModule.class.getName(); + String segment2 = "A binding to " + Foo.class.getName() + " annotated with " + named("pKey") + + " was already configured at " + SimpleModule.class.getName(); + String segment3 = "A binding to " + Foo.class.getName() + " annotated with " + + named("constructor") + " was already configured at " + SimpleModule.class.getName(); + String segment4 = "A binding to " + FooImpl.class.getName() + " was already configured at " + + SimpleModule.class.getName(); + String atSegment = "at " + ScopedModule.class.getName(); + if (isIncludeStackTraceOff()) { + assertContains(ce.getMessage(), segment1 , atSegment, segment2, atSegment, segment3, + atSegment, segment4, atSegment); + } else { + assertContains(ce.getMessage(), segment1 , atSegment, segment2, atSegment, segment4, + atSegment, segment3, atSegment); + } + } + } + + @SuppressWarnings("unchecked") + public void testMixedTargetsFails() { + try { + Guice.createInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo), + new SimpleModule(new FooImpl(), Providers.of(new FooImpl()), + (Class)BarProvider.class, (Class)Bar.class, (Constructor)Bar.cxtor()) + ); + fail("expected exception"); + } catch(CreationException ce) { + assertContains(ce.getMessage(), + "A binding to " + Foo.class.getName() + " annotated with " + named("pInstance") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("pKey") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("linkedKey") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("constructor") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName()); + } + } + + public void testExceptionInEqualsThrowsCreationException() { + try { + Guice.createInjector(new ThrowingModule(), new ThrowingModule()); + fail("expected exception"); + } catch(CreationException ce) { + assertContains(ce.getMessage(), + "A binding to " + Foo.class.getName() + " was already configured at " + ThrowingModule.class.getName(), + "and an error was thrown while checking duplicate bindings. Error: java.lang.RuntimeException: Boo!", + "at " + ThrowingModule.class.getName()); + } + } + + public void testChildInjectorDuplicateParentFail() { + Injector injector = Guice.createInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo) + ); + + try { + injector.createChildInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo) + ); + fail("expected exception"); + } catch(CreationException ce) { + assertContains(ce.getMessage(), + "A binding to " + Foo.class.getName() + " annotated with " + named("pInstance") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("pKey") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("linkedKey") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("constructor") + " was already configured at " + SimpleModule.class.getName(), + "at " + SimpleModule.class.getName(), + "A binding to " + Foo.class.getName() + " annotated with " + named("providerMethod") + " was already configured at " + SimpleProviderModule.class.getName(), + "at " + SimpleProviderModule.class.getName() + ); + } + + + } + + public void testDuplicatesSolelyInChildIgnored() { + Injector injector = Guice.createInjector(); + injector.createChildInjector( + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo), + new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo) + ); + } + + public void testDifferentBindingTypesFail() { + List elements = Elements.getElements( + new FailedModule(foo, pFoo, pclFoo, clFoo, cFoo) + ); + + // Make sure every combination of the elements with another element fails. + // This ensures that duplication checks the kind of binding also. + for(Element e1 : elements) { + for(Element e2: elements) { + // if they're the same, this shouldn't fail. + try { + Guice.createInjector(Elements.getModule(Arrays.asList(e1, e2))); + if(e1 != e2) { + fail("must fail!"); + } + } catch(CreationException expected) { + if(e1 != e2) { + assertContains(expected.getMessage(), + "A binding to " + Foo.class.getName() + " was already configured at " + FailedModule.class.getName(), + "at " + FailedModule.class.getName()); + } else { + throw expected; + } + } + } + } + } + + public void testJitBindingsAreCheckedAfterConversions() { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(A.class); + bind(A.class).to(RealA.class); + } + }); + } + + public void testEqualsNotCalledByDefaultOnInstance() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnEquals = true; + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(HashEqualsTester.class).toInstance(a); + } + }); + } + + public void testEqualsNotCalledByDefaultOnProvider() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnEquals = true; + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(Object.class).toProvider(a); + } + }); + } + + public void testHashcodeNeverCalledOnInstance() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnHashcode = true; + a.equality = "test"; + + final HashEqualsTester b = new HashEqualsTester(); + b.throwOnHashcode = true; + b.equality = "test"; + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(HashEqualsTester.class).toInstance(a); + bind(HashEqualsTester.class).toInstance(b); + } + }); + } + + public void testHashcodeNeverCalledOnProviderInstance() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnHashcode = true; + a.equality = "test"; + + final HashEqualsTester b = new HashEqualsTester(); + b.throwOnHashcode = true; + b.equality = "test"; + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(Object.class).toProvider(a); + bind(Object.class).toProvider(b); + } + }); + } + + private static class RealA extends A {} + @ImplementedBy(RealA.class) private static class A {} + + private void removeBasicBindings(Collection> bindings) { + bindings.remove(Key.get(Injector.class)); + bindings.remove(Key.get(Logger.class)); + bindings.remove(Key.get(Stage.class)); + } + + private static class ThrowingModule extends AbstractModule { + @Override + protected void configure() { + bind(Foo.class).toInstance(new Foo() { + @Override + public boolean equals(Object obj) { + throw new RuntimeException("Boo!"); + } + }); + } + } + + private static abstract class FooModule extends AbstractModule { + protected final FooImpl foo; + protected final Provider pFoo; + protected final Class> pclFoo; + protected final Class clFoo; + protected final Constructor cFoo; + + FooModule(FooImpl foo, Provider pFoo, Class> pclFoo, + Class clFoo, Constructor cFoo) { + this.foo = foo; + this.pFoo = pFoo; + this.pclFoo = pclFoo; + this.clFoo = clFoo; + this.cFoo = cFoo; + } + } + + private static class FailedModule extends FooModule { + FailedModule(FooImpl foo, Provider pFoo, Class> pclFoo, + Class clFoo, Constructor cFoo) { + super(foo, pFoo, pclFoo, clFoo, cFoo); + } + + protected void configure() { + // InstanceBinding + bind(Foo.class).toInstance(foo); + + // ProviderInstanceBinding + bind(Foo.class).toProvider(pFoo); + + // ProviderKeyBinding + bind(Foo.class).toProvider(pclFoo); + + // LinkedKeyBinding + bind(Foo.class).to(clFoo); + + // ConstructorBinding + bind(Foo.class).toConstructor(cFoo); + } + + @Provides Foo foo() { + return null; + } + } + + private static class FailingProviderModule extends AbstractModule { + @Override protected void configure() {} + + @Provides Foo foo() { + return null; + } + } + + private static class SimpleProviderModule extends AbstractModule { + @Override protected void configure() {} + + @Provides @Named("providerMethod") Foo foo() { + return null; + } + + @Override + public boolean equals(Object obj) { + return obj.getClass() == getClass(); + } + } + + private static class SimpleModule extends FooModule { + SimpleModule(FooImpl foo, Provider pFoo, Class> pclFoo, + Class clFoo, Constructor cFoo) { + super(foo, pFoo, pclFoo, clFoo, cFoo); + } + + protected void configure() { + // InstanceBinding + bind(Foo.class).annotatedWith(named("instance")).toInstance(foo); + + // ProviderInstanceBinding + bind(Foo.class).annotatedWith(named("pInstance")).toProvider(pFoo); + + // ProviderKeyBinding + bind(Foo.class).annotatedWith(named("pKey")).toProvider(pclFoo); + + // LinkedKeyBinding + bind(Foo.class).annotatedWith(named("linkedKey")).to(clFoo); + + // UntargettedBinding / ConstructorBinding + bind(FooImpl.class); + + // ConstructorBinding + bind(Foo.class).annotatedWith(named("constructor")).toConstructor(cFoo); + + // ProviderMethod + // (reconstructed from an Element to ensure it doesn't get filtered out + // by deduplicating Modules) + install(Elements.getModule(Elements.getElements(new SimpleProviderModule()))); + } + } + + private static class ScopedModule extends FooModule { + private final Scope scope; + + ScopedModule(Scope scope, FooImpl foo, Provider pFoo, + Class> pclFoo, Class clFoo, + Constructor cFoo) { + super(foo, pFoo, pclFoo, clFoo, cFoo); + this.scope = scope; + } + + protected void configure() { + // ProviderInstanceBinding + bind(Foo.class).annotatedWith(named("pInstance")).toProvider(pFoo).in(scope); + + // ProviderKeyBinding + bind(Foo.class).annotatedWith(named("pKey")).toProvider(pclFoo).in(scope); + + // LinkedKeyBinding + bind(Foo.class).annotatedWith(named("linkedKey")).to(clFoo).in(scope); + + // UntargettedBinding / ConstructorBinding + bind(FooImpl.class).in(scope); + + // ConstructorBinding + bind(Foo.class).annotatedWith(named("constructor")).toConstructor(cFoo).in(scope); + } + } + + private static class AnnotatedScopeModule extends FooModule { + private final Class scope; + + AnnotatedScopeModule(Class scope, FooImpl foo, Provider pFoo, + Class> pclFoo, Class clFoo, + Constructor cFoo) { + super(foo, pFoo, pclFoo, clFoo, cFoo); + this.scope = scope; + } + + + protected void configure() { + // ProviderInstanceBinding + bind(Foo.class).annotatedWith(named("pInstance")).toProvider(pFoo).in(scope); + + // ProviderKeyBinding + bind(Foo.class).annotatedWith(named("pKey")).toProvider(pclFoo).in(scope); + + // LinkedKeyBinding + bind(Foo.class).annotatedWith(named("linkedKey")).to(clFoo).in(scope); + + // UntargettedBinding / ConstructorBinding + bind(FooImpl.class).in(scope); + + // ConstructorBinding + bind(Foo.class).annotatedWith(named("constructor")).toConstructor(cFoo).in(scope); + } + } + + private static interface Foo {} + private static class FooImpl implements Foo { + @Inject public FooImpl() {} + + private static Constructor cxtor() { + try { + return FooImpl.class.getConstructor(); + } catch (SecurityException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + } + private static class FooProvider implements Provider { + public Foo get() { + return new FooImpl(); + } + } + + private static class Bar implements Foo { + @Inject public Bar() {} + + private static Constructor cxtor() { + try { + return Bar.class.getConstructor(); + } catch (SecurityException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + } + private static class BarProvider implements Provider { + public Foo get() { + return new Bar(); + } + } + + private static class HashEqualsTester implements Provider { + private String equality; + private boolean throwOnEquals; + private boolean throwOnHashcode; + + @Override + public boolean equals(Object obj) { + if (throwOnEquals) { + throw new RuntimeException(); + } else if (obj instanceof HashEqualsTester) { + HashEqualsTester o = (HashEqualsTester)obj; + if(o.throwOnEquals) { + throw new RuntimeException(); + } + if(equality == null && o.equality == null) { + return this == o; + } else { + return Objects.equal(equality, o.equality); + } + } else { + return false; + } + } + + @Override + public int hashCode() { + if(throwOnHashcode) { + throw new RuntimeException(); + } else { + return super.hashCode(); + } + } + + public Object get() { + return new Object(); + } + } + +} diff --git a/src/test/java/com/google/inject/EagerSingletonTest.java b/src/test/java/com/google/inject/EagerSingletonTest.java new file mode 100644 index 0000000..d18f194 --- /dev/null +++ b/src/test/java/com/google/inject/EagerSingletonTest.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import junit.framework.TestCase; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class EagerSingletonTest extends TestCase { + + @Override public void setUp() { + A.instanceCount = 0; + B.instanceCount = 0; + C.instanceCount = 0; + } + + public void testJustInTimeEagerSingletons() { + Guice.createInjector(Stage.PRODUCTION, new AbstractModule() { + protected void configure() { + // create a just-in-time binding for A + getProvider(A.class); + + // create a just-in-time binding for C + requestInjection(new Object() { + @Inject void inject(Injector injector) { + injector.getInstance(C.class); + } + }); + } + }); + + assertEquals(1, A.instanceCount); + assertEquals("Singletons discovered when creating singletons should not be built eagerly", + 0, B.instanceCount); + assertEquals(1, C.instanceCount); + } + + public void testJustInTimeSingletonsAreNotEager() { + Injector injector = Guice.createInjector(Stage.PRODUCTION); + injector.getProvider(B.class); + assertEquals(0, B.instanceCount); + } + + public void testChildEagerSingletons() { + Injector parent = Guice.createInjector(Stage.PRODUCTION); + parent.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(D.class).to(C.class); + } + }); + + assertEquals(1, C.instanceCount); + } + + @Singleton + static class A { + static int instanceCount = 0; + int instanceId = instanceCount++; + + @Inject A(Injector injector) { + injector.getProvider(B.class); + } + } + + @Singleton + static class B { + static int instanceCount = 0; + int instanceId = instanceCount++; + } + + @Singleton + static class C implements D { + static int instanceCount = 0; + int instanceId = instanceCount++; + } + + private static interface D {} +} diff --git a/src/test/java/com/google/inject/ErrorHandlingTest.java b/src/test/java/com/google/inject/ErrorHandlingTest.java new file mode 100644 index 0000000..19161ca --- /dev/null +++ b/src/test/java/com/google/inject/ErrorHandlingTest.java @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ErrorHandlingTest { + + public static void main(String[] args) throws CreationException { + try { + Guice.createInjector(new MyModule()); + } + catch (CreationException e) { + e.printStackTrace(); + System.err.println("--"); + } + + Injector bad = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toProvider(new Provider() { + public String get() { + return null; + } + }); + } + }); + try { + bad.getInstance(String.class); + } + catch (Exception e) { + e.printStackTrace(); + System.err.println("--"); + } + try { + bad.getInstance(NeedsString.class); + } + catch (Exception e) { + e.printStackTrace(); + System.err.println("--"); + } + } + + static class NeedsString { + @Inject String mofo; + } + + @Inject @Named("missing") + static List missing = null; + + static class Foo { + @Inject + public Foo(Runnable r) {} + + @Inject void setNames(List names) {} + } + + static class Bar { + // Invalid constructor. + Bar(String s) {} + + @Inject void setNumbers(@Named("numbers") List numbers) {} + + @Inject void bar(@Named("foo") String s) {} + } + + static class Tee { + @Inject String s; + + @Inject void tee(String s, int i) {} + + @Inject Invalid invalid; + } + + static class Invalid { + Invalid(String s) {} + } + + @SuppressWarnings("MoreThanOneScopeAnnotationOnClass") // suppress compiler error to test + @Singleton + @GoodScope + static class TooManyScopes { + } + + @Target(ElementType.TYPE) + @Retention(RUNTIME) + @ScopeAnnotation + @interface GoodScope {} + + @interface BadScope {} + + @ImplementedBy(String.class) + interface I {} + + static class MyModule extends AbstractModule { + protected void configure() { + bind(Runnable.class); + bind(Foo.class); + bind(Bar.class); + bind(Tee.class); + bind(new TypeLiteral>() {}); + bind(String.class).annotatedWith(Names.named("foo")).in(Named.class); + bind(Key.get(Runnable.class)).to(Key.get(Runnable.class)); + bind(TooManyScopes.class); + bindScope(BadScope.class, Scopes.SINGLETON); + bind(Object.class).toInstance(new Object() { + @Inject void foo() { + throw new RuntimeException(); + } + }); + requestStaticInjection(ErrorHandlingTest.class); + + addError("I don't like %s", "you"); + + Object o = "2"; + try { + Integer i = (Integer) o; + } catch (Exception e) { + addError(e); + } + + bind(Module.class).toInstance(this); + bind(I.class); + } + } +} diff --git a/src/test/java/com/google/inject/GenericInjectionTest.java b/src/test/java/com/google/inject/GenericInjectionTest.java new file mode 100644 index 0000000..51a1cb1 --- /dev/null +++ b/src/test/java/com/google/inject/GenericInjectionTest.java @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.util.Modules; + +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class GenericInjectionTest extends TestCase { + + public void testGenericInjection() throws CreationException { + final List names = Arrays.asList("foo", "bar", "bob"); + + Injector injector = Guice.createInjector((Module) new AbstractModule() { + protected void configure() { + bind(new TypeLiteral>() {}).toInstance(names); + } + }); + + Foo foo = injector.getInstance(Foo.class); + assertEquals(names, foo.names); + } + + static class Foo { + @Inject List names; + } + + /** + * Although we may not have intended to support this behaviour, this test + * passes under Guice 1.0. The workaround is to add an explicit binding for + * the parameterized type. See {@link #testExplicitBindingOfGenericType()}. + */ + public void testImplicitBindingOfGenericType() { + Parameterized parameterized + = Guice.createInjector().getInstance(Key.get(new TypeLiteral>() {})); + assertNotNull(parameterized); + } + + public void testExplicitBindingOfGenericType() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Key.get(new TypeLiteral>() {})) + .to((Class) Parameterized.class); + } + }); + + Parameterized parameterized + = injector.getInstance(Key.get(new TypeLiteral>() { })); + assertNotNull(parameterized); + } + + static class Parameterized { + @Inject + Parameterized() { } + } + + public void testInjectingParameterizedDependenciesForImplicitBinding() { + assertParameterizedDepsInjected(new Key>() {}, + Modules.EMPTY_MODULE); + } + + public void testInjectingParameterizedDependenciesForBindingTarget() { + final TypeLiteral> type + = new TypeLiteral>() {}; + + assertParameterizedDepsInjected(Key.get(Object.class), new AbstractModule() { + protected void configure() { + bind(Object.class).to(type); + } + }); + } + + public void testInjectingParameterizedDependenciesForBindingSource() { + final TypeLiteral> type + = new TypeLiteral>() {}; + + assertParameterizedDepsInjected(Key.get(type), new AbstractModule() { + protected void configure() { + bind(type); + } + }); + } + + public void testBindingToSubtype() { + final TypeLiteral> type + = new TypeLiteral>() {}; + + assertParameterizedDepsInjected(Key.get(type), new AbstractModule() { + protected void configure() { + bind(type).to(new TypeLiteral>() {}); + } + }); + } + + public void testBindingSubtype() { + final TypeLiteral> type + = new TypeLiteral>() {}; + + assertParameterizedDepsInjected(Key.get(type), new AbstractModule() { + protected void configure() { + bind(type); + } + }); + } + + @SuppressWarnings("unchecked") + public void assertParameterizedDepsInjected(Key key, Module bindingModule) { + Module bindDataModule = new AbstractModule() { + protected void configure() {} + @Provides Map provideMap() { + return ImmutableMap.of("one", 1, "two", 2); + } + @Provides Set provideSet(Map map) { + return map.keySet(); + } + @Provides Collection provideCollection(Map map) { + return map.values(); + } + }; + + Injector injector = Guice.createInjector(bindDataModule, bindingModule); + ParameterizedDeps parameterizedDeps + = (ParameterizedDeps) injector.getInstance(key); + assertEquals(ImmutableMap.of("one", 1, "two", 2), parameterizedDeps.map); + assertEquals(ImmutableSet.of("one", "two"), parameterizedDeps.keys); + assertEquals(ImmutableSet.of(1, 2), ImmutableSet.copyOf(parameterizedDeps.values)); + } + + static class SubParameterizedDeps extends ParameterizedDeps { + @Inject SubParameterizedDeps(Set keys) { + super(keys); + } + } + + static class ParameterizedDeps { + @Inject private Map map; + private Set keys; + private Collection values; + + @Inject ParameterizedDeps(Set keys) { + this.keys = keys; + } + + @Inject void method(Collection values) { + this.values = values; + } + } + + public void testImmediateTypeVariablesAreInjected() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("tee"); + } + }); + InjectsT injectsT = injector.getInstance(new Key>() {}); + assertEquals("tee", injectsT.t); + } + + static class InjectsT { + @Inject T t; + } +} diff --git a/src/test/java/com/google/inject/ImplicitBindingTest.java b/src/test/java/com/google/inject/ImplicitBindingTest.java new file mode 100644 index 0000000..eb47e49 --- /dev/null +++ b/src/test/java/com/google/inject/ImplicitBindingTest.java @@ -0,0 +1,395 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import com.google.common.collect.Iterables; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.Message; + +import junit.framework.TestCase; + +import java.util.List; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ImplicitBindingTest extends TestCase { + + public void testCircularDependency() throws CreationException { + Injector injector = Guice.createInjector(); + Foo foo = injector.getInstance(Foo.class); + assertSame(foo, foo.bar.foo); + } + + static class Foo { + @Inject Bar bar; + } + + static class Bar { + final Foo foo; + @Inject + public Bar(Foo foo) { + this.foo = foo; + } + } + + public void testDefaultImplementation() { + Injector injector = Guice.createInjector(); + I i = injector.getInstance(I.class); + i.go(); + } + + @ImplementedBy(IImpl.class) + interface I { + void go(); + } + + static class IImpl implements I { + public void go() {} + } + + static class AlternateImpl implements I { + public void go() {} + } + + public void testDefaultProvider() { + Injector injector = Guice.createInjector(); + Provided provided = injector.getInstance(Provided.class); + provided.go(); + } + + public void testBindingOverridesImplementedBy() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(I.class).to(AlternateImpl.class); + } + }); + assertEquals(AlternateImpl.class, injector.getInstance(I.class).getClass()); + } + + @ProvidedBy(ProvidedProvider.class) + interface Provided { + void go(); + } + + public void testNoImplicitBindingIsCreatedForAnnotatedKeys() { + try { + Guice.createInjector().getInstance(Key.get(I.class, Names.named("i"))); + fail(); + } catch (ConfigurationException expected) { + Asserts.assertContains(expected.getMessage(), + "1) No implementation for " + I.class.getName(), + "annotated with @" + Named.class.getName() + "(value=i) was bound.", + "while locating " + I.class.getName(), + " annotated with @" + Named.class.getName() + "(value=i)"); + } + } + + static class ProvidedProvider implements Provider { + public Provided get() { + return new Provided() { + public void go() {} + }; + } + } + + /** + * When we're building the binding for A, we temporarily insert that binding to support circular + * dependencies. And so we can successfully create a binding for B. But later, when the binding + * for A ultimately fails, we need to clean up the dependent binding for B. + * + * The test loops through linked bindings & bindings with constructor & member injections, + * to make sure that all are cleaned up and traversed. It also makes sure we don't touch + * explicit bindings. + */ + public void testCircularJitBindingsLeaveNoResidue() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Valid.class); + bind(Valid2.class); + } + }); + + // Capture good bindings. + Binding v1 = injector.getBinding(Valid.class); + Binding v2 = injector.getBinding(Valid2.class); + Binding jv1 = injector.getBinding(JitValid.class); + Binding jv2 = injector.getBinding(JitValid2.class); + + // Then validate that a whole series of invalid bindings are erased. + assertFailure(injector, Invalid.class); + assertFailure(injector, InvalidLinked.class); + assertFailure(injector, InvalidLinkedImpl.class); + assertFailure(injector, InvalidLinked2.class); + assertFailure(injector, InvalidLinked2Impl.class); + assertFailure(injector, InvalidProvidedBy.class); + assertFailure(injector, InvalidProvidedByProvider.class); + assertFailure(injector, InvalidProvidedBy2.class); + assertFailure(injector, InvalidProvidedBy2Provider.class); + assertFailure(injector, Invalid2.class); + + // Validate we didn't do anything to the valid explicit bindings. + assertSame(v1, injector.getBinding(Valid.class)); + assertSame(v2, injector.getBinding(Valid2.class)); + + // Validate that we didn't erase the valid JIT bindings + assertSame(jv1, injector.getBinding(JitValid.class)); + assertSame(jv2, injector.getBinding(JitValid2.class)); + } + + @SuppressWarnings("unchecked") + private void assertFailure(Injector injector, Class clazz) { + try { + injector.getBinding(clazz); + fail("Shouldn't have been able to get binding of: " + clazz); + } catch(ConfigurationException expected) { + Message msg = Iterables.getOnlyElement(expected.getErrorMessages()); + assertEquals("No implementation for " + InvalidInterface.class.getName() + " was bound.", + msg.getMessage()); + List sources = msg.getSources(); + // Assert that the first item in the sources if the key for the class we're looking up, + // ensuring that each lookup is "new". + assertEquals(Key.get(clazz).toString(), sources.get(0).toString()); + // Assert that the last item in each lookup contains the InvalidInterface class + Asserts.assertContains(sources.get(sources.size()-1).toString(), + Key.get(InvalidInterface.class).toString()); + } + } + + static class Invalid { + @Inject Valid a; + @Inject JitValid b; + @Inject InvalidProvidedBy c; + @Inject Invalid(InvalidLinked a) {} + @Inject void foo(InvalidInterface a) {} + + } + + @ImplementedBy(InvalidLinkedImpl.class) + static interface InvalidLinked {} + static class InvalidLinkedImpl implements InvalidLinked { + @Inject InvalidLinked2 a; + } + + @ImplementedBy(InvalidLinked2Impl.class) + static interface InvalidLinked2 {} + static class InvalidLinked2Impl implements InvalidLinked2 { + @Inject InvalidLinked2Impl(Invalid2 a) {} + } + + @ProvidedBy(InvalidProvidedByProvider.class) + static interface InvalidProvidedBy {} + static class InvalidProvidedByProvider implements Provider { + @Inject InvalidProvidedBy2 a; + public InvalidProvidedBy get() { + return null; + } + } + + @ProvidedBy(InvalidProvidedBy2Provider.class) + static interface InvalidProvidedBy2 {} + static class InvalidProvidedBy2Provider implements Provider { + @Inject Invalid2 a; + public InvalidProvidedBy2 get() { + return null; + } + } + + static class Invalid2 { + @Inject Invalid a; + } + + interface InvalidInterface {} + + static class Valid { @Inject Valid2 a; } + static class Valid2 {} + + static class JitValid { @Inject JitValid2 a; } + static class JitValid2 {} + + /** + * Regression test for https://github.com/google/guice/issues/319 + * + * The bug is that a class that asks for a provider for itself during injection time, + * where any one of the other types required to fulfill the object creation was bound + * in a child constructor, explodes when the injected Provider is called. + * + * It works just fine when the other types are bound in a main injector. + */ + public void testInstancesRequestingProvidersForThemselvesWithChildInjectors() { + final Module testModule = new AbstractModule() { + @Override + protected void configure() { + bind(String.class) + .toProvider(TestStringProvider.class); + } + }; + + // Verify it works when the type is setup in the parent. + Injector parentSetupRootInjector = Guice.createInjector(testModule); + Injector parentSetupChildInjector = parentSetupRootInjector.createChildInjector(); + assertEquals(TestStringProvider.TEST_VALUE, + parentSetupChildInjector.getInstance( + RequiresProviderForSelfWithOtherType.class).getValue()); + + // Verify it works when the type is setup in the child, not the parent. + // If it still occurs, the bug will explode here. + Injector childSetupRootInjector = Guice.createInjector(); + Injector childSetupChildInjector = childSetupRootInjector.createChildInjector(testModule); + assertEquals(TestStringProvider.TEST_VALUE, + childSetupChildInjector.getInstance( + RequiresProviderForSelfWithOtherType.class).getValue()); + } + + static class TestStringProvider implements Provider { + static final String TEST_VALUE = "This is to verify it all works"; + + public String get() { + return TEST_VALUE; + } + } + + static class RequiresProviderForSelfWithOtherType { + private final Provider selfProvider; + private final String providedStringValue; + + @Inject + RequiresProviderForSelfWithOtherType( + String providedStringValue, + Provider selfProvider + ) { + this.providedStringValue = providedStringValue; + this.selfProvider = selfProvider; + } + + public String getValue() { + // Attempt to get another instance of ourself. This pattern + // is possible for recursive processing. + selfProvider.get(); + + return providedStringValue; + } + } + + /** + * Ensure that when we cleanup failed JIT bindings, we don't break. + * The test here requires a sequence of JIT bindings: + * A-> B + * B -> C, A + * C -> A, D + * D not JITable + * The problem was that C cleaned up A's binding and then handed control back to B, + * which tried to continue processing A.. but A was removed from the jitBindings Map, + * so it attempts to create a new JIT binding for A, but we haven't yet finished + * constructing the first JIT binding for A, so we get a recursive + * computation exception from ComputingConcurrentHashMap. + * + * We also throw in a valid JIT binding, E, to guarantee that if + * something fails in this flow, it can be recreated later if it's + * not from a failed sequence. + */ + public void testRecursiveJitBindingsCleanupCorrectly() throws Exception { + Injector injector = Guice.createInjector(); + try { + injector.getInstance(A.class); + fail("Expected failure"); + } catch(ConfigurationException expected) { + Message msg = Iterables.getOnlyElement(expected.getErrorMessages()); + Asserts.assertContains(msg.getMessage(), + "Could not find a suitable constructor in " + D.class.getName()); + } + // Assert that we've removed all the bindings. + assertNull(injector.getExistingBinding(Key.get(A.class))); + assertNull(injector.getExistingBinding(Key.get(B.class))); + assertNull(injector.getExistingBinding(Key.get(C.class))); + assertNull(injector.getExistingBinding(Key.get(D.class))); + + // Confirm that we didn't prevent 'E' from working. + assertNotNull(injector.getBinding(Key.get(E.class))); + } + + static class A { + @Inject public A(B b) {} + } + + static class B { + @Inject public B(C c, A a) {} + } + + static class C { + @Inject public C(A a, D d, E e) {} + } + + static class D { + public D(int i) {} + } + + // Valid JITable binding + static class E { } + + public void testProvidedByNonEmptyEnum() { + NonEmptyEnum cardSuit = Guice.createInjector().getInstance(NonEmptyEnum.class); + + assertEquals(NonEmptyEnum.HEARTS, cardSuit); + } + + public void testProvidedByEmptyEnum() { + EmptyEnum emptyEnumValue = Guice.createInjector().getInstance(EmptyEnum.class); + assertNull(emptyEnumValue); + } + + @ProvidedBy(NonEmptyEnumProvider.class) + enum NonEmptyEnum { HEARTS, DIAMONDS, CLUBS, SPADES } + + static final class NonEmptyEnumProvider implements Provider { + @Override + public NonEmptyEnum get() { + return NonEmptyEnum.HEARTS; + } + } + + @ProvidedBy(EmptyEnumProvider.class) + enum EmptyEnum {} + + static final class EmptyEnumProvider implements Provider { + @Override + public EmptyEnum get() { + return null; + } + } + + // An enum cannot be implemented by anything, so it should not be possible to have a successful + // binding when the enum is annotated with @ImplementedBy. + public void testImplementedByEnum() { + Injector injector = Guice.createInjector(); + try { + injector.getInstance(EnumWithImplementedBy.class); + fail("Expected failure"); + } catch(ConfigurationException expected) { + Message msg = Iterables.getOnlyElement(expected.getErrorMessages()); + Asserts.assertContains(msg.getMessage(), + "No implementation for " + EnumWithImplementedBy.class.getName() + " was bound."); + } + } + + @ImplementedBy(EnumWithImplementedByEnum.class) + enum EnumWithImplementedBy {} + private static class EnumWithImplementedByEnum {} +} diff --git a/src/test/java/com/google/inject/InjectorTest.java b/src/test/java/com/google/inject/InjectorTest.java new file mode 100644 index 0000000..a71eb18 --- /dev/null +++ b/src/test/java/com/google/inject/InjectorTest.java @@ -0,0 +1,413 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.assertNotSerializable; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author crazybob@google.com (Bob Lee) + */ + +public class InjectorTest extends TestCase { + + @Retention(RUNTIME) + @BindingAnnotation @interface Other {} + + @Retention(RUNTIME) + @BindingAnnotation @interface S {} + + @Retention(RUNTIME) + @BindingAnnotation @interface I {} + + public void testToStringDoesNotInfinitelyRecurse() { + Injector injector = Guice.createInjector(Stage.TOOL); + injector.toString(); + injector.getBinding(Injector.class).toString(); + } + + public void testProviderMethods() throws CreationException { + final SampleSingleton singleton = new SampleSingleton(); + final SampleSingleton other = new SampleSingleton(); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(SampleSingleton.class).toInstance(singleton); + bind(SampleSingleton.class) + .annotatedWith(Other.class) + .toInstance(other); + } + }); + + assertSame(singleton, + injector.getInstance(Key.get(SampleSingleton.class))); + assertSame(singleton, injector.getInstance(SampleSingleton.class)); + + assertSame(other, + injector.getInstance(Key.get(SampleSingleton.class, Other.class))); + } + + static class SampleSingleton {} + + public void testInjection() throws CreationException { + Injector injector = createFooInjector(); + Foo foo = injector.getInstance(Foo.class); + + assertEquals("test", foo.s); + assertEquals("test", foo.bar.getTee().getS()); + assertSame(foo.bar, foo.copy); + assertEquals(5, foo.i); + assertEquals(5, foo.bar.getI()); + + // Test circular dependency. + assertSame(foo.bar, foo.bar.getTee().getBar()); + } + + private Injector createFooInjector() throws CreationException { + return Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Bar.class).to(BarImpl.class); + bind(Tee.class).to(TeeImpl.class); + bindConstant().annotatedWith(S.class).to("test"); + bindConstant().annotatedWith(I.class).to(5); + } + }); + } + + public void testGetInstance() throws CreationException { + Injector injector = createFooInjector(); + + Bar bar = injector.getInstance(Key.get(Bar.class)); + assertEquals("test", bar.getTee().getS()); + assertEquals(5, bar.getI()); + } + + public void testIntAndIntegerAreInterchangeable() + throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(I.class).to(5); + } + }); + + IntegerWrapper iw = injector.getInstance(IntegerWrapper.class); + assertEquals(5, (int) iw.i); + } + + public void testInjectorApiIsNotSerializable() throws IOException { + Injector injector = Guice.createInjector(); + assertNotSerializable(injector); + assertNotSerializable(injector.getProvider(String.class)); + assertNotSerializable(injector.getBinding(String.class)); + for (Binding binding : injector.getBindings().values()) { + assertNotSerializable(binding); + } + } + + static class IntegerWrapper { + @Inject @I Integer i; + } + + static class Foo { + + @Inject Bar bar; + @Inject Bar copy; + + @Inject @S String s; + + int i; + + @Inject + void setI(@I int i) { + this.i = i; + } + } + + interface Bar { + + Tee getTee(); + int getI(); + } + + @Singleton + static class BarImpl implements Bar { + + @Inject @I int i; + + Tee tee; + + @Inject + void initialize(Tee tee) { + this.tee = tee; + } + + public Tee getTee() { + return tee; + } + + public int getI() { + return i; + } + } + + interface Tee { + + String getS(); + Bar getBar(); + } + + static class TeeImpl implements Tee { + + final String s; + @Inject Bar bar; + + @Inject + TeeImpl(@S String s) { + this.s = s; + } + + public String getS() { + return s; + } + + public Bar getBar() { + return bar; + } + } + + public void testInjectStatics() throws CreationException { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(S.class).to("test"); + bindConstant().annotatedWith(I.class).to(5); + requestStaticInjection(Static.class); + } + }); + + assertEquals("test", Static.s); + assertEquals(5, Static.i); + } + + public void testInjectStaticInterface() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + requestStaticInjection(Interface.class); + } + }); + fail(); + } catch(CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + Asserts.assertContains( + ce.getMessage(), + "1) " + Interface.class.getName() + + " is an interface, but interfaces have no static injection points.", + "at " + InjectorTest.class.getName(), + "configure"); + } + } + + private static interface Interface {} + + static class Static { + + @Inject @I static int i; + + static String s; + + @Inject static void setS(@S String s) { + Static.s = s; + } + } + + public void testPrivateInjection() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("foo"); + bind(int.class).toInstance(5); + } + }); + + Private p = injector.getInstance(Private.class); + assertEquals("foo", p.fromConstructor); + assertEquals(5, p.fromMethod); + } + + static class Private { + String fromConstructor; + int fromMethod; + + @Inject + private Private(String fromConstructor) { + this.fromConstructor = fromConstructor; + } + + @Inject + private void setInt(int i) { + this.fromMethod = i; + } + } + + public void testProtectedInjection() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("foo"); + bind(int.class).toInstance(5); + } + }); + + Protected p = injector.getInstance(Protected.class); + assertEquals("foo", p.fromConstructor); + assertEquals(5, p.fromMethod); + } + + static class Protected { + String fromConstructor; + int fromMethod; + + @Inject + protected Protected(String fromConstructor) { + this.fromConstructor = fromConstructor; + } + + @Inject + protected void setInt(int i) { + this.fromMethod = i; + } + } + + public void testInstanceInjectionHappensAfterFactoriesAreSetUp() { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Object.class).toInstance(new Object() { + @Inject Runnable r; + }); + + bind(Runnable.class).to(MyRunnable.class); + } + }); + } + + public void testSubtypeNotProvided() { + try { + Guice.createInjector().getInstance(Money.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + Tree.class.getName() + " doesn't provide instances of " + Money.class.getName(), + "while locating ", Tree.class.getName(), + "while locating ", Money.class.getName()); + } + } + + public void testNotASubtype() { + try { + Guice.createInjector().getInstance(PineTree.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + Tree.class.getName() + " doesn't extend " + PineTree.class.getName(), + "while locating ", PineTree.class.getName()); + } + } + + public void testRecursiveImplementationType() { + try { + Guice.createInjector().getInstance(SeaHorse.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "@ImplementedBy points to the same class it annotates.", + "while locating ", SeaHorse.class.getName()); + } + } + + public void testRecursiveProviderType() { + try { + Guice.createInjector().getInstance(Chicken.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "@ProvidedBy points to the same class it annotates", + "while locating ", Chicken.class.getName()); + } + } + + static class MyRunnable implements Runnable { + public void run() {} + } + + @ProvidedBy(Tree.class) + static class Money {} + + static class Tree implements Provider { + public Object get() { + return "Money doesn't grow on trees"; + } + } + + @ImplementedBy(Tree.class) + static class PineTree extends Tree {} + + @ImplementedBy(SeaHorse.class) + static class SeaHorse {} + + @ProvidedBy(Chicken.class) + static class Chicken implements Provider { + public Chicken get() { + return this; + } + } + + public void testJitBindingFromAnotherThreadDuringInjection() { + final ExecutorService executorService = Executors.newSingleThreadExecutor(); + final AtomicReference got = new AtomicReference(); + + Guice.createInjector(new AbstractModule() { + protected void configure() { + requestInjection(new Object() { + @Inject void initialize(final Injector injector) + throws ExecutionException, InterruptedException { + Future future = executorService.submit(new Callable() { + public JustInTime call() throws Exception { + return injector.getInstance(JustInTime.class); + } + }); + got.set(future.get()); + } + }); + } + }); + + assertNotNull(got.get()); + } + + static class JustInTime {} +} diff --git a/src/test/java/com/google/inject/Java8LanguageFeatureBindingTest.java b/src/test/java/com/google/inject/Java8LanguageFeatureBindingTest.java new file mode 100644 index 0000000..394a60b --- /dev/null +++ b/src/test/java/com/google/inject/Java8LanguageFeatureBindingTest.java @@ -0,0 +1,157 @@ +/** + * Copyright (C) 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import junit.framework.TestCase; + +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * Test bindings to lambdas, method references, etc. + * + * @author cgdecker@google.com (Colin Decker) + */ +public class Java8LanguageFeatureBindingTest extends TestCase { + + // Some of these tests are kind of weird. + // See https://github.com/google/guice/issues/757 for more on why they exist. + + public void testBinding_lambdaToInterface() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>() {}).toInstance(o -> o != null); + } + }); + + Predicate predicate = injector.getInstance(new Key>() {}); + assertTrue(predicate.test(new Object())); + assertFalse(predicate.test(null)); + } + + public void testProviderMethod_returningLambda() throws Exception { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() {} + + @Provides + public Callable provideCallable() { + return () -> "foo"; + } + }); + + Callable callable = injector.getInstance(new Key>() {}); + assertEquals("foo", callable.call()); + } + + public void testProviderMethod_containingLambda_throwingException() throws Exception { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() {} + + @Provides + public Callable provideCallable() { + if (Boolean.parseBoolean("false")) { // avoid dead code warnings + return () -> "foo"; + } else { + throw new RuntimeException("foo"); + } + } + }); + + try { + injector.getInstance(new Key>() {}); + } catch (ProvisionException expected) { + assertTrue(expected.getCause() instanceof RuntimeException); + assertEquals("foo", expected.getCause().getMessage()); + } + } + + public void testProvider_usingJdk8Features() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toProvider(StringProvider.class); + } + }); + + fail(); + } catch (CreationException expected) { + } + + UUID uuid = UUID.randomUUID(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(UUID.class).toInstance(uuid); + bind(String.class).toProvider(StringProvider.class); + } + }); + + assertEquals(uuid.toString(), injector.getInstance(String.class)); + } + + private static final class StringProvider implements Provider { + private final UUID uuid; + + @Inject + StringProvider(UUID uuid) { + this.uuid = uuid; + } + + @Override + public String get() { + return Collections.singleton(uuid).stream() + .map(UUID::toString) + .findFirst().get(); + } + } + + public void testBinding_toProvider_lambda() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + AtomicInteger i = new AtomicInteger(); + bind(String.class).toProvider(() -> "Hello" + i.incrementAndGet()); + } + }); + + assertEquals("Hello1", injector.getInstance(String.class)); + assertEquals("Hello2", injector.getInstance(String.class)); + } + + public void testBinding_toProvider_methodReference() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toProvider(Java8LanguageFeatureBindingTest.this::provideString); + } + }); + + Provider provider = injector.getProvider(String.class); + assertEquals("Hello", provider.get()); + } + + private String provideString() { + return "Hello"; + } +} diff --git a/src/test/java/com/google/inject/JitBindingsTest.java b/src/test/java/com/google/inject/JitBindingsTest.java new file mode 100644 index 0000000..243c53e --- /dev/null +++ b/src/test/java/com/google/inject/JitBindingsTest.java @@ -0,0 +1,731 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.common.collect.ImmutableSet.of; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.JitBindingsTest.GetBindingCheck.ALLOW_BINDING; +import static com.google.inject.JitBindingsTest.GetBindingCheck.FAIL_ALL; + +import junit.framework.TestCase; + +import java.util.Set; + +/** + * Some tests for {@link Binder#requireExplicitBindings()} + * + * @author sberlin@gmail.com (Sam Berlin) + */ +public class JitBindingsTest extends TestCase { + + private String jitFailed(Class clazz) { + return jitFailed(TypeLiteral.get(clazz)); + } + + private String jitFailed(TypeLiteral clazz) { + return "Explicit bindings are required and " + clazz + " is not explicitly bound."; + } + + private String jitInParentFailed(Class clazz) { + return jitInParentFailed(TypeLiteral.get(clazz)); + } + + private String jitInParentFailed(TypeLiteral clazz) { + return "Explicit bindings are required and " + clazz + " would be bound in a parent injector."; + } + + private String inChildMessage(Class clazz) { + return "Unable to create binding for " + + clazz.getName() + + ". It was already configured on one or more child injectors or private modules"; + } + + public void testLinkedBindingWorks() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + } + }); + // Foo was explicitly bound + ensureWorks(injector, Foo.class); + // FooImpl was implicitly bound, it is an error to call getInstance or getProvider, + // It is OK to call getBinding for introspection, but an error to get the provider + // of the binding + ensureFails(injector, ALLOW_BINDING, FooImpl.class); + } + + public void testMoreBasicsWork() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + bind(Bar.class); + bind(FooBar.class); + } + }); + // Foo, Bar & FooBar was explicitly bound + ensureWorks(injector, FooBar.class, Bar.class, Foo.class); + // FooImpl was implicitly bound, it is an error to call getInstance or getProvider, + // It is OK to call getBinding for introspection, but an error to get the provider + // of the binding + ensureFails(injector, ALLOW_BINDING, FooImpl.class); + } + + public void testLinkedEagerSingleton() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class).asEagerSingleton(); + } + }); + // Foo was explicitly bound + ensureWorks(injector, Foo.class); + // FooImpl was implicitly bound, it is an error to call getInstance or getProvider, + // It is OK to call getBinding for introspection, but an error to get the provider + // of the binding + ensureFails(injector, ALLOW_BINDING, FooImpl.class); + } + + public void testBasicsWithEagerSingleton() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class).asEagerSingleton(); + bind(Bar.class); + bind(FooBar.class); + } + }); + // Foo, Bar & FooBar was explicitly bound + ensureWorks(injector, FooBar.class, Bar.class, Foo.class); + // FooImpl was implicitly bound, it is an error to call getInstance or getProvider, + // It is OK to call getBinding for introspection, but an error to get the provider + // of the binding + ensureFails(injector, ALLOW_BINDING, FooImpl.class); + } + + public void testLinkedToScoped() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder.requireExplicitBindings(); + bind(Foo.class).to(ScopedFooImpl.class); + } + }); + // Foo was explicitly bound + ensureWorks(injector, Foo.class); + // FooSingletonImpl was implicitly bound, it is an error to call getInstance or getProvider, + // It is OK to call getBinding for introspection, but an error to get the provider + // of the binding + ensureFails(injector, ALLOW_BINDING, ScopedFooImpl.class); + } + + public void testBasicsWithScoped() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(ScopedFooImpl.class); + bind(Bar.class); + bind(FooBar.class); + } + }); + // Foo, Bar & FooBar was explicitly bound + ensureWorks(injector, FooBar.class, Bar.class, Foo.class); + // FooSingletonImpl was implicitly bound, it is an error to call getInstance or getProvider, + // It is OK to call getBinding for introspection, but an error to get the provider + // of the binding + ensureFails(injector, ALLOW_BINDING, ScopedFooImpl.class); + } + + public void testFailsIfInjectingScopedDirectlyWhenItIsntBound() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(ScopedFooImpl.class); + bind(WantsScopedFooImpl.class); + } + }); + fail(); + } catch(CreationException expected) { + assertContains(expected.getMessage(), jitFailed(ScopedFooImpl.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testLinkedProviderBindingWorks() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).toProvider(FooProvider.class); + } + }); + // Foo was explicitly bound + ensureWorks(injector, Foo.class); + // FooImpl was not bound at all (even implicitly), it is an error + // to call getInstance, getProvider, or getBinding. + ensureFails(injector, FAIL_ALL, FooImpl.class); + } + + public void testJitGetFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + } + }).getInstance(Bar.class); + fail("should have failed"); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), jitFailed(Bar.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testJitInjectionFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + bind(FooBar.class); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), jitFailed(Bar.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testJitProviderGetFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + } + }).getProvider(Bar.class); + fail("should have failed"); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), jitFailed(Bar.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testJitProviderInjectionFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + bind(ProviderFooBar.class); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), jitFailed(Bar.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testImplementedBy() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(ImplBy.class); + } + }); + ensureWorks(injector, ImplBy.class); + ensureFails(injector, ALLOW_BINDING, ImplByImpl.class); + } + + public void testImplementedBySomethingThatIsAnnotated() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(ImplByScoped.class); + } + }); + ensureWorks(injector, ImplByScoped.class); + ensureFails(injector, ALLOW_BINDING, ImplByScopedImpl.class); + } + + public void testProvidedBy() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(ProvBy.class); + } + }); + ensureWorks(injector, ProvBy.class); + ensureFails(injector, ALLOW_BINDING, ProvByProvider.class); + } + + public void testProviderMethods() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().requireExplicitBindings(); + } + @SuppressWarnings("unused") @Provides Foo foo() { return new FooImpl(); } + }); + ensureWorks(injector, Foo.class); + } + + public void testChildInjectorInheritsOption() { + Injector parent = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Bar.class); + } + }); + ensureWorks(parent, Bar.class); + ensureFails(parent, FAIL_ALL, FooImpl.class, FooBar.class, Foo.class); + + try { + parent.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(FooBar.class); + } + }); + fail("should have failed"); + } catch(CreationException expected) { + assertContains(expected.getMessage(), jitFailed(Foo.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + + Injector child = parent.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Foo.class).to(FooImpl.class); + } + }); + ensureWorks(child, Foo.class, Bar.class); + ensureFails(child, ALLOW_BINDING, FooImpl.class); + ensureInChild(parent, FooImpl.class, Foo.class); + // TODO(sameb): FooBar may or may not be in a child injector, depending on if GC has run. + // We should fix failed child injectors to remove their contents from the parent blacklist + // immediately, rather than waiting on GC to do it. + // FooBar was succesfully inserted into the child injector (and parent blacklist), but then + // JIT bindings it depended on failed, making the child injector invalid. + + Injector grandchild = child.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(FooBar.class); + } + }); + ensureWorks(grandchild, FooBar.class, Foo.class, Bar.class); + ensureFails(grandchild, ALLOW_BINDING, FooImpl.class); + ensureFails(child, ALLOW_BINDING, FooImpl.class); + ensureInChild(parent, FooImpl.class, FooBar.class, Foo.class); + } + + public void testChildInjectorAddsOption() { + Injector parent = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Bar.class); + } + }); + int totalParentBindings = parent.getAllBindings().size(); + + try { + parent.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(FooBar.class); + } + }); + fail("should have failed"); + } catch(CreationException expected) { + assertContains(expected.getMessage(), jitFailed(Foo.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + assertEquals(totalParentBindings, parent.getAllBindings().size()); + + Injector child = parent.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + bind(FooImpl.class); + } + }); + assertEquals(totalParentBindings, parent.getAllBindings().size()); + ensureWorks(child, Foo.class, Bar.class); + + Injector grandchild = child.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(FooBar.class); + } + }); + assertEquals(totalParentBindings, parent.getAllBindings().size()); + ensureWorks(grandchild, FooBar.class, Foo.class, Bar.class); + + // Make sure siblings of children don't inherit each others settings... + // a new child should be able to get FooImpl. + child = parent.createChildInjector(); + ensureWorks(child, FooImpl.class); + } + + public void testPrivateModulesInheritOptions() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + + install(new PrivateModule() { + public void configure() { + bind(FooBar.class); + expose(FooBar.class); + } + }); + } + }); + fail("should have failed"); + } catch(CreationException expected) { + assertContains(expected.getMessage(), jitFailed(Bar.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + binder().requireExplicitBindings(); + + install(new PrivateModule() { + public void configure() { + bind(Foo.class).to(FooImpl.class); + expose(Foo.class); + } + }); + } + }); + ensureInChild(injector, FooImpl.class); + } + + public void testPrivateModuleAddsOption() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).to(FooImpl.class); + + // Fails because FooBar is in the private module, + // and it wants Bar, but Bar would be JIT. + install(new PrivateModule() { + public void configure() { + binder().requireExplicitBindings(); + bind(FooBar.class); + expose(FooBar.class); + } + }); + } + }); + fail("should have failed"); + } catch(CreationException expected) { + assertContains(expected.getMessage(), jitFailed(Bar.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testPrivateModuleSiblingsDontShareOption() { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).to(FooImpl.class); + + install(new PrivateModule() { + public void configure() { + binder().requireExplicitBindings(); + } + }); + + // This works, even though Bar is JIT, + // because the requireExplicitBindings isn't shared + // between sibling private modules. + install(new PrivateModule() { + public void configure() { + bind(FooBar.class); + expose(FooBar.class); + } + }); + } + }); + } + + public void testTypeLiteralsCanBeInjected() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().requireExplicitBindings(); + bind(new TypeLiteral>() {}); + bind(new TypeLiteral>() {}).toInstance(of("bar")); + } + }); + + WantsTypeLiterals foo = injector.getInstance(new Key>() {}); + assertEquals(foo.literal.getRawType(), String.class); + assertEquals(of("bar"), foo.set); + } + + public void testMembersInjectorsCanBeInjected() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().requireExplicitBindings(); + } + + @Provides String data(MembersInjector mi) { + String data = "foo"; + mi.injectMembers(data); + return data; + } + }); + + String data = injector.getInstance(String.class); + assertEquals("foo", data); + } + + public void testJitLinkedBindingInParentFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new PrivateModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).to(FooImpl.class); + } + }); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), jitInParentFailed(FooImpl.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testJitProviderBindingInParentFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new PrivateModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class).toProvider(FooProvider.class); + } + }); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), jitInParentFailed(FooProvider.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testJitImplementedByBindingInParentFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new PrivateModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(ImplBy.class); + } + }); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), jitInParentFailed(ImplByImpl.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + public void testJitProvidedByBindingInParentFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + install(new PrivateModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(ProvBy.class); + } + }); + } + }); + fail("should have failed"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), jitInParentFailed(ProvByProvider.class)); + assertEquals(1, expected.getErrorMessages().size()); + } + } + + private void ensureWorks(Injector injector, Class... classes) { + for(int i = 0; i < classes.length; i++) { + injector.getInstance(classes[i]); + injector.getProvider(classes[i]).get(); + injector.getBinding(classes[i]).getProvider().get(); + } + } + + enum GetBindingCheck { FAIL_ALL, ALLOW_BINDING, ALLOW_BINDING_PROVIDER } + private void ensureFails(Injector injector, GetBindingCheck getBinding, Class... classes) { + for(int i = 0; i < classes.length; i++) { + try { + injector.getInstance(classes[i]); + fail("should have failed tring to retrieve class: " + classes[i]); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), jitFailed(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + + try { + injector.getProvider(classes[i]); + fail("should have failed tring to retrieve class: " + classes[i]); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), jitFailed(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + + if (getBinding == GetBindingCheck.ALLOW_BINDING + || getBinding == GetBindingCheck.ALLOW_BINDING_PROVIDER) { + Binding binding = injector.getBinding(classes[i]); + try { + binding.getProvider(); + if (getBinding != GetBindingCheck.ALLOW_BINDING_PROVIDER) { + fail("should have failed trying to retrieve class: " + classes[i]); + } + } catch(ConfigurationException expected) { + if (getBinding == GetBindingCheck.ALLOW_BINDING_PROVIDER) { + throw expected; + } + assertContains(expected.getMessage(), jitFailed(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + } else { + try { + injector.getBinding(classes[i]); + fail("should have failed tring to retrieve class: " + classes[i]); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), jitFailed(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + } + } + } + + private void ensureInChild(Injector injector, Class... classes) { + for(int i = 0; i < classes.length; i++) { + try { + injector.getInstance(classes[i]); + fail("should have failed tring to retrieve class: " + classes[i]); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), inChildMessage(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + + try { + injector.getProvider(classes[i]); + fail("should have failed tring to retrieve class: " + classes[i]); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), inChildMessage(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + + try { + injector.getBinding(classes[i]); + fail("should have failed tring to retrieve class: " + classes[i]); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), inChildMessage(classes[i])); + assertEquals(1, expected.getErrorMessages().size()); + } + } + } + + private static interface Foo {} + private static class FooImpl implements Foo {} + @Singleton private static class ScopedFooImpl implements Foo {} + private static class WantsScopedFooImpl { + @SuppressWarnings("unused") @Inject ScopedFooImpl scopedFoo; + } + private static class Bar {} + private static class FooBar { + @SuppressWarnings("unused") @Inject Foo foo; + @SuppressWarnings("unused") @Inject Bar bar; + } + private static class ProviderFooBar { + @SuppressWarnings("unused") @Inject Provider foo; + @SuppressWarnings("unused") @Inject Provider bar; + } + private static class FooProvider implements Provider { + public Foo get() { + return new FooImpl(); + } + } + + @ImplementedBy(ImplByImpl.class) + private static interface ImplBy {} + private static class ImplByImpl implements ImplBy {} + + @ImplementedBy(ImplByScopedImpl.class) + private static interface ImplByScoped {} + @Singleton + private static class ImplByScopedImpl implements ImplByScoped {} + + @ProvidedBy(ProvByProvider.class) + private static interface ProvBy {} + private static class ProvByProvider implements Provider { + public ProvBy get() { + return new ProvBy() {}; + } + } + + private static class WantsTypeLiterals { + TypeLiteral literal; + Set set; + + @Inject WantsTypeLiterals(TypeLiteral literal, Set set) { + this.literal = literal; + this.set = set; + + } + } +} diff --git a/src/test/java/com/google/inject/KeyTest.java b/src/test/java/com/google/inject/KeyTest.java new file mode 100644 index 0000000..d9dd943 --- /dev/null +++ b/src/test/java/com/google/inject/KeyTest.java @@ -0,0 +1,321 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.assertEqualsBothWays; +import static com.google.inject.Asserts.assertNotSerializable; +import static com.google.inject.Asserts.awaitClear; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.Dependency; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class KeyTest extends TestCase { + + public void foo(List a, List b) {} + public void bar(Provider> a) {} + @Foo String baz; + List wildcardExtends; + + public void testOfType() { + Key k = Key.get(Object.class, Foo.class); + Key ki = k.ofType(Integer.class); + assertEquals(Integer.class, ki.getRawType()); + assertEquals(Foo.class, ki.getAnnotationType()); + } + + public void testKeyEquality() { + Key> a = new Key>(Foo.class) {}; + Key> b = Key.get(new TypeLiteral>() {}, Foo.class); + assertEqualsBothWays(a, b); + } + + public void testProviderKey() throws NoSuchMethodException { + Key actual = Key.get(getClass().getMethod("foo", List.class, List.class) + .getGenericParameterTypes()[0]).providerKey(); + Key expected = Key.get(getClass().getMethod("bar", Provider.class) + .getGenericParameterTypes()[0]); + assertEqualsBothWays(expected, actual); + assertEquals(expected.toString(), actual.toString()); + } + + public void testTypeEquality() throws Exception { + Method m = getClass().getMethod("foo", List.class, List.class); + Type[] types = m.getGenericParameterTypes(); + assertEquals(types[0], types[1]); + Key> k = new Key>() {}; + assertEquals(types[0], k.getTypeLiteral().getType()); + assertFalse(types[0].equals( + new Key>() {}.getTypeLiteral().getType())); + } + + /** + * Key canonicalizes {@link int.class} to {@code Integer.class}, and + * won't expose wrapper types. + */ + public void testPrimitivesAndWrappersAreEqual() { + Class[] primitives = new Class[] { + boolean.class, byte.class, short.class, int.class, long.class, + float.class, double.class, char.class, void.class + }; + Class[] wrappers = new Class[] { + Boolean.class, Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, Character.class, Void.class + }; + + for (int t = 0; t < primitives.length; t++) { + @SuppressWarnings("unchecked") + Key primitiveKey = Key.get(primitives[t]); + @SuppressWarnings("unchecked") + Key wrapperKey = Key.get(wrappers[t]); + + assertEquals(primitiveKey, wrapperKey); + assertEquals(wrappers[t], primitiveKey.getRawType()); + assertEquals(wrappers[t], wrapperKey.getRawType()); + assertEquals(wrappers[t], primitiveKey.getTypeLiteral().getType()); + assertEquals(wrappers[t], wrapperKey.getTypeLiteral().getType()); + } + + Key integerKey = Key.get(Integer.class); + Key integerKey2 = Key.get(Integer.class, Named.class); + Key integerKey3 = Key.get(Integer.class, Names.named("int")); + + Class intClassLiteral = int.class; + assertEquals(integerKey, Key.get(intClassLiteral)); + assertEquals(integerKey2, Key.get(intClassLiteral, Named.class)); + assertEquals(integerKey3, Key.get(intClassLiteral, Names.named("int"))); + + Type intType = int.class; + assertEquals(integerKey, Key.get(intType)); + assertEquals(integerKey2, Key.get(intType, Named.class)); + assertEquals(integerKey3, Key.get(intType, Names.named("int"))); + + TypeLiteral intTypeLiteral = TypeLiteral.get(int.class); + assertEquals(integerKey, Key.get(intTypeLiteral)); + assertEquals(integerKey2, Key.get(intTypeLiteral, Named.class)); + assertEquals(integerKey3, Key.get(intTypeLiteral, Names.named("int"))); + } + + public void testSerialization() throws IOException, NoSuchFieldException { + assertNotSerializable(Key.get(B.class)); + assertNotSerializable(Key.get(B.class, Names.named("bee"))); + assertNotSerializable(Key.get(B.class, Named.class)); + assertNotSerializable(Key.get(B[].class)); + assertNotSerializable(Key.get(new TypeLiteral, B>>() {})); + assertNotSerializable(Key.get(new TypeLiteral>() {})); + assertNotSerializable(Key.get(Types.listOf(Types.subtypeOf(CharSequence.class)))); + } + + public void testEqualityOfAnnotationTypesAndInstances() throws NoSuchFieldException { + Foo instance = getClass().getDeclaredField("baz").getAnnotation(Foo.class); + Key keyWithInstance = Key.get(String.class, instance); + Key keyWithLiteral = Key.get(String.class, Foo.class); + assertEqualsBothWays(keyWithInstance, keyWithLiteral); + } + + public void testNonBindingAnnotationOnKey() { + try { + Key.get(String.class, Deprecated.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), "java.lang.Deprecated is not a binding annotation. ", + "Please annotate it with @BindingAnnotation."); + } + } + + public void testBindingAnnotationWithoutRuntimeRetention() { + try { + Key.get(String.class, Bar.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), Bar.class.getName() + " is not retained at runtime.", + "Please annotate it with @Retention(RUNTIME)."); + } + } + + void parameterizedWithVariable(List typeWithVariables) {} + + /** Test for issue 186 */ + public void testCannotCreateKeysWithTypeVariables() throws NoSuchMethodException { + ParameterizedType listOfTType = (ParameterizedType) getClass().getDeclaredMethod( + "parameterizedWithVariable", List.class).getGenericParameterTypes()[0]; + + TypeLiteral listOfT = TypeLiteral.get(listOfTType); + try { + Key.get(listOfT); + fail("Guice should not allow keys for java.util.List"); + } catch (ConfigurationException e) { + assertContains(e.getMessage(), + "java.util.List cannot be used as a key; It is not fully specified."); + } + + TypeVariable tType = (TypeVariable) listOfTType.getActualTypeArguments()[0]; + TypeLiteral t = TypeLiteral.get(tType); + try { + Key.get(t); + fail("Guice should not allow keys for T"); + } catch (ConfigurationException e) { + assertContains(e.getMessage(), + "T cannot be used as a key; It is not fully specified."); + } + } + + public void testCannotGetKeyWithUnspecifiedTypeVariables() { + TypeLiteral typeLiteral = KeyTest.createTypeLiteral(); + try { + Key.get(typeLiteral); + fail("Guice should not allow keys for T"); + } catch (ConfigurationException e) { + assertContains(e.getMessage(), + "T cannot be used as a key; It is not fully specified."); + } + } + + private static TypeLiteral createTypeLiteral() { + return new TypeLiteral() {}; + } + + public void testCannotCreateKeySubclassesWithUnspecifiedTypeVariables() { + try { + KeyTest.createKey(); + fail("Guice should not allow keys for T"); + } catch (ConfigurationException e) { + assertContains(e.getMessage(), + "T cannot be used as a key; It is not fully specified."); + } + } + + private static Key createKey() { + return new Key() {}; + } + + interface B {} + + @Retention(RUNTIME) + @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) + @BindingAnnotation @interface Foo {} + + @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) + @BindingAnnotation @interface Bar {} + + class HasTypeParameters & Runnable, C extends Runnable> { + A a; B b; C c; + } + + public void testKeysWithDefaultAnnotations() { + AllDefaults allDefaults = HasAnnotations.class.getAnnotation(AllDefaults.class); + assertEquals(Key.get(Foo.class, allDefaults), Key.get(Foo.class, AllDefaults.class)); + + Marker marker = HasAnnotations.class.getAnnotation(Marker.class); + assertEquals(Key.get(Foo.class, marker), Key.get(Foo.class, Marker.class)); + + Key noDefaults = Key.get(Foo.class, NoDefaults.class); + assertNull(noDefaults.getAnnotation()); + assertEquals(NoDefaults.class, noDefaults.getAnnotationType()); + + Key someDefaults = Key.get(Foo.class, SomeDefaults.class); + assertNull(someDefaults.getAnnotation()); + assertEquals(SomeDefaults.class, someDefaults.getAnnotationType()); + } + + @Retention(RUNTIME) + @BindingAnnotation @interface AllDefaults { + int v1() default 1; + String v2() default "foo"; + } + + @Retention(RUNTIME) + @BindingAnnotation @interface SomeDefaults { + int v1() default 1; + String v2() default "foo"; + Class clazz(); + } + + @Retention(RUNTIME) + @BindingAnnotation @interface NoDefaults { + int value(); + } + + @Retention(RUNTIME) + @BindingAnnotation @interface Marker { + } + + @AllDefaults + @Marker + class HasAnnotations {} + + public void testAnonymousClassesDontHoldRefs() { + final AtomicReference>> stringProvider = + new AtomicReference>>(); + final AtomicReference>> intProvider = + new AtomicReference>>(); + final Object foo = new Object() { + @SuppressWarnings("unused") @Inject List list; + }; + Module module = new AbstractModule() { + @Override protected void configure() { + bind(new Key>() {}).toInstance(new ArrayList()); + bind(new TypeLiteral>() {}).toInstance(new ArrayList()); + + stringProvider.set(getProvider(new Key>() {})); + intProvider.set(binder().getProvider(Dependency.get(new Key>() {}))); + + binder().requestInjection(new TypeLiteral() {}, foo); + } + }; + WeakReference moduleRef = new WeakReference(module); + final Injector injector = Guice.createInjector(module); + module = null; + awaitClear(moduleRef); // Make sure anonymous keys & typeliterals don't hold the module. + + Runnable runner = new Runnable() { + @Override public void run() { + injector.getInstance(new Key>() {}); + injector.getInstance(Key.get(new TypeLiteral>() {})); + } + }; + WeakReference runnerRef = new WeakReference(runner); + runner.run(); + runner = null; + awaitClear(runnerRef); // also make sure anonymous keys & typeliterals don't hold for JITs + } + + static class Typed {} + +} diff --git a/src/test/java/com/google/inject/MembersInjectorTest.java b/src/test/java/com/google/inject/MembersInjectorTest.java new file mode 100644 index 0000000..6bf7589 --- /dev/null +++ b/src/test/java/com/google/inject/MembersInjectorTest.java @@ -0,0 +1,287 @@ +/** + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; + +import com.google.inject.name.Names; +import com.google.inject.util.Providers; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class MembersInjectorTest extends TestCase { + + private static final A uninjectableA = new A() { + @Override void doNothing() { + throw new AssertionFailedError(); + } + }; + + private static final B uninjectableB = new B() { + @Override void doNothing() { + throw new AssertionFailedError(); + } + }; + + private static final C myFavouriteC = new C(); + + public void testMembersInjectorFromBinder() { + final AtomicReference>> aMembersInjectorReference + = new AtomicReference>>(); + final AtomicReference> bMembersInjectorReference + = new AtomicReference>(); + + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + MembersInjector> aMembersInjector = getMembersInjector(new TypeLiteral>() {}); + try { + aMembersInjector.injectMembers(uninjectableA); + fail(); + } catch (IllegalStateException expected) { + assertContains(expected.getMessage(), + "This MembersInjector cannot be used until the Injector has been created."); + } + + MembersInjector bMembersInjector = getMembersInjector(B.class); + try { + bMembersInjector.injectMembers(uninjectableB); + fail(); + } catch (IllegalStateException expected) { + assertContains(expected.getMessage(), + "This MembersInjector cannot be used until the Injector has been created."); + } + + aMembersInjectorReference.set(aMembersInjector); + bMembersInjectorReference.set(bMembersInjector); + + assertEquals("MembersInjector", + getMembersInjector(String.class).toString()); + + bind(C.class).toInstance(myFavouriteC); + } + }); + + A injectableA = new A(); + aMembersInjectorReference.get().injectMembers(injectableA); + assertSame(myFavouriteC, injectableA.t); + assertSame(myFavouriteC, injectableA.b.c); + + B injectableB = new B(); + bMembersInjectorReference.get().injectMembers(injectableB); + assertSame(myFavouriteC, injectableB.c); + + B anotherInjectableB = new B(); + bMembersInjectorReference.get().injectMembers(anotherInjectableB); + assertSame(myFavouriteC, anotherInjectableB.c); + } + + public void testMembersInjectorFromInjector() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(C.class).toInstance(myFavouriteC); + } + }); + + MembersInjector> aMembersInjector + = injector.getMembersInjector(new TypeLiteral>() {}); + MembersInjector bMembersInjector = injector.getMembersInjector(B.class); + + A injectableA = new A(); + aMembersInjector.injectMembers(injectableA); + assertSame(myFavouriteC, injectableA.t); + assertSame(myFavouriteC, injectableA.b.c); + + B injectableB = new B(); + bMembersInjector.injectMembers(injectableB); + assertSame(myFavouriteC, injectableB.c); + + B anotherInjectableB = new B(); + bMembersInjector.injectMembers(anotherInjectableB); + assertSame(myFavouriteC, anotherInjectableB.c); + + assertEquals("MembersInjector", + injector.getMembersInjector(String.class).toString()); + } + + public void testMembersInjectorWithNonInjectedTypes() { + Injector injector = Guice.createInjector(); + + MembersInjector membersInjector + = injector.getMembersInjector(NoInjectedMembers.class); + + membersInjector.injectMembers(new NoInjectedMembers()); + membersInjector.injectMembers(new NoInjectedMembers()); + } + + public void testInjectionFailure() { + Injector injector = Guice.createInjector(); + + MembersInjector membersInjector + = injector.getMembersInjector(InjectionFailure.class); + + try { + membersInjector.injectMembers(new InjectionFailure()); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "1) Error injecting method, java.lang.ClassCastException: whoops, failure #1"); + } + } + + public void testInjectionAppliesToSpecifiedType() { + Injector injector = Guice.createInjector(); + + MembersInjector membersInjector = injector.getMembersInjector(Object.class); + membersInjector.injectMembers(new InjectionFailure()); + } + + public void testInjectingMembersInjector() { + InjectsMembersInjector injectsMembersInjector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(C.class).toInstance(myFavouriteC); + } + }).getInstance(InjectsMembersInjector.class); + + A a = new A(); + injectsMembersInjector.aMembersInjector.injectMembers(a); + assertSame(myFavouriteC, a.t); + assertSame(myFavouriteC, a.b.c); + } + + public void testCannotBindMembersInjector() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(MembersInjector.class).toProvider(Providers.of(null)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Binding to core guice framework type is not allowed: MembersInjector."); + } + + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(new TypeLiteral>>() {}) + .toProvider(Providers.>>of(null)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Binding to core guice framework type is not allowed: MembersInjector."); + } + } + + public void testInjectingMembersInjectorWithErrorsInDependencies() { + try { + Guice.createInjector().getInstance(InjectsBrokenMembersInjector.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) No implementation for " + Unimplemented.class.getName() + " was bound.", + "while locating " + Unimplemented.class.getName(), + "for field at " + A.class.getName() + ".t(", + "while locating com.google.inject.MembersInjector<", + "for field at " + InjectsBrokenMembersInjector.class.getName() + ".aMembersInjector(", + "while locating " + InjectsBrokenMembersInjector.class.getName()); + } + } + + public void testLookupMembersInjectorBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(C.class).toInstance(myFavouriteC); + } + }); + MembersInjector> membersInjector = + injector.getInstance(new Key>>() {}); + + A a = new A(); + membersInjector.injectMembers(a); + assertSame(myFavouriteC, a.t); + assertSame(myFavouriteC, a.b.c); + + assertEquals("MembersInjector", + injector.getInstance(new Key>() {}).toString()); + } + + public void testGettingRawMembersInjector() { + Injector injector = Guice.createInjector(); + try { + injector.getInstance(MembersInjector.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "Cannot inject a MembersInjector that has no type parameter"); + } + } + + public void testGettingAnnotatedMembersInjector() { + Injector injector = Guice.createInjector(); + try { + injector.getInstance(new Key>(Names.named("foo")) {}); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) No implementation for com.google.inject.MembersInjector " + + "annotated with @com.google.inject.name.Named(value=foo) was bound."); + } + } + + static class A { + @Inject B b; + @Inject T t; + @Inject void doNothing() {} + } + + static class B { + @Inject C c; + @Inject void doNothing() {} + } + + static class C {} + + static class NoInjectedMembers {} + + static class InjectionFailure { + int failures = 0; + + @Inject void fail() { + throw new ClassCastException("whoops, failure #" + (++failures)); + } + } + + static class InjectsMembersInjector { + @Inject MembersInjector> aMembersInjector; + @Inject A ab; + } + + static class InjectsBrokenMembersInjector { + @Inject MembersInjector> aMembersInjector; + } + + static interface Unimplemented {} +} diff --git a/src/test/java/com/google/inject/ModuleTest.java b/src/test/java/com/google/inject/ModuleTest.java new file mode 100644 index 0000000..e938da5 --- /dev/null +++ b/src/test/java/com/google/inject/ModuleTest.java @@ -0,0 +1,56 @@ +// Copyright 2007 Google Inc. All Rights Reserved. + +package com.google.inject; + +import junit.framework.TestCase; + +/** + * Tests relating to modules. + * + * @author kevinb + */ +public class ModuleTest extends TestCase { + + static class A implements Module { + public void configure(Binder binder) { + binder.bind(X.class); + binder.install(new B()); + binder.install(new C()); + } + } + + static class B implements Module { + public void configure(Binder binder) { + binder.bind(Y.class); + binder.install(new D()); + } + } + + static class C implements Module { + public void configure(Binder binder) { + binder.bind(Z.class); + binder.install(new D()); + } + } + + static class D implements Module { + public void configure(Binder binder) { + binder.bind(W.class); + } + @Override public boolean equals(Object obj) { + return obj.getClass() == D.class; // we're all equal in the eyes of guice + } + @Override public int hashCode() { + return D.class.hashCode(); + } + } + + static class X {} + static class Y {} + static class Z {} + static class W {} + + public void testDiamond() throws Exception { + Guice.createInjector(new A()); + } +} diff --git a/src/test/java/com/google/inject/ModulesTest.java b/src/test/java/com/google/inject/ModulesTest.java new file mode 100644 index 0000000..b32e4f6 --- /dev/null +++ b/src/test/java/com/google/inject/ModulesTest.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import com.google.common.collect.ImmutableList; +import com.google.inject.spi.ElementSource; +import com.google.inject.util.Modules; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class ModulesTest extends TestCase { + + public void testCombineVarargs() { + Module combined = Modules.combine(newModule(1), newModule(2L), newModule((short) 3)); + Injector injector = Guice.createInjector(combined); + assertEquals(1, injector.getInstance(Integer.class).intValue()); + assertEquals(2L, injector.getInstance(Long.class).longValue()); + assertEquals(3, injector.getInstance(Short.class).shortValue()); + } + + public void testCombineIterable() { + Iterable modules = Arrays.asList(newModule(1), newModule(2L), newModule((short) 3)); + Injector injector = Guice.createInjector(Modules.combine(modules)); + assertEquals(1, injector.getInstance(Integer.class).intValue()); + assertEquals(2, injector.getInstance(Long.class).longValue()); + assertEquals(3, injector.getInstance(Short.class).shortValue()); + } + + /** + * The module returned by Modules.combine shouldn't show up in binder sources. + */ + public void testCombineSources() { + final Module m1 = newModule(1); + final Module m2 = newModule(2L); + final Module combined1 = Modules.combine(m1, m2); + Module skipSourcesModule = new AbstractModule() { + @Override protected void configure() { + install(combined1); + } + }; + final Module combined2 = Modules.combine(skipSourcesModule); + Injector injector = Guice.createInjector(combined2); + ElementSource source = (ElementSource) injector.getBinding(Integer.class).getSource(); + assertEquals(source.getModuleClassNames().size(), 4); + assertEquals(ImmutableList.of(m1.getClass().getName(), + combined1.getClass().getName(), skipSourcesModule.getClass().getName(), + combined2.getClass().getName()), source.getModuleClassNames()); + StackTraceElement stackTraceElement = (StackTraceElement) source.getDeclaringSource(); + assertEquals(skipSourcesModule.getClass().getName(), stackTraceElement.getClassName()); + } + + private Module newModule(final T toBind) { + return new AbstractModule() { + protected void configure() { + @SuppressWarnings("unchecked") // getClass always needs a cast + Class tClass = (Class) toBind.getClass(); + binder().skipSources(getClass()).bind(tClass).toInstance(toBind); + } + }; + } +} diff --git a/src/test/java/com/google/inject/NullableInjectionPointTest.java b/src/test/java/com/google/inject/NullableInjectionPointTest.java new file mode 100644 index 0000000..9cf22cc --- /dev/null +++ b/src/test/java/com/google/inject/NullableInjectionPointTest.java @@ -0,0 +1,251 @@ +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; + +import junit.framework.TestCase; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class NullableInjectionPointTest extends TestCase { + + public void testInjectNullIntoNotNullableConstructor() { + try { + createInjector().getInstance(FooConstructor.class); + fail("Injecting null should fail with an error"); + } + catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "null returned by binding at " + getClass().getName(), + "parameter 0 of " + FooConstructor.class.getName() + ".() is not @Nullable"); + } + } + + public void testInjectNullIntoNotNullableMethod() { + try { + createInjector().getInstance(FooMethod.class); + fail("Injecting null should fail with an error"); + } + catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "null returned by binding at " + getClass().getName(), + "parameter 0 of " + FooMethod.class.getName() + ".setFoo() is not @Nullable"); + } + } + + public void testInjectNullIntoNotNullableField() { + try { + createInjector().getInstance(FooField.class); + fail("Injecting null should fail with an error"); + } + catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "null returned by binding at " + getClass().getName(), + " but " + FooField.class.getName() + ".foo is not @Nullable"); + } + } + + /** + * Provider.getInstance() is allowed to return null via direct calls to + * getInstance(). + */ + public void testGetInstanceOfNull() { + assertNull(createInjector().getInstance(Foo.class)); + } + + public void testInjectNullIntoNullableConstructor() { + NullableFooConstructor nfc + = createInjector().getInstance(NullableFooConstructor.class); + assertNull(nfc.foo); + } + + public void testInjectNullIntoNullableMethod() { + NullableFooMethod nfm + = createInjector().getInstance(NullableFooMethod.class); + assertNull(nfm.foo); + } + + public void testInjectNullIntoNullableField() { + NullableFooField nff + = createInjector().getInstance(NullableFooField.class); + assertNull(nff.foo); + } + + public void testInjectNullIntoCustomNullableConstructor() { + CustomNullableFooConstructor nfc + = createInjector().getInstance(CustomNullableFooConstructor.class); + assertNull(nfc.foo); + } + + public void testInjectNullIntoCustomNullableMethod() { + CustomNullableFooMethod nfm + = createInjector().getInstance(CustomNullableFooMethod.class); + assertNull(nfm.foo); + } + + public void testInjectNullIntoCustomNullableField() { + CustomNullableFooField nff + = createInjector().getInstance(CustomNullableFooField.class); + assertNull(nff.foo); + } + + private Injector createInjector() { + return Guice.createInjector( + new AbstractModule() { + protected void configure() { + bind(Foo.class).toProvider(new Provider() { + public Foo get() { + return null; + } + }); + } + }); + } + + /** + * We haven't decided on what the desired behaviour of this test should be... + */ + public void testBindNullToInstance() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toInstance(null); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Binding to null instances is not allowed.", + "at " + getClass().getName(), getDeclaringSourcePart(getClass())); + } + } + + public void testBindNullToProvider() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toProvider(new Provider() { + public Foo get() { + return null; + } + }); + } + }); + assertNull(injector.getInstance(NullableFooField.class).foo); + assertNull(injector.getInstance(CustomNullableFooField.class).foo); + + try { + injector.getInstance(FooField.class); + } + catch(ProvisionException expected) { + assertContains(expected.getMessage(), "null returned by binding at"); + } + } + + public void testBindScopedNull() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toProvider(new Provider() { + public Foo get() { + return null; + } + }).in(Scopes.SINGLETON); + } + }); + assertNull(injector.getInstance(NullableFooField.class).foo); + assertNull(injector.getInstance(CustomNullableFooField.class).foo); + + try { + injector.getInstance(FooField.class); + } + catch(ProvisionException expected) { + assertContains(expected.getMessage(), "null returned by binding at"); + } + } + + public void testBindNullAsEagerSingleton() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toProvider(new Provider() { + public Foo get() { + return null; + } + }).asEagerSingleton(); + } + }); + assertNull(injector.getInstance(NullableFooField.class).foo); + assertNull(injector.getInstance(CustomNullableFooField.class).foo); + + try { + injector.getInstance(FooField.class); + fail(); + } catch(ProvisionException expected) { + assertContains(expected.getMessage(), "null returned by binding " + + "at com.google.inject.NullableInjectionPointTest"); + } + } + + static class Foo { } + + static class FooConstructor { + @Inject FooConstructor(Foo foo) { } + } + static class FooField { + @Inject Foo foo; + } + static class FooMethod { + @Inject + void setFoo(Foo foo) { } + } + + static class NullableFooConstructor { + Foo foo; + @Inject NullableFooConstructor(@Nullable Foo foo) { + this.foo = foo; + } + } + static class NullableFooField { + @Inject @Nullable Foo foo; + } + static class NullableFooMethod { + Foo foo; + @Inject void setFoo(@Nullable Foo foo) { + this.foo = foo; + } + } + + static class CustomNullableFooConstructor { + Foo foo; + @Inject CustomNullableFooConstructor(@Namespace.Nullable Foo foo) { + this.foo = foo; + } + } + + static class CustomNullableFooField { + @Inject @Namespace.Nullable Foo foo; + } + static class CustomNullableFooMethod { + Foo foo; + @Inject void setFoo(@Namespace.Nullable Foo foo) { + this.foo = foo; + } + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.FIELD}) + @interface Nullable { } + + static interface Namespace { + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.FIELD}) + @interface Nullable { } + } +} diff --git a/src/test/java/com/google/inject/OptionalBindingTest.java b/src/test/java/com/google/inject/OptionalBindingTest.java new file mode 100644 index 0000000..a62191c --- /dev/null +++ b/src/test/java/com/google/inject/OptionalBindingTest.java @@ -0,0 +1,298 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; + +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import junit.framework.TestCase; + +/** + * This test verifies the ways things are injected (ie. getInstance(), + * injectMembers(), bind to instance, and bind to provider instance) for all + * states of optional bindings (fields, methods, multiple-argument methods, + * provider fields, provider methods, constructors). + * + * @author jessewilson@google.com (Jesse Wilson) + */ +public class OptionalBindingTest extends TestCase { + + private static final A injectA = new A() {}; + private static final B injectB = new B() {}; + private static final C injectC = new C() {}; + private static final D injectD = new D() {}; + private static final E injectE = new E() {}; + private static final F injectF = new F() {}; + private static final G injectG = new G() {}; + + private Module everythingModule = new AbstractModule() { + protected void configure() { + bind(A.class).toInstance(injectA); + bind(B.class).toInstance(injectB); + bind(C.class).toInstance(injectC); + bind(D.class).toInstance(injectD); + bind(E.class).annotatedWith(Names.named("e")).toInstance(injectE); + bind(F.class).toInstance(injectF); + bind(G.class).toInstance(injectG); + } + }; + + private Module partialModule = new AbstractModule() { + protected void configure() { + bind(C.class).toInstance(new C() {}); + } + }; + + private Module toInstanceModule = new AbstractModule() { + protected void configure() { + bind(HasOptionalInjections.class) + .toInstance(new HasOptionalInjections()); + } + }; + + private Module toProviderInstanceModule = new AbstractModule() { + protected void configure() { + bind(HasOptionalInjections.class) + .toProvider(new HasOptionalInjectionsProvider()); + } + }; + + private Module toProviderModule = new AbstractModule() { + protected void configure() { + bind(HasOptionalInjections.class) + .toProvider(HasOptionalInjectionsProvider.class); + } + }; + + public void testEverythingInjectorGetInstance() { + Guice.createInjector(everythingModule) + .getInstance(HasOptionalInjections.class) + .assertEverythingInjected(); + } + + public void testPartialInjectorGetInstance() { + Guice.createInjector(partialModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testNothingInjectorGetInstance() { + Guice.createInjector() + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testEverythingInjectorInjectMembers() { + HasOptionalInjections instance = new HasOptionalInjections(); + Guice.createInjector(everythingModule).injectMembers(instance); + instance.assertEverythingInjected(); + } + + public void testPartialInjectorInjectMembers() { + HasOptionalInjections instance = new HasOptionalInjections(); + Guice.createInjector(partialModule).injectMembers(instance); + instance.assertNothingInjected(); + } + + public void testNothingInjectorInjectMembers() { + HasOptionalInjections instance = new HasOptionalInjections(); + Guice.createInjector().injectMembers(instance); + instance.assertNothingInjected(); + } + + public void testEverythingInjectorToInstance() { + Guice.createInjector(everythingModule, toInstanceModule) + .getInstance(HasOptionalInjections.class) + .assertEverythingInjected(); + } + + public void testPartialInjectorToInstance() { + Guice.createInjector(partialModule, toInstanceModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testNothingInjectorToInstance() { + Guice.createInjector(toInstanceModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testEverythingInjectorToProviderInstance() { + Guice.createInjector(everythingModule, toProviderInstanceModule) + .getInstance(HasOptionalInjections.class) + .assertEverythingInjected(); + } + + public void testPartialInjectorToProviderInstance() { + Guice.createInjector(partialModule, toProviderInstanceModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testNothingInjectorToProviderInstance() { + Guice.createInjector(toProviderInstanceModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testEverythingInjectorToProvider() { + Guice.createInjector(everythingModule, toProviderModule) + .getInstance(HasOptionalInjections.class) + .assertEverythingInjected(); + } + + public void testPartialInjectorToProvider() { + Guice.createInjector(partialModule, toProviderModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + public void testNothingInjectorToProvider() { + Guice.createInjector(toProviderModule) + .getInstance(HasOptionalInjections.class) + .assertNothingInjected(); + } + + static class HasOptionalInjections { + A originalA = new A() {}; + @Inject(optional=true) A a = originalA; // field injection + B b; // method injection with one argument + C c; // method injection with two arguments + D d; // method injection with two arguments + E e; // annotated injection + @Inject(optional=true) Provider fProvider; // provider + Provider gProvider; // method injection of provider + boolean invoked0, invoked1, invoked2, invokedAnnotated, invokeProvider; + + @Inject(optional=true) void methodInjectZeroArguments() { + invoked0 = true; + } + + @Inject(optional=true) void methodInjectOneArgument(B b) { + this.b = b; + invoked1 = true; + } + + @Inject(optional=true) void methodInjectTwoArguments(C c, D d) { + this.c = c; + this.d = d; + invoked2 = true; + } + + @Inject(optional=true) void methodInjectAnnotated(@Named("e") E e) { + this.e = e; + invokedAnnotated = true; + } + + @Inject(optional=true) void methodInjectProvider(Provider gProvider) { + this.gProvider = gProvider; + invokeProvider = true; + } + + void assertNothingInjected() { + assertSame(originalA, a); + assertNull(b); + assertNull(c); + assertNull(d); + assertNull(e); + assertNull(fProvider); + assertNull(gProvider); + assertTrue(invoked0); + assertFalse(invoked1); + assertFalse(invoked2); + assertFalse(invokedAnnotated); + } + + public void assertEverythingInjected() { + assertNotSame(injectA, originalA); + assertSame(injectA, a); + assertSame(injectB, b); + assertSame(injectC, c); + assertSame(injectD, d); + assertSame(injectE, e); + assertSame(injectF, fProvider.get()); + assertSame(injectG, gProvider.get()); + assertTrue(invoked0); + assertTrue(invoked1); + assertTrue(invoked2); + assertTrue(invokedAnnotated); + } + } + + static class HasOptionalInjectionsProvider + extends HasOptionalInjections implements Provider { + public HasOptionalInjections get() { + return this; + } + } + + public void testOptionalConstructorBlowsUp() { + try { + Guice.createInjector().getInstance(HasOptionalConstructor.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), "OptionalBindingTest$HasOptionalConstructor.() " + + "is annotated @Inject(optional=true), but constructors cannot be optional."); + } + } + + static class HasOptionalConstructor { + // Suppress compiler errors by the error-prone checker InjectedConstructorAnnotations, + // which catches optional injected constructors. + @SuppressWarnings("InjectedConstructorAnnotations") + @Inject(optional=true) + HasOptionalConstructor() {} + } + + @Inject(optional=true) static A staticInjectA; + + public void testStaticInjection() { + staticInjectA = injectA; + Guice.createInjector(new AbstractModule() { + protected void configure() { + requestStaticInjection(OptionalBindingTest.class); + } + }); + assertSame(staticInjectA, injectA); + } + + /** + * Test for bug 107, where we weren't doing optional injection properly for + * indirect injections. + */ + public void testIndirectOptionalInjection() { + Indirect indirect = Guice.createInjector().getInstance(Indirect.class); + assertNotNull(indirect.hasOptionalInjections); + indirect.hasOptionalInjections.assertNothingInjected(); + } + + static class Indirect { + @Inject HasOptionalInjections hasOptionalInjections; + } + + interface A {} + interface B {} + interface C {} + interface D {} + interface E {} + interface F {} + interface G {} +} diff --git a/src/test/java/com/google/inject/ParentInjectorTest.java b/src/test/java/com/google/inject/ParentInjectorTest.java new file mode 100644 index 0000000..7c7182d --- /dev/null +++ b/src/test/java/com/google/inject/ParentInjectorTest.java @@ -0,0 +1,305 @@ +/* +Copyright (C) 2007 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.inject.matcher.Matchers; +import com.google.inject.name.Names; +import com.google.inject.spi.TypeConverter; + +import junit.framework.TestCase; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class ParentInjectorTest extends TestCase { + + public void testParentAndChildCannotShareExplicitBindings() { + Injector parent = Guice.createInjector(bindsA); + try { + parent.createChildInjector(bindsA); + fail("Created the same explicit binding on both parent and child"); + } catch (CreationException e) { + assertContains(e.getMessage(), "A binding to ", A.class.getName(), " was already configured", + " at ", getClass().getName(), getDeclaringSourcePart(getClass()), + " at ", getClass().getName(), getDeclaringSourcePart(getClass())); + } + } + + public void testParentJitBindingWontClobberChildBinding() { + Injector parent = Guice.createInjector(); + parent.createChildInjector(bindsA); + try { + parent.getInstance(A.class); + fail("Created a just-in-time binding on the parent that's the same as a child's binding"); + } catch (ConfigurationException e) { + assertContains(e.getMessage(), + "Unable to create binding for " + A.class.getName(), + "It was already configured on one or more child injectors or private modules", + "bound at " + bindsA.getClass().getName() + ".configure(", + "If it was in a PrivateModule, did you forget to expose the binding?", + "while locating " + A.class.getName()); + } + } + + public void testChildCannotBindToAParentJitBinding() { + Injector parent = Guice.createInjector(); + parent.getInstance(A.class); + try { + parent.createChildInjector(bindsA); + fail(); + } catch(CreationException ce) { + assertContains(Iterables.getOnlyElement(ce.getErrorMessages()).getMessage(), + "A just-in-time binding to " + A.class.getName() + " was already configured on a parent injector."); + } + } + + public void testJustInTimeBindingsAreSharedWithParentIfPossible() { + Injector parent = Guice.createInjector(); + Injector child = parent.createChildInjector(); + assertSame(child.getInstance(A.class), parent.getInstance(A.class)); + + Injector anotherChild = parent.createChildInjector(); + assertSame(anotherChild.getInstance(A.class), parent.getInstance(A.class)); + + Injector grandchild = child.createChildInjector(); + assertSame(grandchild.getInstance(A.class), parent.getInstance(A.class)); + } + + public void testBindingsInherited() { + Injector parent = Guice.createInjector(bindsB); + Injector child = parent.createChildInjector(); + assertSame(RealB.class, child.getInstance(B.class).getClass()); + } + + public void testGetParent() { + Injector top = Guice.createInjector(bindsA); + Injector middle = top.createChildInjector(bindsB); + Injector bottom = middle.createChildInjector(); + assertSame(middle, bottom.getParent()); + assertSame(top, middle.getParent()); + assertNull(top.getParent()); + } + + public void testChildBindingsNotVisibleToParent() { + Injector parent = Guice.createInjector(); + parent.createChildInjector(bindsB); + try { + parent.getBinding(B.class); + fail(); + } catch (ConfigurationException expected) { + } + } + + public void testScopesInherited() { + Injector parent = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindScope(MyScope.class, Scopes.SINGLETON); + } + }); + Injector child = parent.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(A.class).in(MyScope.class); + } + }); + assertSame(child.getInstance(A.class), child.getInstance(A.class)); + } + + public void testTypeConvertersInherited() { + Injector parent = Guice.createInjector(bindListConverterModule); + Injector child = parent.createChildInjector(bindStringNamedB); + + assertEquals(ImmutableList.of(), child.getInstance(Key.get(List.class, Names.named("B")))); + } + + public void testTypeConvertersConflicting() { + Injector parent = Guice.createInjector(bindListConverterModule); + Injector child = parent.createChildInjector(bindListConverterModule, bindStringNamedB); + + try { + child.getInstance(Key.get(List.class, Names.named("B"))); + fail(); + } catch (ConfigurationException expected) { + Asserts.assertContains(expected.getMessage(), "Multiple converters can convert"); + } + } + + public void testInjectorInjectionSpanningInjectors() { + Injector parent = Guice.createInjector(); + Injector child = parent.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(D.class); + } + }); + + D d = child.getInstance(D.class); + assertSame(d.injector, child); + + E e = child.getInstance(E.class); + assertSame(e.injector, parent); + } + + public void testSeveralLayersOfHierarchy() { + Injector top = Guice.createInjector(bindsA); + Injector left = top.createChildInjector(); + Injector leftLeft = left.createChildInjector(bindsD); + Injector right = top.createChildInjector(bindsD); + + assertSame(leftLeft, leftLeft.getInstance(D.class).injector); + assertSame(right, right.getInstance(D.class).injector); + assertSame(top, leftLeft.getInstance(E.class).injector); + assertSame(top.getInstance(A.class), leftLeft.getInstance(A.class)); + + Injector leftRight = left.createChildInjector(bindsD); + assertSame(leftRight, leftRight.getInstance(D.class).injector); + + try { + top.getInstance(D.class); + fail(); + } catch (ConfigurationException expected) { + } + + try { + left.getInstance(D.class); + fail(); + } catch (ConfigurationException expected) { + } + } + + public void testScopeBoundInChildInjectorOnly() { + Injector parent = Guice.createInjector(); + Injector child = parent.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bindScope(MyScope.class, Scopes.SINGLETON); + } + }); + + try { + parent.getProvider(F.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "No scope is bound to com.google.inject.ParentInjectorTest$MyScope.", + "at " + F.class.getName() + ".class(", + " while locating " + F.class.getName()); + } + + assertNotNull(child.getProvider(F.class).get()); + } + + public void testErrorInParentButOkayInChild() { + Injector parent = Guice.createInjector(); + Injector childInjector = parent.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bindScope(MyScope.class, Scopes.SINGLETON); + bind(Object.class).to(F.class); + } + }); + Object one = childInjector.getInstance(Object.class); + Object two = childInjector.getInstance(Object.class); + assertSame(one, two); + } + + public void testErrorInParentAndChild() { + Injector parent = Guice.createInjector(); + Injector childInjector = parent.createChildInjector(); + + try { + childInjector.getInstance(G.class); + fail(); + } catch(ConfigurationException expected) { + assertContains(expected.getMessage(), "No scope is bound to " + MyScope.class.getName(), + "at " + F.class.getName() + ".class(", + " while locating " + G.class.getName()); + } + } + + @Singleton + static class A {} + + private final Module bindsA = new AbstractModule() { + @Override protected void configure() { + bind(A.class).toInstance(new A()); + } + }; + + interface B {} + static class RealB implements B {} + + private final Module bindsB = new AbstractModule() { + @Override protected void configure() { + bind(B.class).to(RealB.class); + } + }; + + @Target(TYPE) @Retention(RUNTIME) @ScopeAnnotation + public @interface MyScope {} + + private final TypeConverter listConverter = new TypeConverter() { + public Object convert(String value, TypeLiteral toType) { + return ImmutableList.of(); + } + }; + + private final Module bindListConverterModule = new AbstractModule() { + @Override protected void configure() { + convertToTypes(Matchers.any(), listConverter); + } + }; + + private final Module bindStringNamedB = new AbstractModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(Names.named("B")).toInstance("buzz"); + } + }; + + public static class C { + public A interceptedMethod() { + return new A(); + } + } + + static class D { + @Inject Injector injector; + } + + static class E { + @Inject Injector injector; + } + + private final Module bindsD = new AbstractModule() { + @Override protected void configure() { + bind(D.class); + } + }; + + @MyScope + static class F implements G {} + + @ImplementedBy(F.class) + interface G {} +} diff --git a/src/test/java/com/google/inject/PrivateModuleTest.java b/src/test/java/com/google/inject/PrivateModuleTest.java new file mode 100644 index 0000000..226d73d --- /dev/null +++ b/src/test/java/com/google/inject/PrivateModuleTest.java @@ -0,0 +1,564 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static com.google.inject.name.Names.named; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.ExposedBinding; +import com.google.inject.spi.PrivateElements; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class PrivateModuleTest extends TestCase { + + public void testBasicUsage() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(named("a")).toInstance("public"); + + install(new PrivateModule() { + @Override public void configure() { + bind(String.class).annotatedWith(named("b")).toInstance("i"); + + bind(AB.class).annotatedWith(named("one")).to(AB.class); + expose(AB.class).annotatedWith(named("one")); + } + }); + + install(new PrivateModule() { + @Override public void configure() { + bind(String.class).annotatedWith(named("b")).toInstance("ii"); + + bind(AB.class).annotatedWith(named("two")).to(AB.class); + expose(AB.class).annotatedWith(named("two")); + } + }); + } + }); + + AB ab1 = injector.getInstance(Key.get(AB.class, named("one"))); + assertEquals("public", ab1.a); + assertEquals("i", ab1.b); + + AB ab2 = injector.getInstance(Key.get(AB.class, named("two"))); + assertEquals("public", ab2.a); + assertEquals("ii", ab2.b); + } + + public void testWithoutPrivateModules() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + PrivateBinder bindA = binder().newPrivateBinder(); + bindA.bind(String.class).annotatedWith(named("a")).toInstance("i"); + bindA.expose(String.class).annotatedWith(named("a")); + bindA.bind(String.class).annotatedWith(named("c")).toInstance("private to A"); + + PrivateBinder bindB = binder().newPrivateBinder(); + bindB.bind(String.class).annotatedWith(named("b")).toInstance("ii"); + bindB.expose(String.class).annotatedWith(named("b")); + bindB.bind(String.class).annotatedWith(named("c")).toInstance("private to B"); + } + }); + + assertEquals("i", injector.getInstance(Key.get(String.class, named("a")))); + assertEquals("ii", injector.getInstance(Key.get(String.class, named("b")))); + } + + public void testMisplacedExposedAnnotation() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() {} + + @Provides @Exposed + String provideString() { + return "i"; + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "Cannot expose java.lang.String on a standard binder. ", + "Exposed bindings are only applicable to private binders.", + " at " + PrivateModuleTest.class.getName(), "provideString("); + } + } + + public void testMisplacedExposeStatement() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + ((PrivateBinder) binder()).expose(String.class).annotatedWith(named("a")); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), "Cannot expose java.lang.String on a standard binder. ", + "Exposed bindings are only applicable to private binders.", + " at " + PrivateModuleTest.class.getName(), getDeclaringSourcePart(getClass())); + } + } + + public void testPrivateModulesAndProvidesMethods() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + install(new PrivateModule() { + @Override public void configure() { + expose(String.class).annotatedWith(named("a")); + } + + @Provides @Named("a") String providePublicA() { + return "i"; + } + + @Provides @Named("b") String providePrivateB() { + return "private"; + } + }); + + install(new PrivateModule() { + @Override public void configure() {} + + @Provides @Named("c") String providePrivateC() { + return "private"; + } + + @Provides @Exposed @Named("d") String providePublicD() { + return "ii"; + } + }); + } + }); + + assertEquals("i", injector.getInstance(Key.get(String.class, named("a")))); + + try { + injector.getInstance(Key.get(String.class, named("b"))); + fail(); + } catch(ConfigurationException expected) { + } + + try { + injector.getInstance(Key.get(String.class, named("c"))); + fail(); + } catch(ConfigurationException expected) { + } + + assertEquals("ii", injector.getInstance(Key.get(String.class, named("d")))); + } + + public void testCannotBindAKeyExportedByASibling() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + install(new PrivateModule() { + @Override public void configure() { + bind(String.class).toInstance("public"); + expose(String.class); + } + }); + + install(new PrivateModule() { + @Override public void configure() { + bind(String.class).toInstance("private"); + } + }); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "A binding to java.lang.String was already configured at ", + getClass().getName(), getDeclaringSourcePart(getClass()), + " at " + getClass().getName(), getDeclaringSourcePart(getClass())); + } + } + + public void testExposeButNoBind() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(named("a")).toInstance("a"); + bind(String.class).annotatedWith(named("b")).toInstance("b"); + + install(new PrivateModule() { + @Override public void configure() { + expose(AB.class); + } + }); + } + }); + fail("AB was exposed but not bound"); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Could not expose() " + AB.class.getName() + ", it must be explicitly bound", + getDeclaringSourcePart(getClass())); + } + } + + /** + * Ensure that when we've got errors in different private modules, Guice presents all errors + * in a unified message. + */ + public void testMessagesFromPrivateModulesAreNicelyIntegrated() { + try { + Guice.createInjector( + new PrivateModule() { + @Override public void configure() { + bind(C.class); + } + }, + new PrivateModule() { + @Override public void configure() { + bind(AB.class); + } + } + ); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) No implementation for " + C.class.getName() + " was bound.", + "at " + getClass().getName(), getDeclaringSourcePart(getClass()), + "2) No implementation for " + String.class.getName(), "Named(value=a) was bound.", + "for field at " + AB.class.getName() + ".a(", + "3) No implementation for " + String.class.getName(), "Named(value=b) was bound.", + "for field at " + AB.class.getName() + ".b(", + "3 errors"); + } + } + + public void testNestedPrivateInjectors() { + Injector injector = Guice.createInjector(new PrivateModule() { + @Override public void configure() { + expose(String.class); + + install(new PrivateModule() { + @Override public void configure() { + bind(String.class).toInstance("nested"); + expose(String.class); + } + }); + } + }); + + assertEquals("nested", injector.getInstance(String.class)); + } + + public void testInstallingRegularModulesFromPrivateModules() { + Injector injector = Guice.createInjector(new PrivateModule() { + @Override public void configure() { + expose(String.class); + + install(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("nested"); + } + }); + } + }); + + assertEquals("nested", injector.getInstance(String.class)); + } + + public void testNestedPrivateModulesWithSomeKeysUnexposed() { + Injector injector = Guice.createInjector(new PrivateModule() { + @Override public void configure() { + bind(String.class).annotatedWith(named("bound outer, exposed outer")).toInstance("boeo"); + expose(String.class).annotatedWith(named("bound outer, exposed outer")); + bind(String.class).annotatedWith(named("bound outer, exposed none")).toInstance("boen"); + expose(String.class).annotatedWith(named("bound inner, exposed both")); + + install(new PrivateModule() { + @Override public void configure() { + bind(String.class).annotatedWith(named("bound inner, exposed both")).toInstance("bieb"); + expose(String.class).annotatedWith(named("bound inner, exposed both")); + bind(String.class).annotatedWith(named("bound inner, exposed none")).toInstance("bien"); + } + }); + } + }); + + assertEquals("boeo", + injector.getInstance(Key.get(String.class, named("bound outer, exposed outer")))); + assertEquals("bieb", + injector.getInstance(Key.get(String.class, named("bound inner, exposed both")))); + + try { + injector.getInstance(Key.get(String.class, named("bound outer, exposed none"))); + fail(); + } catch (ConfigurationException expected) { + } + + try { + injector.getInstance(Key.get(String.class, named("bound inner, exposed none"))); + fail(); + } catch (ConfigurationException expected) { + } + } + + public void testDependenciesBetweenPrivateAndPublic() { + Injector injector = Guice.createInjector( + new PrivateModule() { + @Override protected void configure() {} + + @Provides @Exposed @Named("a") String provideA() { + return "A"; + } + + @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) { + return ab + "C"; + } + }, + new AbstractModule() { + @Override protected void configure() {} + + @Provides @Named("ab") String provideAb(@Named("a") String a) { + return a + "B"; + } + + @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) { + return abc + "D"; + } + } + ); + + assertEquals("ABCD", injector.getInstance(Key.get(String.class, named("abcd")))); + } + + public void testDependenciesBetweenPrivateAndPublicWithPublicEagerSingleton() { + Injector injector = Guice.createInjector( + new PrivateModule() { + @Override protected void configure() {} + + @Provides @Exposed @Named("a") String provideA() { + return "A"; + } + + @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) { + return ab + "C"; + } + }, + new AbstractModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(named("abcde")).toProvider(new Provider() { + @Inject @Named("abcd") String abcd; + + public String get() { + return abcd + "E"; + } + }).asEagerSingleton(); + } + + @Provides @Named("ab") String provideAb(@Named("a") String a) { + return a + "B"; + } + + @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) { + return abc + "D"; + } + } + ); + + assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde")))); + } + + public void testDependenciesBetweenPrivateAndPublicWithPrivateEagerSingleton() { + Injector injector = Guice.createInjector( + new AbstractModule() { + @Override protected void configure() {} + + @Provides @Named("ab") String provideAb(@Named("a") String a) { + return a + "B"; + } + + @Provides @Named("abcd") String provideAbcd(@Named("abc") String abc) { + return abc + "D"; + } + }, + new PrivateModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(named("abcde")).toProvider(new Provider() { + @Inject @Named("abcd") String abcd; + + public String get() { + return abcd + "E"; + } + }).asEagerSingleton(); + expose(String.class).annotatedWith(named("abcde")); + } + + @Provides @Exposed @Named("a") String provideA() { + return "A"; + } + + @Provides @Exposed @Named("abc") String provideAbc(@Named("ab") String ab) { + return ab + "C"; + } + } + ); + + assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde")))); + } + + static class AB { + @Inject @Named("a") String a; + @Inject @Named("b") String b; + } + + interface C {} + + public void testSpiAccess() { + Injector injector = Guice.createInjector(new PrivateModule() { + @Override public void configure() { + bind(String.class).annotatedWith(named("a")).toInstance("private"); + bind(String.class).annotatedWith(named("b")).toInstance("exposed"); + expose(String.class).annotatedWith(named("b")); + } + }); + + ExposedBinding binding + = (ExposedBinding) injector.getBinding(Key.get(String.class, Names.named("b"))); + assertEquals(ImmutableSet.>of(Dependency.get(Key.get(Injector.class))), + binding.getDependencies()); + PrivateElements privateElements = binding.getPrivateElements(); + assertEquals(ImmutableSet.>of(Key.get(String.class, named("b"))), + privateElements.getExposedKeys()); + assertContains(privateElements.getExposedSource(Key.get(String.class, named("b"))).toString(), + PrivateModuleTest.class.getName(), getDeclaringSourcePart(getClass())); + Injector privateInjector = privateElements.getInjector(); + assertEquals("private", privateInjector.getInstance(Key.get(String.class, Names.named("a")))); + } + + public void testParentBindsSomethingInPrivate() { + try { + Guice.createInjector(new FailingModule()); + fail(); + } catch(CreationException expected) { + assertEquals(1, expected.getErrorMessages().size()); + assertContains(expected.toString(), + "Unable to create binding for java.util.List.", + "It was already configured on one or more child injectors or private modules", + "bound at " + FailingPrivateModule.class.getName() + ".configure(", + asModuleChain(FailingModule.class, ManyPrivateModules.class, FailingPrivateModule.class), + "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(", + asModuleChain( + FailingModule.class, ManyPrivateModules.class, SecondFailingPrivateModule.class), + "If it was in a PrivateModule, did you forget to expose the binding?", + "at " + FailingModule.class.getName() + ".configure("); + } + } + + public void testParentBindingToPrivateLinkedJitBinding() { + Injector injector = Guice.createInjector(new ManyPrivateModules()); + try { + injector.getBinding(Key.get(Types.providerOf(List.class))); + fail(); + } catch(ConfigurationException expected) { + assertEquals(1, expected.getErrorMessages().size()); + assertContains(expected.toString(), + "Unable to create binding for com.google.inject.Provider.", + "It was already configured on one or more child injectors or private modules", + "bound at " + FailingPrivateModule.class.getName() + ".configure(", + asModuleChain(ManyPrivateModules.class, FailingPrivateModule.class), + "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(", + asModuleChain(ManyPrivateModules.class, SecondFailingPrivateModule.class), + "If it was in a PrivateModule, did you forget to expose the binding?", + "while locating com.google.inject.Provider"); + } + } + + public void testParentBindingToPrivateJitBinding() { + Injector injector = Guice.createInjector(new ManyPrivateModules()); + try { + injector.getBinding(PrivateFoo.class); + fail(); + } catch(ConfigurationException expected) { + assertEquals(1, expected.getErrorMessages().size()); + assertContains(expected.toString(), + "Unable to create binding for " + PrivateFoo.class.getName(), + "It was already configured on one or more child injectors or private modules", + "(bound by a just-in-time binding)", + "If it was in a PrivateModule, did you forget to expose the binding?", + "while locating " + PrivateFoo.class.getName()); + } + } + + private static class FailingModule extends AbstractModule { + @Override protected void configure() { + bind(Collection.class).to(List.class); + install(new ManyPrivateModules()); + } + } + + private static class ManyPrivateModules extends AbstractModule { + @Override protected void configure() { + // make sure duplicate sources are collapsed + install(new FailingPrivateModule()); + install(new FailingPrivateModule()); + // but additional sources are listed + install(new SecondFailingPrivateModule()); + } + } + + private static class FailingPrivateModule extends PrivateModule { + @Override protected void configure() { + bind(List.class).toInstance(new ArrayList()); + + // Add the Provider binding, created just-in-time, + // to make sure our linked JIT bindings have the correct source. + getProvider(Key.get(Types.providerOf(List.class))); + + // Request a JIT binding for PrivateFoo, which can only + // be created in the private module because it depends + // on List. + getProvider(PrivateFoo.class); + } + } + + /** A second class, so we can see another name in the source list. */ + private static class SecondFailingPrivateModule extends PrivateModule { + @Override protected void configure() { + bind(List.class).toInstance(new ArrayList()); + + // Add the Provider binding, created just-in-time, + // to make sure our linked JIT bindings have the correct source. + getProvider(Key.get(Types.providerOf(List.class))); + + // Request a JIT binding for PrivateFoo, which can only + // be created in the private module because it depends + // on List. + getProvider(PrivateFoo.class); + } + } + + private static class PrivateFoo { + @Inject List list; + } +} diff --git a/src/test/java/com/google/inject/ProviderInjectionTest.java b/src/test/java/com/google/inject/ProviderInjectionTest.java new file mode 100644 index 0000000..755ff97 --- /dev/null +++ b/src/test/java/com/google/inject/ProviderInjectionTest.java @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.name.Names.named; + +import com.google.inject.name.Named; + +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.List; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ProviderInjectionTest extends TestCase { + + public void testProviderInjection() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Bar.class); + bind(SampleSingleton.class).in(Scopes.SINGLETON); + } + }); + + Foo foo = injector.getInstance(Foo.class); + + Bar bar = foo.barProvider.get(); + assertNotNull(bar); + assertNotSame(bar, foo.barProvider.get()); + + SampleSingleton singleton = foo.singletonProvider.get(); + assertNotNull(singleton); + assertSame(singleton, foo.singletonProvider.get()); + } + + /** Test for bug 155. */ + public void testProvidersAreInjectedWhenBound() { + Module m = new AbstractModule() { + @Override + protected void configure() { + bind(Bar.class).toProvider(new Provider() { + @SuppressWarnings("unused") + @Inject void cantBeCalled(Baz baz) { + fail("Can't have called this method since Baz is not bound."); + } + public Bar get() { + return new Bar() {}; + } + }); + } + }; + + try { + Guice.createInjector(m); + fail("Should have thrown a CreationException"); + } + catch (CreationException expected) { + } + } + + /** + * When custom providers are used at injector creation time, they should be + * injected before use. In this testcase, we verify that a provider for + * List.class is injected before it is used. + */ + public void testProvidersAreInjectedBeforeTheyAreUsed() { + Injector injector = Guice.createInjector(new AbstractModule() { + public void configure() { + // should bind String to "[true]" + bind(String.class).toProvider(new Provider() { + private String value; + @Inject void initialize(List list) { + value = list.toString(); + } + public String get() { + return value; + } + }); + + // should bind List to [true] + bind(List.class).toProvider(new Provider() { + @Inject Boolean injectedYet = Boolean.FALSE; + public List get() { + return Arrays.asList(injectedYet); + } + }); + + // should bind Boolean to true + bind(Boolean.class).toInstance(Boolean.TRUE); + } + }); + + assertEquals("Providers not injected before use", + "[true]", + injector.getInstance(String.class)); + } + + /** + * This test ensures that regardless of binding order, instances are injected + * before they are used. It injects mutable Count objects and records their + * value at the time that they're injected. + */ + public void testCreationTimeInjectionOrdering() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + // instance injection + bind(Count.class).annotatedWith(named("a")).toInstance(new Count(0) { + @Inject void initialize(@Named("b") Count bCount) { + value = bCount.value + 1; + } + }); + + // provider injection + bind(Count.class).annotatedWith(named("b")).toProvider(new Provider() { + Count count; + @Inject void initialize(@Named("c") Count cCount) { + count = new Count(cCount.value + 2); + } + public Count get() { + return count; + } + }); + + // field and method injection, fields first + bind(Count.class).annotatedWith(named("c")).toInstance(new Count(0) { + @Inject @Named("d") Count dCount; + @Inject void initialize(@Named("e") Count eCount) { + value = dCount.value + eCount.value + 4; + } + }); + + // static injection + requestStaticInjection(StaticallyInjectable.class); + + bind(Count.class).annotatedWith(named("d")).toInstance(new Count(8)); + bind(Count.class).annotatedWith(named("e")).toInstance(new Count(16)); + } + }); + + assertEquals(28, injector.getInstance(Key.get(Count.class, named("c"))).value); + assertEquals(30, injector.getInstance(Key.get(Count.class, named("b"))).value); + assertEquals(31, injector.getInstance(Key.get(Count.class, named("a"))).value); + assertEquals(28, StaticallyInjectable.cCountAtInjectionTime); + } + + static class Count { + int value; + Count(int value) { + this.value = value; + } + } + + static class StaticallyInjectable { + static int cCountAtInjectionTime; + @Inject static void initialize(@Named("c") Count cCount) { + cCountAtInjectionTime = cCount.value; + } + } + + static class Foo { + @Inject Provider barProvider; + @Inject Provider singletonProvider; + } + + static class Bar {} + + static class SampleSingleton {} + + interface Baz { } + +} diff --git a/src/test/java/com/google/inject/ProvisionExceptionTest.java b/src/test/java/com/google/inject/ProvisionExceptionTest.java new file mode 100644 index 0000000..f95eea6 --- /dev/null +++ b/src/test/java/com/google/inject/ProvisionExceptionTest.java @@ -0,0 +1,351 @@ +/** + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +@SuppressWarnings("UnusedDeclaration") +public class ProvisionExceptionTest extends TestCase { + + public void testExceptionsCollapsed() { + try { + Guice.createInjector().getInstance(A.class); + fail(); + } catch (ProvisionException e) { + assertTrue(e.getCause() instanceof UnsupportedOperationException); + assertContains(e.getMessage(), "Error injecting constructor", + "for parameter 0 at com.google.inject.ProvisionExceptionTest$C.setD", + "for field at com.google.inject.ProvisionExceptionTest$B.c", + "for parameter 0 at com.google.inject.ProvisionExceptionTest$A"); + } + } + + /** + * There's a pass-through of user code in the scope. We want exceptions thrown by Guice to be + * limited to a single exception, even if it passes through user code. + */ + public void testExceptionsCollapsedWithScopes() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(B.class).in(Scopes.SINGLETON); + } + }).getInstance(A.class); + fail(); + } catch (ProvisionException e) { + assertTrue(e.getCause() instanceof UnsupportedOperationException); + assertFalse(e.getMessage().contains("custom provider")); + assertContains(e.getMessage(), "Error injecting constructor", + "for parameter 0 at com.google.inject.ProvisionExceptionTest$C.setD", + "for field at com.google.inject.ProvisionExceptionTest$B.c", + "for parameter 0 at com.google.inject.ProvisionExceptionTest$A"); + } + } + + public void testMethodInjectionExceptions() { + try { + Guice.createInjector().getInstance(E.class); + fail(); + } catch (ProvisionException e) { + assertTrue(e.getCause() instanceof UnsupportedOperationException); + assertContains(e.getMessage(), "Error injecting method", + "at " + E.class.getName() + ".setObject("); + } + } + + public void testBindToProviderInstanceExceptions() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(D.class).toProvider(new DProvider()); + } + }).getInstance(D.class); + fail(); + } catch (ProvisionException e) { + assertTrue(e.getCause() instanceof UnsupportedOperationException); + assertContains(e.getMessage(), + "1) Error in custom provider, java.lang.UnsupportedOperationException", + "at " + ProvisionExceptionTest.class.getName(), + getDeclaringSourcePart(getClass())); + } + } + + /** + * This test demonstrates that if the user throws a ProvisionException, we wrap it to add context. + */ + public void testProvisionExceptionsAreWrappedForBindToType() { + try { + Guice.createInjector().getInstance(F.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), "1) User Exception", + "at " + F.class.getName() + ".("); + } + } + + public void testProvisionExceptionsAreWrappedForBindToProviderType() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(F.class).toProvider(FProvider.class); + } + }).getInstance(F.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), "1) User Exception", + "while locating ", FProvider.class.getName(), + "while locating ", F.class.getName()); + } + } + + public void testProvisionExceptionsAreWrappedForBindToProviderInstance() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(F.class).toProvider(new FProvider()); + } + }).getInstance(F.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), "1) User Exception", + "at " + ProvisionExceptionTest.class.getName(), + getDeclaringSourcePart(getClass())); + } + } + + public void testProvisionExceptionIsSerializable() throws IOException { + try { + Guice.createInjector().getInstance(A.class); + fail(); + } catch (ProvisionException expected) { + //ProvisionException reserialized = reserialize(expected); + /*assertContains(reserialized.getMessage(), + "1) Error injecting constructor, java.lang.UnsupportedOperationException", + "at com.google.inject.ProvisionExceptionTest$RealD.()", + "at Key[type=com.google.inject.ProvisionExceptionTest$RealD, annotation=[none]]", + "@com.google.inject.ProvisionExceptionTest$C.setD()[0]", + "at Key[type=com.google.inject.ProvisionExceptionTest$C, annotation=[none]]", + "@com.google.inject.ProvisionExceptionTest$B.c", + "at Key[type=com.google.inject.ProvisionExceptionTest$B, annotation=[none]]", + "@com.google.inject.ProvisionExceptionTest$A.()[0]", + "at Key[type=com.google.inject.ProvisionExceptionTest$A, annotation=[none]]");*/ + } + } + + public void testMultipleCauses() { + try { + Guice.createInjector().getInstance(G.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error injecting method, java.lang.IllegalArgumentException", + "Caused by: java.lang.IllegalArgumentException: java.lang.UnsupportedOperationException", + "Caused by: java.lang.UnsupportedOperationException: Unsupported", + "2) Error injecting method, java.lang.NullPointerException: can't inject second either", + "Caused by: java.lang.NullPointerException: can't inject second either", + "2 errors"); + } + } + + public void testInjectInnerClass() throws Exception { + Injector injector = Guice.createInjector(); + try { + injector.getInstance(InnerClass.class); + fail(); + } catch (Exception expected) { + assertContains(expected.getMessage(), + "Injecting into inner classes is not supported.", + "while locating " + InnerClass.class.getName()); + } + } + + public void testInjectLocalClass() throws Exception { + class LocalClass {} + + Injector injector = Guice.createInjector(); + try { + injector.getInstance(LocalClass.class); + fail(); + } catch (Exception expected) { + assertContains(expected.getMessage(), + "Injecting into inner classes is not supported.", + "while locating " + LocalClass.class.getName()); + } + } + + public void testBindingAnnotationsOnMethodsAndConstructors() { + try { + Injector injector = Guice.createInjector(); + injector.getInstance(MethodWithBindingAnnotation.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), MethodWithBindingAnnotation.class.getName() + + ".injectMe() is annotated with @", Green.class.getName() + "(), ", + "but binding annotations should be applied to its parameters instead.", + "while locating " + MethodWithBindingAnnotation.class.getName()); + } + + try { + Guice.createInjector().getInstance(ConstructorWithBindingAnnotation.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), ConstructorWithBindingAnnotation.class.getName() + + ".() is annotated with @", Green.class.getName() + "(), ", + "but binding annotations should be applied to its parameters instead.", + "at " + ConstructorWithBindingAnnotation.class.getName() + ".class", + "while locating " + ConstructorWithBindingAnnotation.class.getName()); + } + } + + public void testBindingAnnotationWarningForScala() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).annotatedWith(Green.class).toInstance("lime!"); + } + }); + injector.getInstance(LikeScala.class); + } + + public void testLinkedBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(D.class).to(RealD.class); + } + }); + + try { + injector.getInstance(D.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "at " + RealD.class.getName() + ".(", + "while locating " + RealD.class.getName(), + "while locating " + D.class.getName()); + } + } + + public void testProviderKeyBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(D.class).toProvider(DProvider.class); + } + }); + + try { + injector.getInstance(D.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "while locating " + DProvider.class.getName(), + "while locating " + D.class.getName()); + } + } + + private class InnerClass {} + + static class A { + @Inject + A(B b) { } + } + static class B { + @Inject C c; + } + static class C { + @Inject + void setD(RealD d) { } + } + static class E { + @Inject void setObject(Object o) { + throw new UnsupportedOperationException(); + } + } + + static class MethodWithBindingAnnotation { + @Inject @Green void injectMe(String greenString) {} + } + + static class ConstructorWithBindingAnnotation { + // Suppress compiler errors by the error-prone checker InjectedConstructorAnnotations, + // which catches injected constructors with binding annotations. + @SuppressWarnings("InjectedConstructorAnnotations") + @Inject @Green ConstructorWithBindingAnnotation(String greenString) {} + } + + /** + * In Scala, fields automatically get accessor methods with the same name. So we don't do + * misplaced-binding annotation detection if the offending method has a matching field. + */ + static class LikeScala { + @Inject @Green String green; + @Inject @Green String green() { return green; } + } + + @Retention(RUNTIME) + @Target({ FIELD, PARAMETER, CONSTRUCTOR, METHOD }) + @BindingAnnotation + @interface Green {} + + interface D {} + + static class RealD implements D { + @Inject RealD() { + throw new UnsupportedOperationException(); + } + } + + static class DProvider implements Provider { + public D get() { + throw new UnsupportedOperationException(); + } + } + + static class F { + @Inject public F() { + throw new ProvisionException("User Exception", new RuntimeException()); + } + } + + static class FProvider implements Provider { + public F get() { + return new F(); + } + } + + static class G { + @Inject void injectFirst() { + throw new IllegalArgumentException(new UnsupportedOperationException("Unsupported")); + } + @Inject void injectSecond() { + throw new NullPointerException("can't inject second either"); + } + } +} diff --git a/src/test/java/com/google/inject/ProvisionExceptionsTest.java b/src/test/java/com/google/inject/ProvisionExceptionsTest.java new file mode 100644 index 0000000..23c323c --- /dev/null +++ b/src/test/java/com/google/inject/ProvisionExceptionsTest.java @@ -0,0 +1,178 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import com.google.inject.internal.Errors; +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import junit.framework.TestCase; + +import java.io.IOException; + +/** + * Tests that ProvisionExceptions are readable and clearly indicate to the user what went wrong with + * their code. + * + * @author sameb@google.com (Sam Berlin) + */ +public class ProvisionExceptionsTest extends TestCase { + + public void testConstructorRuntimeException() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(Names.named("runtime")).to(true); + bind(Exploder.class).to(Explosion.class); + bind(Tracer.class).to(TracerImpl.class); + } + }); + try { + injector.getInstance(Tracer.class); + fail(); + } catch(ProvisionException pe) { + // Make sure our initial error message gives the user exception. + Asserts.assertContains(pe.getMessage(), + "1) Error injecting constructor", "java.lang.IllegalStateException: boom!"); + assertEquals(1, pe.getErrorMessages().size()); + assertEquals(IllegalStateException.class, pe.getCause().getClass()); + assertEquals(IllegalStateException.class, Errors.getOnlyCause(pe.getErrorMessages()).getClass()); + } + } + + public void testConstructorCheckedException() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(Names.named("runtime")).to(false); + bind(Exploder.class).to(Explosion.class); + bind(Tracer.class).to(TracerImpl.class); + } + }); + try { + injector.getInstance(Tracer.class); + fail(); + } catch(ProvisionException pe) { + // Make sure our initial error message gives the user exception. + Asserts.assertContains(pe.getMessage(), + "1) Error injecting constructor", "java.io.IOException: boom!"); + assertEquals(1, pe.getErrorMessages().size()); + assertEquals(IOException.class, pe.getCause().getClass()); + assertEquals(IOException.class, Errors.getOnlyCause(pe.getErrorMessages()).getClass()); + } + } + + public void testCustomProvidersRuntimeException() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Exploder.class).toProvider(new Provider() { + public Exploder get() { + return Explosion.createRuntime(); + } + }); + bind(Tracer.class).to(TracerImpl.class); + } + }); + try { + injector.getInstance(Tracer.class); + fail(); + } catch(ProvisionException pe) { + // Make sure our initial error message gives the user exception. + Asserts.assertContains(pe.getMessage(), + "1) Error in custom provider", "java.lang.IllegalStateException: boom!"); + assertEquals(1, pe.getErrorMessages().size()); + assertEquals(IllegalStateException.class, pe.getCause().getClass()); + assertEquals(IllegalStateException.class, Errors.getOnlyCause(pe.getErrorMessages()).getClass()); + } + } + + public void testProviderMethodRuntimeException() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Tracer.class).to(TracerImpl.class); + } + @Provides Exploder exploder() { + return Explosion.createRuntime(); + } + }); + try { + injector.getInstance(Tracer.class); + fail(); + } catch(ProvisionException pe) { + // Make sure our initial error message gives the user exception. + Asserts.assertContains(pe.getMessage(), + "1) Error in custom provider", "java.lang.IllegalStateException: boom!"); + assertEquals(1, pe.getErrorMessages().size()); + assertEquals(IllegalStateException.class, pe.getCause().getClass()); + assertEquals(IllegalStateException.class, Errors.getOnlyCause(pe.getErrorMessages()).getClass()); + } + } + + public void testProviderMethodCheckedException() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Tracer.class).to(TracerImpl.class); + } + @Provides Exploder exploder() throws IOException { + return Explosion.createChecked(); + } + }); + try { + injector.getInstance(Tracer.class); + fail(); + } catch(ProvisionException pe) { + pe.printStackTrace(); + // Make sure our initial error message gives the user exception. + Asserts.assertContains(pe.getMessage(), + "1) Error in custom provider", "java.io.IOException: boom!"); + assertEquals(1, pe.getErrorMessages().size()); + assertEquals(IOException.class, pe.getCause().getClass()); + assertEquals(IOException.class, Errors.getOnlyCause(pe.getErrorMessages()).getClass()); + } + } + + private static interface Exploder {} + public static class Explosion implements Exploder { + @Inject public Explosion(@Named("runtime") boolean runtime) throws IOException { + if(runtime) { + throw new IllegalStateException("boom!"); + } else { + throw new IOException("boom!"); + } + } + + public static Explosion createRuntime() { + try { + return new Explosion(true); + } catch(IOException iox) { + throw new RuntimeException(); + } + } + + public static Explosion createChecked() throws IOException { + return new Explosion(false); + } + } + private static interface Tracer {} + private static class TracerImpl implements Tracer { + @Inject TracerImpl(Exploder explosion) { + } + } +} diff --git a/src/test/java/com/google/inject/ProvisionListenerTest.java b/src/test/java/com/google/inject/ProvisionListenerTest.java new file mode 100644 index 0000000..6e1be84 --- /dev/null +++ b/src/test/java/com/google/inject/ProvisionListenerTest.java @@ -0,0 +1,796 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.common.collect.ImmutableList.of; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.name.Names.named; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.matcher.AbstractMatcher; +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import com.google.inject.name.Named; +import com.google.inject.spi.DependencyAndSource; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.spi.ProvisionListener; +import com.google.inject.util.Providers; + +import junit.framework.TestCase; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests for {@link Binder#bindListener(Matcher, ProvisionListener...)} + * + * @author sameb@google.com (Sam Berlin) + */ +// TODO(sameb): Add some tests for private modules & child injectors. +public class ProvisionListenerTest extends TestCase { + + public void testExceptionInListenerBeforeProvisioning() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new FailBeforeProvision()); + } + }); + try { + injector.getInstance(Foo.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error notifying ProvisionListener " + FailBeforeProvision.class.getName() + + " of " + Foo.class.getName(), + "Reason: java.lang.RuntimeException: boo", + "while locating " + Foo.class.getName()); + assertEquals("boo", pe.getCause().getMessage()); + } + } + + public void testExceptionInListenerAfterProvisioning() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new FailAfterProvision()); + } + }); + try { + injector.getInstance(Foo.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error notifying ProvisionListener " + FailAfterProvision.class.getName() + + " of " + Foo.class.getName(), + "Reason: java.lang.RuntimeException: boo", + "while locating " + Foo.class.getName()); + assertEquals("boo", pe.getCause().getMessage()); + } + } + + public void testExceptionInProvisionExplicitlyCalled() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new JustProvision()); + } + }); + try { + injector.getInstance(FooBomb.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail", + " at " + FooBomb.class.getName(), + " while locating " + FooBomb.class.getName()); + assertEquals("Retry, Abort, Fail", pe.getCause().getMessage()); + } + } + + public void testExceptionInProvisionAutomaticallyCalled() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new NoProvision()); + } + }); + try { + injector.getInstance(FooBomb.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail", + " at " + FooBomb.class.getName(), + " while locating " + FooBomb.class.getName()); + assertEquals("Retry, Abort, Fail", pe.getCause().getMessage()); + } + } + + public void testExceptionInFieldProvision() throws Exception { + final CountAndCaptureExceptionListener listener = new CountAndCaptureExceptionListener(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(new AbstractMatcher>() { + @Override public boolean matches(Binding binding) { + return binding.getKey().getRawType().equals(DependsOnFooBombInField.class); + } + }, listener); + } + }); + assertEquals(0, listener.beforeProvision); + String expectedMsg = null; + try { + injector.getInstance(DependsOnFooBombInField.class); + fail(); + } catch (ProvisionException expected) { + assertEquals(1, expected.getErrorMessages().size()); + expectedMsg = expected.getMessage(); + assertContains(listener.capture.get().getMessage(), + "1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail", + " at " + FooBomb.class.getName(), + " while locating " + FooBomb.class.getName(), + " while locating " + DependsOnFooBombInField.class.getName()); + } + assertEquals(1, listener.beforeProvision); + assertEquals(expectedMsg, listener.capture.get().getMessage()); + assertEquals(0, listener.afterProvision); + } + + public void testExceptionInCxtorProvision() throws Exception { + final CountAndCaptureExceptionListener listener = new CountAndCaptureExceptionListener(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(new AbstractMatcher>() { + @Override public boolean matches(Binding binding) { + return binding.getKey().getRawType().equals(DependsOnFooBombInCxtor.class); + } + }, listener); + } + }); + assertEquals(0, listener.beforeProvision); + String expectedMsg = null; + try { + injector.getInstance(DependsOnFooBombInCxtor.class); + fail(); + } catch (ProvisionException expected) { + assertEquals(1, expected.getErrorMessages().size()); + expectedMsg = expected.getMessage(); + assertContains(listener.capture.get().getMessage(), + "1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail", + " at " + FooBomb.class.getName(), + " while locating " + FooBomb.class.getName(), + " while locating " + DependsOnFooBombInCxtor.class.getName()); + } + assertEquals(1, listener.beforeProvision); + assertEquals(expectedMsg, listener.capture.get().getMessage()); + assertEquals(0, listener.afterProvision); + } + + public void testListenerCallsProvisionTwice() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new ProvisionTwice()); + } + }); + try { + injector.getInstance(Foo.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error notifying ProvisionListener " + ProvisionTwice.class.getName() + + " of " + Foo.class.getName(), + "Reason: java.lang.IllegalStateException: Already provisioned in this listener.", + "while locating " + Foo.class.getName()); + assertEquals("Already provisioned in this listener.", pe.getCause().getMessage()); + } + } + + public void testCachedInScopePreventsProvisionNotify() { + final Counter count1 = new Counter(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), count1); + bind(Foo.class).in(Scopes.SINGLETON); + } + }); + Foo foo = injector.getInstance(Foo.class); + assertNotNull(foo); + assertEquals(1, count1.count); + + // not notified the second time because nothing is provisioned + // (it's cached in the scope) + count1.count = 0; + assertSame(foo, injector.getInstance(Foo.class)); + assertEquals(0, count1.count); + } + + public void testCombineAllBindListenerCalls() { + final Counter count1 = new Counter(); + final Counter count2 = new Counter(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), count1); + bindListener(Matchers.any(), count2); + } + }); + assertNotNull(injector.getInstance(Foo.class)); + assertEquals(1, count1.count); + assertEquals(1, count2.count); + } + + public void testNotifyEarlyListenersIfFailBeforeProvision() { + final Counter count1 = new Counter(); + final Counter count2 = new Counter(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), count1, new FailBeforeProvision(), count2); + } + }); + try { + injector.getInstance(Foo.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error notifying ProvisionListener " + FailBeforeProvision.class.getName() + + " of " + Foo.class.getName(), + "Reason: java.lang.RuntimeException: boo", + "while locating " + Foo.class.getName()); + assertEquals("boo", pe.getCause().getMessage()); + + assertEquals(1, count1.count); + assertEquals(0, count2.count); + } + } + + public void testNotifyLaterListenersIfFailAfterProvision() { + final Counter count1 = new Counter(); + final Counter count2 = new Counter(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), count1, new FailAfterProvision(), count2); + } + }); + try { + injector.getInstance(Foo.class); + fail(); + } catch(ProvisionException pe) { + assertEquals(1, pe.getErrorMessages().size()); + assertContains(pe.getMessage(), + "1) Error notifying ProvisionListener " + FailAfterProvision.class.getName() + + " of " + Foo.class.getName(), + "Reason: java.lang.RuntimeException: boo", + "while locating " + Foo.class.getName()); + assertEquals("boo", pe.getCause().getMessage()); + + assertEquals(1, count1.count); + assertEquals(1, count2.count); + } + } + + public void testNotifiedKeysOfAllBindTypes() { + final Capturer capturer = new Capturer(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), capturer); + bind(Foo.class).annotatedWith(named("pk")).toProvider(FooP.class); + try { + bind(Foo.class).annotatedWith(named("cxtr")).toConstructor(Foo.class.getDeclaredConstructor()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + bind(LinkedFoo.class).to(Foo.class); + bind(Interface.class).toInstance(new Implementation()); + bindConstant().annotatedWith(named("constant")).to("MyConstant"); + } + + @Provides @Named("pi") Foo provideFooBar() { + return new Foo(); + } + }); + + // toInstance & constant bindings are notified in random order, at the very beginning. + assertEquals( + ImmutableSet.of(Key.get(Interface.class), Key.get(String.class, named("constant"))), + capturer.getAsSetAndClear()); + + // simple binding + assertNotNull(injector.getInstance(Foo.class)); + assertEquals(of(Key.get(Foo.class)), capturer.getAndClear()); + + // provider key binding -- notifies about provider & the object, always + assertNotNull(injector.getInstance(Key.get(Foo.class, named("pk")))); + assertEquals(of(Key.get(FooP.class), Key.get(Foo.class, named("pk"))), capturer.getAndClear()); + assertNotNull(injector.getInstance(Key.get(Foo.class, named("pk")))); + assertEquals(of(Key.get(FooP.class), Key.get(Foo.class, named("pk"))), capturer.getAndClear()); + + // JIT provider key binding -- notifies about provider & the object, always + assertNotNull(injector.getInstance(JitFoo2.class)); + assertEquals(of(Key.get(JitFoo2P.class), Key.get(JitFoo2.class)), capturer.getAndClear()); + assertNotNull(injector.getInstance(JitFoo2.class)); + assertEquals(of(Key.get(JitFoo2P.class), Key.get(JitFoo2.class)), capturer.getAndClear()); + + // provider instance binding -- just the object (not the provider) + assertNotNull(injector.getInstance(Key.get(Foo.class, named("pi")))); + assertEquals(of(Key.get(Foo.class, named("pi"))), capturer.getAndClear()); + + // toConstructor binding + assertNotNull(injector.getInstance(Key.get(Foo.class, named("cxtr")))); + assertEquals(of(Key.get(Foo.class, named("cxtr"))), capturer.getAndClear()); + + // linked binding -- notifies about the target (that's what's provisioned), not the link + assertNotNull(injector.getInstance(LinkedFoo.class)); + assertEquals(of(Key.get(Foo.class)), capturer.getAndClear()); + + // JIT linked binding -- notifies about the target (that's what's provisioned), not the link + assertNotNull(injector.getInstance(JitFoo.class)); + assertEquals(of(Key.get(Foo.class)), capturer.getAndClear()); + } + + public void testSingletonMatcher() { + final Counter counter = new Counter(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(new AbstractMatcher>() { + @Override + public boolean matches(Binding t) { + return Scopes.isSingleton(t); + } + }, counter); + } + }); + assertEquals(0, counter.count); + // no increment for getting Many. + injector.getInstance(Many.class); + assertEquals(0, counter.count); + // but an increment for getting Sole, since it's a singleton. + injector.getInstance(Sole.class); + assertEquals(1, counter.count); + } + + public void testCallingBindingDotGetProviderDotGet() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new ProvisionListener() { + @Override + public void onProvision(ProvisionInvocation provision) { + provision.getBinding().getProvider().get(); // AGH! + } + }); + } + }); + + try { + injector.getInstance(Sole.class); + fail(); + } catch(ProvisionException expected) { + // We don't really care what kind of error you get, we only care you get an error. + } + + try { + injector.getInstance(Many.class); + fail(); + } catch(ProvisionException expected) { + // We don't really care what kind of error you get, we only care you get an error. + } + } + + interface Interface {} + class Implementation implements Interface {} + + @Singleton static class Sole {} + static class Many {} + + @ImplementedBy(Foo.class) static interface JitFoo {} + @ProvidedBy(JitFoo2P.class) static class JitFoo2 {} + static interface LinkedFoo {} + static class Foo implements JitFoo, LinkedFoo {} + static class FooP implements Provider { + public Foo get() { + return new Foo(); + } + } + static class JitFoo2P implements Provider { + public JitFoo2 get() { + return new JitFoo2(); + } + } + + static class FooBomb { + FooBomb() { + throw new RuntimeException("Retry, Abort, Fail"); + } + } + + static class DependsOnFooBombInField { + @Inject FooBomb fooBomb; + } + + static class DependsOnFooBombInCxtor { + @Inject DependsOnFooBombInCxtor(FooBomb fooBomb) {} + } + + private static class Counter implements ProvisionListener { + int count = 0; + public void onProvision(ProvisionInvocation provision) { + count++; + } + } + + private static class CountAndCaptureExceptionListener implements ProvisionListener { + int beforeProvision = 0; + int afterProvision = 0; + AtomicReference capture = new AtomicReference(); + public void onProvision(ProvisionInvocation provision) { + beforeProvision++; + try { + provision.provision(); + } catch (RuntimeException re) { + capture.set(re); + throw re; + } + afterProvision++; + } + } + + private static class Capturer implements ProvisionListener { + List keys = Lists.newArrayList(); + public void onProvision(ProvisionInvocation provision) { + keys.add(provision.getBinding().getKey()); + T provisioned = provision.provision(); + // InstanceBindings are the only kind of binding where the key can + // be an instanceof the provisioned, because it isn't linked to any + // direct implementation. I guess maybe it'd also be possible + // with a toConstructor binding... but we don't use that in our tests. + if (provision.getBinding() instanceof InstanceBinding) { + Class expected = provision.getBinding().getKey().getRawType(); + assertTrue("expected instanceof: " + expected + ", but was: " + provisioned, + expected.isInstance(provisioned)); + } else { + assertEquals(provision.getBinding().getKey().getRawType(), provisioned.getClass()); + } + } + + Set getAsSetAndClear() { + Set copy = ImmutableSet.copyOf(keys); + keys.clear(); + return copy; + } + + List getAndClear() { + List copy = ImmutableList.copyOf(keys); + keys.clear(); + return copy; + } + } + + private static class FailBeforeProvision implements ProvisionListener { + public void onProvision(ProvisionInvocation provision) { + throw new RuntimeException("boo"); + } + } + private static class FailAfterProvision implements ProvisionListener { + public void onProvision(ProvisionInvocation provision) { + provision.provision(); + throw new RuntimeException("boo"); + } + } + private static class JustProvision implements ProvisionListener { + public void onProvision(ProvisionInvocation provision) { + provision.provision(); + } + } + private static class NoProvision implements ProvisionListener { + public void onProvision(ProvisionInvocation provision) { + } + } + private static class ProvisionTwice implements ProvisionListener { + public void onProvision(ProvisionInvocation provision) { + provision.provision(); + provision.provision(); + } + } + + private static class ChainAsserter implements ProvisionListener { + private final List> provisionList; + private final List> expected; + + public ChainAsserter(List> provisionList, Iterable> expected) { + this.provisionList = provisionList; + this.expected = ImmutableList.copyOf(expected); + } + + public void onProvision(ProvisionInvocation provision) { + List> actual = Lists.newArrayList(); + for (DependencyAndSource dep : provision.getDependencyChain()) { + actual.add(dep.getDependency().getKey().getRawType()); + } + assertEquals(expected, actual); + provisionList.add(provision.getBinding().getKey().getRawType()); + } + } + + private static Matcher> keyMatcher(final Class clazz) { + return new AbstractMatcher>() { + @Override + public boolean matches(Binding t) { + return t.getKey().equals(Key.get(clazz)); + } + }; + } + + @SuppressWarnings("unchecked") + public void testDependencyChain() { + final List> pList = Lists.newArrayList(); + final List> totalList = Lists.newArrayList(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Instance.class).toInstance(new Instance()); + bind(B.class).to(BImpl.class); + bind(D.class).toProvider(DP.class); + + bindListener(Matchers.any(), new ProvisionListener() { + public void onProvision(ProvisionInvocation provision) { + totalList.add(provision.getBinding().getKey().getRawType()); + } + }); + + // Build up a list of asserters for our dependency chains. + ImmutableList.Builder> chain = ImmutableList.builder(); + chain.add(Instance.class); + bindListener(keyMatcher(Instance.class), new ChainAsserter(pList, chain.build())); + + chain.add(A.class); + bindListener(keyMatcher(A.class), new ChainAsserter(pList, chain.build())); + + chain.add(B.class).add(BImpl.class); + bindListener(keyMatcher(BImpl.class), new ChainAsserter(pList, chain.build())); + + chain.add(C.class); + bindListener(keyMatcher(C.class), new ChainAsserter(pList, chain.build())); + + // the chain has D before DP even though DP is provisioned & notified first + // because we do DP because of D, and need DP to provision D. + chain.add(D.class).add(DP.class); + bindListener(keyMatcher(D.class), new ChainAsserter(pList, chain.build())); + bindListener(keyMatcher(DP.class), new ChainAsserter(pList, chain.build())); + + chain.add(E.class); + bindListener(keyMatcher(E.class), new ChainAsserter(pList, chain.build())); + + chain.add(F.class); + bindListener(keyMatcher(F.class), new ChainAsserter(pList, chain.build())); + } + @Provides C c(D d) { + return new C() {}; + } + }); + Instance instance = injector.getInstance(Instance.class); + // make sure we're checking all of the chain asserters.. + assertEquals( + of(Instance.class, A.class, BImpl.class, C.class, DP.class, D.class, E.class, F.class), + pList); + // and make sure that nothing else was notified that we didn't expect. + assertEquals(totalList, pList); + } + + public void testModuleRequestInjection() { + final AtomicBoolean notified = new AtomicBoolean(); + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + requestInjection(new Object() { + @Inject Foo foo; + }); + bindListener(Matchers.any(), + new SpecialChecker(Foo.class, getClass().getName() + ".configure(", notified)); + } + }); + assertTrue(notified.get()); + } + + public void testToProviderInstance() { + final AtomicBoolean notified = new AtomicBoolean(); + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Object.class).toProvider(new Provider() { + @Inject Foo foo; + public Object get() { + return null; + } + }); + bindListener(Matchers.any(), + new SpecialChecker(Foo.class, getClass().getName() + ".configure(", notified)); + } + }); + assertTrue(notified.get()); + } + + public void testInjectorInjectMembers() { + final Object object = new Object() { + @Inject Foo foo; + }; + final AtomicBoolean notified = new AtomicBoolean(); + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), + new SpecialChecker(Foo.class, object.getClass().getName(), notified)); + } + }).injectMembers(object); + assertTrue(notified.get()); + } + + private static class SpecialChecker implements ProvisionListener { + private final Class notifyType; + private final String firstSource; + private final AtomicBoolean notified; + + public SpecialChecker(Class notifyType, String firstSource, AtomicBoolean notified) { + this.notifyType = notifyType; + this.firstSource = firstSource; + this.notified = notified; + } + + public void onProvision(ProvisionInvocation provision) { + notified.set(true); + assertEquals(notifyType, provision.getBinding().getKey().getRawType()); + assertEquals(2, provision.getDependencyChain().size()); + + assertNull(provision.getDependencyChain().get(0).getDependency()); + assertContains(provision.getDependencyChain().get(0).getBindingSource(), firstSource); + + assertEquals(notifyType, + provision.getDependencyChain().get(1).getDependency().getKey().getRawType()); + assertContains(provision.getDependencyChain().get(1).getBindingSource(), + notifyType.getName() + ".class("); + } + } + + private static class Instance { + @Inject A a; + } + private static class A { + @Inject A(B b) {} + } + private interface B {} + private static class BImpl implements B { + @Inject void inject(C c) {} + } + private interface C {} + private interface D {} + private static class DP implements Provider { + @Inject Provider ep; + public D get() { + ep.get(); + return new D() {}; + } + } + private static class E { + @SuppressWarnings("unused") + @Inject F f; + } + private static class F { + } + + public void testBindToInjectorWithListeningGivesSaneException() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new Counter()); + bind(Injector.class).toProvider(Providers.of(null)); + } + }); + fail(); + } catch (CreationException ce) { + assertContains( + ce.getMessage(), "Binding to core guice framework type is not allowed: Injector."); + } + } + + public void testProvisionIsNotifiedAfterContextsClear() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), new ProvisionListener() { + @Override + public void onProvision(ProvisionInvocation provision) { + Object provisioned = provision.provision(); + if (provisioned instanceof X) { + ((X)provisioned).init(); + } else if (provisioned instanceof Y) { + X.createY = false; + ((Y)provisioned).init(); + } + } + }); + } + }); + + X.createY = true; + X x = injector.getInstance(X.class); + assertNotSame(x, x.y.x); + assertFalse("x.ID: " + x.ID + ", x.y.x.iD: " + x.y.x.ID, x.ID == x.y.x.ID); + } + + private static class X { + final static AtomicInteger COUNTER = new AtomicInteger(); + static boolean createY; + + final int ID = COUNTER.getAndIncrement(); + final Provider yProvider; + Y y; + + @Inject X(Provider yProvider) { + this.yProvider = yProvider; + } + + void init() { + if (createY) { + this.y = yProvider.get(); + } + } + } + + private static class Y { + final Provider xProvider; + X x; + + @Inject Y(Provider xProvider) { + this.xProvider = xProvider; + } + + void init() { + this.x = xProvider.get(); + } + } + + public void testDeDuplicateProvisionListeners() { + final Counter counter = new Counter(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(Matchers.any(), counter); + bindListener(Matchers.any(), counter); + } + }); + injector.getInstance(Many.class); + assertEquals("ProvisionListener not de-duplicated", 1, counter.count); + } +} diff --git a/src/test/java/com/google/inject/ReflectionTest.java b/src/test/java/com/google/inject/ReflectionTest.java new file mode 100644 index 0000000..610f099 --- /dev/null +++ b/src/test/java/com/google/inject/ReflectionTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.spi.ElementSource; + +import junit.framework.TestCase; + +import java.lang.annotation.Retention; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ReflectionTest extends TestCase { + + @Retention(RUNTIME) + @BindingAnnotation @interface I {} + + public void testNormalBinding() throws CreationException { + final Foo foo = new Foo(); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class).toInstance(foo); + } + }); + + Binding fooBinding = injector.getBinding(Key.get(Foo.class)); + assertSame(foo, fooBinding.getProvider().get()); + ElementSource source = (ElementSource) fooBinding.getSource(); + assertNotNull(source.getDeclaringSource()); + assertEquals(Key.get(Foo.class), fooBinding.getKey()); + } + + public void testConstantBinding() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(I.class).to(5); + } + }); + + Binding i = injector.getBinding(Key.get(int.class, I.class)); + assertEquals(5, i.getProvider().get()); + ElementSource source = (ElementSource) i.getSource(); + assertNotNull(source.getDeclaringSource()); + assertEquals(Key.get(int.class, I.class), i.getKey()); + } + + public void testLinkedBinding() throws CreationException { + final Bar bar = new Bar(); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Bar.class).toInstance(bar); + bind(Key.get(Foo.class)).to(Key.get(Bar.class)); + } + }); + + Binding fooBinding = injector.getBinding(Key.get(Foo.class)); + assertSame(bar, fooBinding.getProvider().get()); + ElementSource source = (ElementSource) fooBinding.getSource(); + assertNotNull(source.getDeclaringSource()); + assertEquals(Key.get(Foo.class), fooBinding.getKey()); + } + + static class Foo {} + + static class Bar extends Foo {} +} diff --git a/src/test/java/com/google/inject/RequestInjectionTest.java b/src/test/java/com/google/inject/RequestInjectionTest.java new file mode 100644 index 0000000..e7e225d --- /dev/null +++ b/src/test/java/com/google/inject/RequestInjectionTest.java @@ -0,0 +1,235 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; + +import junit.framework.TestCase; + +import java.lang.annotation.Retention; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class RequestInjectionTest extends TestCase { + + @Retention(RUNTIME) + @BindingAnnotation @interface ForField {} + + @Retention(RUNTIME) + @BindingAnnotation @interface ForMethod {} + + @Override + protected void setUp() throws Exception { + super.setUp(); + HasInjections.staticField = 0; + HasInjections.staticMethod = null; + } + + public void testInjectMembers() { + final HasInjections hi = new HasInjections(); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(ForMethod.class).to("test"); + bindConstant().annotatedWith(ForField.class).to(5); + requestInjection(hi); + } + }); + + assertEquals("test", hi.instanceMethod); + assertEquals(5, hi.instanceField); + assertNull(HasInjections.staticMethod); + assertEquals(0, HasInjections.staticField); + } + + public void testInjectStatics() throws CreationException { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(ForMethod.class).to("test"); + bindConstant().annotatedWith(ForField.class).to(5); + requestStaticInjection(HasInjections.class); + } + }); + + assertEquals("test", HasInjections.staticMethod); + assertEquals(5, HasInjections.staticField); + } + + public void testInjectMembersAndStatics() { + final HasInjections hi = new HasInjections(); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(ForMethod.class).to("test"); + bindConstant().annotatedWith(ForField.class).to(5); + requestStaticInjection(HasInjections.class); + requestInjection(hi); + } + }); + + assertEquals("test", hi.instanceMethod); + assertEquals(5, hi.instanceField); + assertEquals("test", HasInjections.staticMethod); + assertEquals(5, HasInjections.staticField); + } + + public void testValidationErrorOnInjectedMembers() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + requestInjection(new NeedsRunnable()); + } + }); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) No implementation for java.lang.Runnable was bound", + "at " + NeedsRunnable.class.getName(), ".runnable("); + } + } + + public void testInjectionErrorOnInjectedMembers() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Runnable.class).toProvider(new Provider() { + public Runnable get() { + throw new UnsupportedOperationException(); + } + }); + requestInjection(new NeedsRunnable()); + } + }); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Error in custom provider, java.lang.UnsupportedOperationException", + "for field at " + NeedsRunnable.class.getName() + ".runnable(", + "at " + getClass().getName(), getDeclaringSourcePart(getClass())); + } + } + + public void testUserExceptionWhileInjectingInstance() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + requestInjection(new BlowsUpOnInject()); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Error injecting method, java.lang.UnsupportedOperationException: Pop", + "at " + BlowsUpOnInject.class.getName() + ".injectInstance("); + } + } + + public void testUserExceptionWhileInjectingStatically() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(BlowsUpOnInject.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Error injecting method, java.lang.UnsupportedOperationException: Snap", + "at " + BlowsUpOnInject.class.getName() + ".injectStatically("); + } + } + + static class NeedsRunnable { + @Inject Runnable runnable; + } + + static class HasInjections { + + @Inject @ForField static int staticField; + @Inject @ForField int instanceField; + + static String staticMethod; + String instanceMethod; + + @Inject static void setStaticMethod(@ForMethod String staticMethod) { + HasInjections.staticMethod = staticMethod; + } + + @Inject void setInstanceS(@ForMethod String instanceS) { + this.instanceMethod = instanceS; + } + } + + static class BlowsUpOnInject { + @Inject void injectInstance() { + throw new UnsupportedOperationException("Pop"); + } + + @Inject static void injectStatically() { + throw new UnsupportedOperationException("Snap"); + } + } + + /* + * Tests that initializables of the same instance don't clobber + * membersInjectors in InitializableReference, so that they ultimately + * can be requested in any order. + */ + public void testEarlyInjectableReferencesWithSameIdentity() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + // Add a listener to trigger all toInstance bindings to get an Initializable. + bindListener(Matchers.any(), new TypeListener() { + @Override + public void hear(TypeLiteral type, TypeEncounter encounter) { + } + }); + + // Bind two different Keys to the IDENTITICAL object + // ORDER MATTERS! We want the String binding to push out the Object one + String fail = new String("better not fail!"); + bind(Object.class).toInstance(fail); + bind(String.class).toInstance(fail); + + // Then try to inject those objects in a requestInjection, + // letting us get into InjectableReference.get before it has + // finished running through all its injections. + // Each of these technically has its own InjectableReference internally. + // ORDER MATTERS!.. because Object is injected first, that InjectableReference + // attempts to process its members injector, but it wasn't initialized, + // because String pushed it out of the queue! + requestInjection(new Object() { + @SuppressWarnings("unused") @Inject Object obj; + @SuppressWarnings("unused") @Inject String str; + }); + } + }); + } +} diff --git a/src/test/java/com/google/inject/RequireAtInjectOnConstructorsTest.java b/src/test/java/com/google/inject/RequireAtInjectOnConstructorsTest.java new file mode 100644 index 0000000..e5f90c2 --- /dev/null +++ b/src/test/java/com/google/inject/RequireAtInjectOnConstructorsTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.inject; + +import junit.framework.TestCase; + +/** + * Tests for {@link Binder#requireAtInjectOnConstructors()} + * + * @author sameb@google.com (Sam Berlin) + */ +public class RequireAtInjectOnConstructorsTest extends TestCase { + + public void testNoCxtors_explicitBinding() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(NoCxtors.class); + binder().requireAtInjectOnConstructors(); + } + }); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + NoCxtors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure"); + } + } + + public void testNoCxtors_jitBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireAtInjectOnConstructors(); + } + }); + try { + injector.getInstance(NoCxtors.class); + fail(); + } catch (ConfigurationException ce) { + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + NoCxtors.class.getName() + " has no constructors annotated with @Inject", + "while locating " + NoCxtors.class.getName()); + } + } + + public void testNoCxtors_implicitBinding() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Interface.class).to(NoCxtors.class); + binder().requireAtInjectOnConstructors(); + } + }); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + NoCxtors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure"); + } + } + + public void testNoCxtors_inheritedByPrivateModules() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + binder().requireAtInjectOnConstructors(); + install(new PrivateModule() { + @Override + protected void configure() { + bind(NoCxtors.class); + } + }); + } + }); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + NoCxtors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure"); + } + } + + public void testNoCxtors_accumulatesAllErrors() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(NoCxtors.class); + bind(AnotherNoCxtors.class); + binder().requireAtInjectOnConstructors(); + } + }); + fail(); + } catch (CreationException ce) { + assertEquals(2, ce.getErrorMessages().size()); + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + NoCxtors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure", + "2) Explicit @Inject annotations are required on constructors, but " + + AnotherNoCxtors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure"); + } + } + + public void testNoCxtors_separateOptionsForPrivateModules() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(AnotherNoCxtors.class); + install(new PrivateModule() { + @Override + protected void configure() { + binder().requireAtInjectOnConstructors(); + bind(NoCxtors.class); + } + }); + } + }); + fail(); + } catch (CreationException ce) { + // This is testing that the parent module doesn't fail because it isn't included + // in the error message. + assertEquals(1, ce.getErrorMessages().size()); + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + NoCxtors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure"); + } + } + + public void testManyConstructorsButNoneWithAtInject() { + try { + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(ManyConstructors.class); + binder().requireAtInjectOnConstructors(); + } + }); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + Asserts.assertContains(ce.getMessage(), + "1) Explicit @Inject annotations are required on constructors, but " + + ManyConstructors.class.getName() + " has no constructors annotated with @Inject", + "at " + RequireAtInjectOnConstructorsTest.class.getName() + "$", "configure"); + } + } + + public void testRequireAtInjectStillAllowsToConstructorBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + try { + bind(ManyConstructors.class) + .toConstructor(ManyConstructors.class.getDeclaredConstructor()); + } catch (Exception e) { + throw new RuntimeException(e); + } + binder().requireAtInjectOnConstructors(); + } + }); + injector.getInstance(ManyConstructors.class); + } + + private static interface Interface {} + private static class NoCxtors implements Interface {} + private static class AnotherNoCxtors {} + private static class ManyConstructors { + @SuppressWarnings("unused") ManyConstructors() {} + @SuppressWarnings("unused") ManyConstructors(String a) {} + @SuppressWarnings("unused") ManyConstructors(int a) {} + } +} diff --git a/src/test/java/com/google/inject/ScopesTest.java b/src/test/java/com/google/inject/ScopesTest.java new file mode 100644 index 0000000..15c2d2e --- /dev/null +++ b/src/test/java/com/google/inject/ScopesTest.java @@ -0,0 +1,1192 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.name.Named; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.google.inject.spi.PrivateElements; +import com.google.inject.util.Providers; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ScopesTest extends TestCase { + + static final long DEADLOCK_TIMEOUT_SECONDS = 1; + + private final AbstractModule singletonsModule = new AbstractModule() { + @Override protected void configure() { + bind(BoundAsSingleton.class).in(Scopes.SINGLETON); + bind(AnnotatedSingleton.class); + bind(EagerSingleton.class).asEagerSingleton(); + bind(LinkedSingleton.class).to(RealLinkedSingleton.class); + bind(DependsOnJustInTimeSingleton.class); + bind(NotASingleton.class); + bind(ImplementedBySingleton.class).in(Scopes.SINGLETON); + bind(ProvidedBySingleton.class).in(Scopes.SINGLETON); + } + }; + + @Override protected void setUp() throws Exception { + AnnotatedSingleton.nextInstanceId = 0; + BoundAsSingleton.nextInstanceId = 0; + EagerSingleton.nextInstanceId = 0; + RealLinkedSingleton.nextInstanceId = 0; + JustInTimeSingleton.nextInstanceId = 0; + NotASingleton.nextInstanceId = 0; + Implementation.nextInstanceId = 0; + ProvidedBySingleton.nextInstanceId = 0; + ThrowingSingleton.nextInstanceId = 0; + } + + public void testSingletons() { + Injector injector = Guice.createInjector(singletonsModule); + + assertSame( + injector.getInstance(BoundAsSingleton.class), + injector.getInstance(BoundAsSingleton.class)); + + assertSame( + injector.getInstance(AnnotatedSingleton.class), + injector.getInstance(AnnotatedSingleton.class)); + + assertSame( + injector.getInstance(EagerSingleton.class), + injector.getInstance(EagerSingleton.class)); + + assertSame( + injector.getInstance(LinkedSingleton.class), + injector.getInstance(LinkedSingleton.class)); + + assertSame( + injector.getInstance(JustInTimeSingleton.class), + injector.getInstance(JustInTimeSingleton.class)); + + assertNotSame( + injector.getInstance(NotASingleton.class), + injector.getInstance(NotASingleton.class)); + + assertSame( + injector.getInstance(ImplementedBySingleton.class), + injector.getInstance(ImplementedBySingleton.class)); + + assertSame( + injector.getInstance(ProvidedBySingleton.class), + injector.getInstance(ProvidedBySingleton.class)); + } + + public void testJustInTimeAnnotatedSingleton() { + Injector injector = Guice.createInjector(); + + assertSame( + injector.getInstance(AnnotatedSingleton.class), + injector.getInstance(AnnotatedSingleton.class)); + } + + public void testSingletonIsPerInjector() { + assertNotSame( + Guice.createInjector().getInstance(AnnotatedSingleton.class), + Guice.createInjector().getInstance(AnnotatedSingleton.class)); + } + + public void testOverriddingAnnotation() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(AnnotatedSingleton.class).in(Scopes.NO_SCOPE); + } + }); + + assertNotSame( + injector.getInstance(AnnotatedSingleton.class), + injector.getInstance(AnnotatedSingleton.class)); + } + + public void testScopingAnnotationsOnAbstractTypeViaBind() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(A.class).to(AImpl.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + A.class.getName() + " is annotated with " + Singleton.class.getName(), + "but scope annotations are not supported for abstract types.", + "at " + A.class.getName() + ".class(ScopesTest.java:"); + } + } + + @Singleton + interface A {} + static class AImpl implements A {} + + @Retention(RUNTIME) + @interface Component {} + + @Component + @Singleton + interface ComponentAnnotationTest {} + static class ComponentAnnotationTestImpl implements ComponentAnnotationTest {} + + public void testScopingAnnotationsOnAbstractTypeIsValidForComponent() { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(ComponentAnnotationTest.class).to(ComponentAnnotationTestImpl.class); + } + }); + } + + public void testScopingAnnotationsOnAbstractTypeViaImplementedBy() { + try { + Guice.createInjector().getInstance(D.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + D.class.getName() + " is annotated with " + Singleton.class.getName(), + "but scope annotations are not supported for abstract types.", + "at " + D.class.getName() + ".class(ScopesTest.java:"); + } + } + + @Singleton @ImplementedBy(DImpl.class) + interface D {} + static class DImpl implements D {} + + public void testScopingAnnotationsOnAbstractTypeViaProvidedBy() { + try { + Guice.createInjector().getInstance(E.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + E.class.getName() + " is annotated with " + Singleton.class.getName(), + "but scope annotations are not supported for abstract types.", + "at " + E.class.getName() + ".class(ScopesTest.java:"); + } + } + + @Singleton @ProvidedBy(EProvider.class) + interface E {} + static class EProvider implements Provider { + public E get() { + return null; + } + } + + public void testScopeUsedButNotBound() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(B.class).in(CustomScoped.class); + bind(C.class); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) No scope is bound to " + CustomScoped.class.getName(), + "at " + getClass().getName(), getDeclaringSourcePart(getClass()), + "2) No scope is bound to " + CustomScoped.class.getName(), + "at " + C.class.getName() + ".class"); + } + } + + static class B {} + + @CustomScoped + static class C {} + + public void testSingletonsInProductionStage() { + Guice.createInjector(Stage.PRODUCTION, singletonsModule); + + assertEquals(1, AnnotatedSingleton.nextInstanceId); + assertEquals(1, BoundAsSingleton.nextInstanceId); + assertEquals(1, EagerSingleton.nextInstanceId); + assertEquals(1, RealLinkedSingleton.nextInstanceId); + assertEquals(1, JustInTimeSingleton.nextInstanceId); + assertEquals(0, NotASingleton.nextInstanceId); + } + + public void testSingletonsInDevelopmentStage() { + Guice.createInjector(Stage.DEVELOPMENT, singletonsModule); + + assertEquals(0, AnnotatedSingleton.nextInstanceId); + assertEquals(0, BoundAsSingleton.nextInstanceId); + assertEquals(1, EagerSingleton.nextInstanceId); + assertEquals(0, RealLinkedSingleton.nextInstanceId); + assertEquals(0, JustInTimeSingleton.nextInstanceId); + assertEquals(0, NotASingleton.nextInstanceId); + } + + public void testSingletonScopeIsNotSerializable() throws IOException { + Asserts.assertNotSerializable(Scopes.SINGLETON); + } + + public void testNoScopeIsNotSerializable() throws IOException { + Asserts.assertNotSerializable(Scopes.NO_SCOPE); + } + + public void testUnscopedProviderWorksOutsideOfRequestedScope() { + final RememberProviderScope scope = new RememberProviderScope(); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindScope(CustomScoped.class, scope); + bind(List.class).to(ArrayList.class).in(CustomScoped.class); + } + }); + + injector.getInstance(List.class); + Provider listProvider = scope.providers.get(Key.get(List.class)); + + // this line fails with a NullPointerException because the Providers + // passed to Scope.scope() don't work outside of the scope() method. + assertTrue(listProvider.get() instanceof ArrayList); + } + + static class OuterRuntimeModule extends AbstractModule { + @Override protected void configure() { + install(new InnerRuntimeModule()); + } + } + static class InnerRuntimeModule extends AbstractModule { + @Override protected void configure() { + bindScope(NotRuntimeRetainedScoped.class, Scopes.NO_SCOPE); + } + } + public void testScopeAnnotationWithoutRuntimeRetention() { + try { + Guice.createInjector(new OuterRuntimeModule()); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Please annotate " + NotRuntimeRetainedScoped.class.getName() + + " with @Retention(RUNTIME).", + "at " + InnerRuntimeModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterRuntimeModule.class, InnerRuntimeModule.class)); + } + } + + static class OuterDeprecatedModule extends AbstractModule { + @Override protected void configure() { + install(new InnerDeprecatedModule()); + } + } + static class InnerDeprecatedModule extends AbstractModule { + @Override protected void configure() { + bindScope(Deprecated.class, Scopes.NO_SCOPE); + } + } + public void testBindScopeToAnnotationWithoutScopeAnnotation() { + try { + Guice.createInjector(new OuterDeprecatedModule()); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Please annotate " + Deprecated.class.getName() + " with @ScopeAnnotation.", + "at " + InnerDeprecatedModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterDeprecatedModule.class, InnerDeprecatedModule.class)); + } + } + + static class OuterScopeModule extends AbstractModule { + @Override protected void configure() { + install(new CustomNoScopeModule()); + install(new CustomSingletonModule()); + } + } + static class CustomNoScopeModule extends AbstractModule { + @Override protected void configure() { + bindScope(CustomScoped.class, Scopes.NO_SCOPE); + } + } + static class CustomSingletonModule extends AbstractModule { + @Override protected void configure() { + bindScope(CustomScoped.class, Scopes.SINGLETON); + } + } + + public void testBindScopeTooManyTimes() { + try { + Guice.createInjector(new OuterScopeModule()); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Scope Scopes.NO_SCOPE is already bound to " + CustomScoped.class.getName() + + " at " + CustomNoScopeModule.class.getName() + getDeclaringSourcePart(getClass()), + asModuleChain(OuterScopeModule.class, CustomNoScopeModule.class), + "Cannot bind Scopes.SINGLETON.", + "at " + ScopesTest.class.getName(), getDeclaringSourcePart(getClass()), + asModuleChain(OuterScopeModule.class, CustomSingletonModule.class)); + } + } + + public void testBindDuplicateScope() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindScope(CustomScoped.class, Scopes.SINGLETON); + bindScope(CustomScoped.class, Scopes.SINGLETON); + } + }); + + assertSame( + injector.getInstance(AnnotatedCustomScoped.class), + injector.getInstance(AnnotatedCustomScoped.class)); + } + + public void testDuplicateScopeAnnotations() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindScope(CustomScoped.class, Scopes.NO_SCOPE); + } + }); + + try { + injector.getInstance(SingletonAndCustomScoped.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) More than one scope annotation was found: ", + "while locating " + SingletonAndCustomScoped.class.getName()); + } + } + + public void testNullScopedAsASingleton() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() {} + + final Iterator values = Arrays.asList(null, "A").iterator(); + + @Provides @Singleton String provideString() { + return values.next(); + } + }); + + assertNull(injector.getInstance(String.class)); + assertNull(injector.getInstance(String.class)); + assertNull(injector.getInstance(String.class)); + } + + class RememberProviderScope implements Scope { + final Map, Provider> providers = Maps.newHashMap(); + public Provider scope(Key key, Provider unscoped) { + providers.put(key, unscoped); + return unscoped; + } + } + + public void testSingletonAnnotationOnParameterizedType() { + Injector injector = Guice.createInjector(); + assertSame(injector.getInstance(new Key>() {}), + injector.getInstance(new Key>() {})); + assertSame(injector.getInstance(new Key>() {}), + injector.getInstance(new Key>() {})); + } + + @ImplementedBy(Injected.class) public interface In {} + @Singleton public static class Injected implements In {} + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RUNTIME) + @ScopeAnnotation + public @interface CustomScoped {} + + static final Scope CUSTOM_SCOPE = new Scope() { + public Provider scope(Key key, Provider unscoped) { + return Scopes.SINGLETON.scope(key, unscoped); + } + }; + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @ScopeAnnotation + public @interface NotRuntimeRetainedScoped {} + + @CustomScoped + static class AnnotatedCustomScoped {} + + @Singleton + static class AnnotatedSingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + static class BoundAsSingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + static class EagerSingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + interface LinkedSingleton {} + + @Singleton + static class RealLinkedSingleton implements LinkedSingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + static class DependsOnJustInTimeSingleton { + @Inject JustInTimeSingleton justInTimeSingleton; + } + + @Singleton + static class JustInTimeSingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + static class NotASingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + @SuppressWarnings("MoreThanOneScopeAnnotationOnClass") // suppress compiler error for testing + @Singleton + @CustomScoped + static class SingletonAndCustomScoped {} + + @ImplementedBy(Implementation.class) + static interface ImplementedBySingleton {} + + @ProvidedBy(ImplementationProvider.class) + static class ProvidedBySingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + static class Implementation implements ImplementedBySingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + } + + static class ImplementationProvider implements Provider { + public ProvidedBySingleton get() { + return new ProvidedBySingleton(); + } + } + + public void testScopeThatGetsAnUnrelatedObject() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(B.class); + bind(C.class); + ProviderGetScope providerGetScope = new ProviderGetScope(); + requestInjection(providerGetScope); + bindScope(CustomScoped.class, providerGetScope); + } + }); + + injector.getInstance(C.class); + } + + class ProviderGetScope implements Scope { + @Inject Provider bProvider; + + public Provider scope(Key key, final Provider unscoped) { + return new Provider() { + public T get() { + bProvider.get(); + return unscoped.get(); + } + }; + } + } + + public void testIsSingletonPositive() { + final Key a = Key.get(String.class, named("A")); + final Key b = Key.get(String.class, named("B")); + final Key c = Key.get(String.class, named("C")); + final Key d = Key.get(String.class, named("D")); + final Key e = Key.get(String.class, named("E")); + final Key f = Key.get(String.class, named("F")); + final Key g = Key.get(String.class, named("G")); + final Key h = Key.get(Object.class, named("H")); + final Key i = Key.get(String.class, named("I")); + + Module singletonBindings = new AbstractModule() { + @Override protected void configure() { + bind(a).to(b); + bind(b).to(c); + bind(c).toProvider(Providers.of("c")).in(Scopes.SINGLETON); + bind(d).toInstance("d"); + bind(e).toProvider(Providers.of("e")).asEagerSingleton(); + bind(f).toProvider(Providers.of("f")).in(Singleton.class); + bind(h).to(AnnotatedSingleton.class); + install(new PrivateModule() { + @Override protected void configure() { + bind(i).toProvider(Providers.of("i")).in(Singleton.class); + expose(i); + } + }); + } + + @Provides @Named("G") @Singleton String provideG() { + return "g"; + } + }; + + @SuppressWarnings("unchecked") // we know the module contains only bindings + List moduleBindings = Elements.getElements(singletonBindings); + ImmutableMap, Binding> map = indexBindings(moduleBindings); + assertFalse(Scopes.isSingleton(map.get(a))); // linked bindings are not followed by modules + assertFalse(Scopes.isSingleton(map.get(b))); + assertTrue(Scopes.isSingleton(map.get(c))); + assertTrue(Scopes.isSingleton(map.get(d))); + assertTrue(Scopes.isSingleton(map.get(e))); + assertTrue(Scopes.isSingleton(map.get(f))); + assertTrue(Scopes.isSingleton(map.get(g))); + assertFalse(Scopes.isSingleton(map.get(h))); // annotated classes are not followed by modules + assertTrue(Scopes.isSingleton(map.get(i))); + + Injector injector = Guice.createInjector(singletonBindings); + assertTrue(Scopes.isSingleton(injector.getBinding(a))); + assertTrue(Scopes.isSingleton(injector.getBinding(b))); + assertTrue(Scopes.isSingleton(injector.getBinding(c))); + assertTrue(Scopes.isSingleton(injector.getBinding(d))); + assertTrue(Scopes.isSingleton(injector.getBinding(e))); + assertTrue(Scopes.isSingleton(injector.getBinding(f))); + assertTrue(Scopes.isSingleton(injector.getBinding(g))); + assertTrue(Scopes.isSingleton(injector.getBinding(h))); + assertTrue(Scopes.isSingleton(injector.getBinding(i))); + } + + public void testIsSingletonNegative() { + final Key a = Key.get(String.class, named("A")); + final Key b = Key.get(String.class, named("B")); + final Key c = Key.get(String.class, named("C")); + final Key d = Key.get(String.class, named("D")); + final Key e = Key.get(String.class, named("E")); + final Key f = Key.get(String.class, named("F")); + + Module singletonBindings = new AbstractModule() { + @Override protected void configure() { + bind(a).to(b); + bind(b).to(c); + bind(c).toProvider(Providers.of("c")).in(Scopes.NO_SCOPE); + bind(d).toProvider(Providers.of("d")).in(CustomScoped.class); + bindScope(CustomScoped.class, Scopes.NO_SCOPE); + install(new PrivateModule() { + @Override protected void configure() { + bind(f).toProvider(Providers.of("f")).in(CustomScoped.class); + expose(f); + } + }); + } + + @Provides @Named("E") @CustomScoped String provideE() { + return "e"; + } + }; + + @SuppressWarnings("unchecked") // we know the module contains only bindings + List moduleBindings = Elements.getElements(singletonBindings); + ImmutableMap, Binding> map = indexBindings(moduleBindings); + assertFalse(Scopes.isSingleton(map.get(a))); + assertFalse(Scopes.isSingleton(map.get(b))); + assertFalse(Scopes.isSingleton(map.get(c))); + assertFalse(Scopes.isSingleton(map.get(d))); + assertFalse(Scopes.isSingleton(map.get(e))); + assertFalse(Scopes.isSingleton(map.get(f))); + + Injector injector = Guice.createInjector(singletonBindings); + assertFalse(Scopes.isSingleton(injector.getBinding(a))); + assertFalse(Scopes.isSingleton(injector.getBinding(b))); + assertFalse(Scopes.isSingleton(injector.getBinding(c))); + assertFalse(Scopes.isSingleton(injector.getBinding(d))); + assertFalse(Scopes.isSingleton(injector.getBinding(e))); + assertFalse(Scopes.isSingleton(injector.getBinding(f))); + } + + public void testIsScopedPositive() { + final Key a = Key.get(String.class, named("A")); + final Key b = Key.get(String.class, named("B")); + final Key c = Key.get(String.class, named("C")); + final Key d = Key.get(String.class, named("D")); + final Key e = Key.get(String.class, named("E")); + final Key f = Key.get(Object.class, named("F")); + final Key g = Key.get(String.class, named("G")); + + Module customBindings = new AbstractModule() { + @Override protected void configure() { + bindScope(CustomScoped.class, CUSTOM_SCOPE); + bind(a).to(b); + bind(b).to(c); + bind(c).toProvider(Providers.of("c")).in(CUSTOM_SCOPE); + bind(d).toProvider(Providers.of("d")).in(CustomScoped.class); + bind(f).to(AnnotatedCustomScoped.class); + install(new PrivateModule() { + @Override protected void configure() { + bind(g).toProvider(Providers.of("g")).in(CustomScoped.class); + expose(g); + } + }); + } + + @Provides @Named("E") @CustomScoped String provideE() { + return "e"; + } + }; + + @SuppressWarnings("unchecked") // we know the module contains only bindings + List moduleBindings = Elements.getElements(customBindings); + ImmutableMap, Binding> map = indexBindings(moduleBindings); + assertFalse(isCustomScoped(map.get(a))); // linked bindings are not followed by modules + assertFalse(isCustomScoped(map.get(b))); + assertTrue(isCustomScoped(map.get(c))); + assertTrue(isCustomScoped(map.get(d))); + assertTrue(isCustomScoped(map.get(e))); + assertFalse(isCustomScoped(map.get(f))); // annotated classes are not followed by modules + assertTrue(isCustomScoped(map.get(g))); + + Injector injector = Guice.createInjector(customBindings); + assertTrue(isCustomScoped(injector.getBinding(a))); + assertTrue(isCustomScoped(injector.getBinding(b))); + assertTrue(isCustomScoped(injector.getBinding(c))); + assertTrue(isCustomScoped(injector.getBinding(d))); + assertTrue(isCustomScoped(injector.getBinding(e))); + assertTrue(isCustomScoped(injector.getBinding(f))); + assertTrue(isCustomScoped(injector.getBinding(g))); + } + + public void testIsScopedNegative() { + final Key a = Key.get(String.class, named("A")); + final Key b = Key.get(String.class, named("B")); + final Key c = Key.get(String.class, named("C")); + final Key d = Key.get(String.class, named("D")); + final Key e = Key.get(String.class, named("E")); + final Key f = Key.get(String.class, named("F")); + final Key g = Key.get(String.class, named("G")); + final Key h = Key.get(String.class, named("H")); + + Module customBindings = new AbstractModule() { + @Override protected void configure() { + bind(a).to(b); + bind(b).to(c); + bind(c).toProvider(Providers.of("c")).in(Scopes.NO_SCOPE); + bind(d).toProvider(Providers.of("d")).in(Singleton.class); + install(new PrivateModule() { + @Override + protected void configure() { + bind(f).toProvider(Providers.of("f")).in(Singleton.class); + expose(f); + } + }); + bind(g).toInstance("g"); + bind(h).toProvider(Providers.of("h")).asEagerSingleton(); + } + + @Provides @Named("E") @Singleton String provideE() { + return "e"; + } + }; + + @SuppressWarnings("unchecked") // we know the module contains only bindings + List moduleBindings = Elements.getElements(customBindings); + ImmutableMap, Binding> map = indexBindings(moduleBindings); + assertFalse(isCustomScoped(map.get(a))); + assertFalse(isCustomScoped(map.get(b))); + assertFalse(isCustomScoped(map.get(c))); + assertFalse(isCustomScoped(map.get(d))); + assertFalse(isCustomScoped(map.get(e))); + assertFalse(isCustomScoped(map.get(f))); + assertFalse(isCustomScoped(map.get(g))); + assertFalse(isCustomScoped(map.get(h))); + + Injector injector = Guice.createInjector(customBindings); + assertFalse(isCustomScoped(injector.getBinding(a))); + assertFalse(isCustomScoped(injector.getBinding(b))); + assertFalse(isCustomScoped(injector.getBinding(c))); + assertFalse(isCustomScoped(injector.getBinding(d))); + assertFalse(isCustomScoped(injector.getBinding(e))); + assertFalse(isCustomScoped(injector.getBinding(f))); + assertFalse(isCustomScoped(injector.getBinding(g))); + assertFalse(isCustomScoped(injector.getBinding(h))); + } + + private boolean isCustomScoped(Binding binding) { + return Scopes.isScoped(binding, CUSTOM_SCOPE, CustomScoped.class); + } + + ImmutableMap, Binding> indexBindings(Iterable elements) { + ImmutableMap.Builder, Binding> builder = ImmutableMap.builder(); + for (Element element : elements) { + if (element instanceof Binding) { + Binding binding = (Binding) element; + builder.put(binding.getKey(), binding); + } else if (element instanceof PrivateElements) { + PrivateElements privateElements = (PrivateElements)element; + Map, Binding> privateBindings = indexBindings(privateElements.getElements()); + for(Key exposed : privateElements.getExposedKeys()) { + builder.put(exposed, privateBindings.get(exposed)); + } + } + } + return builder.build(); + } + + @Singleton + static class ThrowingSingleton { + static int nextInstanceId; + final int instanceId = nextInstanceId++; + + ThrowingSingleton() { + if (instanceId == 0) { + throw new RuntimeException(); + } + } + } + + public void testSingletonConstructorThrows() { + Injector injector = Guice.createInjector(); + + try { + injector.getInstance(ThrowingSingleton.class); + fail(); + } catch (ProvisionException expected) { + } + + // this behaviour is unspecified. If we change Guice to re-throw the exception, this test + // should be changed + injector.getInstance(ThrowingSingleton.class); + assertEquals(2, ThrowingSingleton.nextInstanceId); + } + + /** + * Should only be created by {@link SBarrierProvider}. + * + *

{@code S} stands for synchronization. + * + * @see SBarrierProvider + */ + static class S { + + private S(int preventInjectionWithoutProvider) { + } + } + + /** + * Provides all the instances of S simultaneously using {@link CyclicBarrier} with {@code + * nThreads}. Intended to be used for threads synchronization during injection. + */ + static class SBarrierProvider implements Provider { + + final CyclicBarrier barrier; + volatile boolean barrierPassed = false; + + SBarrierProvider(int nThreads) { + barrier = new CyclicBarrier(nThreads, new Runnable() { + public void run() { + // would finish before returning from await() for any thread + barrierPassed = true; + } + }); + } + + public S get() { + try { + if (!barrierPassed) { + // only if we're triggering barrier for the first time + barrier.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return new S(0); + } + } + + /** + * Tests that different injectors should not affect each other. + * + *

This creates a second thread to work in parallel, to create two instances of + * {@link S} as the same time. If the lock if not granular enough (i.e. JVM-wide) + * then they would block each other creating a deadlock and await timeout. + */ + + public void testInjectorsDontDeadlockOnSingletons() throws Exception { + final Provider provider = new SBarrierProvider(2); + final Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Thread.currentThread().setName("S.class[1]"); + bind(S.class).toProvider(provider).in(Scopes.SINGLETON); + } + }); + final Injector secondInjector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + Thread.currentThread().setName("S.class[2]"); + bind(S.class).toProvider(provider).in(Scopes.SINGLETON); + } + }); + + Future secondThreadResult = Executors.newSingleThreadExecutor().submit(new Callable() { + public S call() { + return secondInjector.getInstance(S.class); + } + }); + + S firstS = injector.getInstance(S.class); + S secondS = secondThreadResult.get(); + + assertNotSame(firstS, secondS); + } + + @ImplementedBy(GImpl.class) + interface G { + + } + + @Singleton + static class GImpl implements G { + + final H h; + + /** + * Relies on Guice implementation to inject S first and H later, which provides a barrier . + */ + @Inject + GImpl(S synchronizationBarrier, H h) { + this.h = h; + } + } + + @ImplementedBy(HImpl.class) + interface H { + + } + + @Singleton + static class HImpl implements H { + + final G g; + + /** + * Relies on Guice implementation to inject S first and G later, which provides a barrier . + */ + @Inject + HImpl(S synchronizationBarrier, G g) throws Exception { + this.g = g; + } + } + + /** + * Tests that injector can create two singletons with circular dependency in parallel. + * + *

This creates two threads to work in parallel, to create instances of + * {@link G} and {@link H}. Creation is synchronized by injection of {@link S}, + * first thread would block until second would be inside a singleton creation as well. + * + *

Both instances are created by sibling injectors, that share singleton scope. + * Verifies that exactly one circular proxy object is created. + */ + + public void testSiblingInjectorGettingCircularSingletonsOneCircularProxy() throws Exception { + final Provider provider = new SBarrierProvider(2); + final Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(S.class).toProvider(provider); + } + }); + + Future firstThreadResult = Executors.newSingleThreadExecutor().submit(new Callable() { + public G call() { + Thread.currentThread().setName("G.class"); + return injector.createChildInjector().getInstance(G.class); + } + }); + Future secondThreadResult = Executors.newSingleThreadExecutor().submit(new Callable() { + public H call() { + Thread.currentThread().setName("H.class"); + return injector.createChildInjector().getInstance(H.class); + } + }); + + // using separate threads to avoid potential deadlock on the main thread + // waiting twice as much to be sure that both would time out in their respective barriers + GImpl g = (GImpl) firstThreadResult.get(DEADLOCK_TIMEOUT_SECONDS * 3, TimeUnit.SECONDS); + HImpl h = (HImpl) secondThreadResult.get(DEADLOCK_TIMEOUT_SECONDS * 3, TimeUnit.SECONDS); + + // Check that G and H created are not proxied + assertTrue(!Scopes.isCircularProxy(g) && !Scopes.isCircularProxy(h)); + + // Check that we have no more than one circular proxy created + assertFalse(Scopes.isCircularProxy(g.h) && Scopes.isCircularProxy(h.g)); + + // Check that non-proxy variable points to another singleton + assertTrue(g.h == h || h.g == g); + + // Check correct proxy initialization as default equals implementation would + assertEquals(g.h, h); + assertEquals(h.g, g); + } + + @Singleton + static class I0 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + I0(I1 i) { + } + } + + @Singleton + static class I1 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + I1(S synchronizationBarrier, I2 i) { + } + } + + @Singleton + static class I2 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + I2(J1 j) { + } + } + + @Singleton + static class J0 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + J0(J1 j) { + } + } + + @Singleton + static class J1 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + J1(S synchronizationBarrier, J2 j) { + } + } + + @Singleton + static class J2 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + J2(K1 k) { + } + } + + @Singleton + static class K0 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + K0(K1 k) { + } + } + + @Singleton + static class K1 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + K1(S synchronizationBarrier, K2 k) { + } + } + + @Singleton + static class K2 { + + /** + * Relies on Guice implementation to inject S first, which provides a barrier . + */ + @Inject + K2(I1 i) { + } + } + + /** + * Check that circular dependencies on non-interfaces are correctly resolved in multi-threaded + * case. And that an error message constructed is a good one. + * + *

I0 -> I1 -> I2 -> J1 and J0 -> J1 -> J2 -> K1 and K0 -> K1 -> K2, + * where I1, J1 and K1 are created in parallel. + * + *

Creation is synchronized by injection of {@link S}, first thread would block until second + * would be inside a singleton creation as well. + * + *

Verifies that provision results in an error, that spans two threads and + * has a dependency cycle. + */ + + public void testUnresolvableSingletonCircularDependencyErrorMessage() throws Exception { + final Provider provider = new SBarrierProvider(3); + final Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(S.class).toProvider(provider); + } + }); + + Future firstThreadResult = Executors.newSingleThreadExecutor().submit(new Callable() { + public I0 call() { + Thread.currentThread().setName("I0.class"); + return injector.getInstance(I0.class); + } + }); + Future secondThreadResult = Executors.newSingleThreadExecutor().submit(new Callable() { + public J0 call() { + Thread.currentThread().setName("J0.class"); + return injector.getInstance(J0.class); + } + }); + Future thirdThreadResult = Executors.newSingleThreadExecutor().submit(new Callable() { + public K0 call() { + Thread.currentThread().setName("K0.class"); + return injector.getInstance(K0.class); + } + }); + + // using separate threads to avoid potential deadlock on the main thread + // waiting twice as much to be sure that both would time out in their respective barriers + Throwable firstException = null; + Throwable secondException = null; + Throwable thirdException = null; + try { + firstThreadResult.get(DEADLOCK_TIMEOUT_SECONDS * 3, TimeUnit.SECONDS); + fail(); + } catch (ExecutionException e) { + firstException = e.getCause(); + } + try { + secondThreadResult.get(DEADLOCK_TIMEOUT_SECONDS * 3, TimeUnit.SECONDS); + fail(); + } catch (ExecutionException e) { + secondException = e.getCause(); + } + try { + thirdThreadResult.get(DEADLOCK_TIMEOUT_SECONDS * 3, TimeUnit.SECONDS); + fail(); + } catch (ExecutionException e) { + thirdException = e.getCause(); + } + + // verification of error messages generated + assertEquals(firstException.getClass(), ProvisionException.class); + assertEquals(secondException.getClass(), ProvisionException.class); + assertEquals(thirdException.getClass(), ProvisionException.class); + List errorMessages = Lists.newArrayList( + String.format("%s\n%s\n%s", + firstException.getMessage(), secondException.getMessage(), thirdException.getMessage()) + .split("\\n\\n")); + Collections.sort(errorMessages, new Comparator() { + @Override + public int compare(String s1, String s2) { + return s2.length() - s1.length(); + } + }); + // this is brittle, but turns out that second to longest message spans all threads + String errorMessage = errorMessages.get(1); + assertContains(errorMessage, + "Encountered circular dependency spanning several threads. Tried proxying " + + this.getClass().getName()); + assertFalse("Both I0 and J0 can not be a part of a dependency cycle", + errorMessage.contains(I0.class.getName()) && errorMessage.contains(J0.class.getName())); + assertFalse("Both J0 and K0 can not be a part of a dependency cycle", + errorMessage.contains(J0.class.getName()) && errorMessage.contains(K0.class.getName())); + assertFalse("Both K0 and I0 can not be a part of a dependency cycle", + errorMessage.contains(K0.class.getName()) && errorMessage.contains(I0.class.getName())); + + List firstErrorLineForThread = new ArrayList(); + boolean addNextLine = false; + for (String errorLine : errorMessage.split("\\n")) { + if (errorLine.contains("in thread")) { + addNextLine = true; + firstErrorLineForThread.add(errorLine); + } else if (addNextLine) { + addNextLine = false; + firstErrorLineForThread.add(errorLine); + } + } + assertEquals("we expect to see [T1, $A, T2, $B, T3, $C, T1, $A]", + 8, firstErrorLineForThread.size()); + assertEquals("first four elements should be different", + 6, new HashSet(firstErrorLineForThread.subList(0, 6)).size()); + assertEquals(firstErrorLineForThread.get(6), firstErrorLineForThread.get(0)); + assertEquals(firstErrorLineForThread.get(7), firstErrorLineForThread.get(1)); + assertFalse("K0 thread could not be blocked by J0", + firstErrorLineForThread.get(0).contains("J0") + && firstErrorLineForThread.get(2).contains("K0")); + assertFalse("J0 thread could not be blocked by I0", + firstErrorLineForThread.get(0).contains("I0") + && firstErrorLineForThread.get(2).contains("J0")); + assertFalse("I0 thread could not be blocked by K0", + firstErrorLineForThread.get(0).contains("K0") + && firstErrorLineForThread.get(2).contains("I0")); + } +} diff --git a/src/test/java/com/google/inject/SuiteUtils.java b/src/test/java/com/google/inject/SuiteUtils.java new file mode 100644 index 0000000..cd9eb10 --- /dev/null +++ b/src/test/java/com/google/inject/SuiteUtils.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import java.util.Enumeration; +import java.util.Set; + +public class SuiteUtils { + + public static TestSuite removeSuppressedTests(TestSuite suite, Set suppressedTestNames) { + TestSuite result = new TestSuite(suite.getName()); + + for(Enumeration e = suite.tests(); e.hasMoreElements(); ) { + Test test = (Test) e.nextElement(); + + if (suppressedTestNames.contains(test.toString())) { + continue; + } + + if (test instanceof TestSuite) { + result.addTest(removeSuppressedTests((TestSuite) test, suppressedTestNames)); + } else { + result.addTest(test); + } + } + + return result; + } + +} diff --git a/src/test/java/com/google/inject/SuperclassTest.java b/src/test/java/com/google/inject/SuperclassTest.java new file mode 100644 index 0000000..3dda585 --- /dev/null +++ b/src/test/java/com/google/inject/SuperclassTest.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import junit.framework.TestCase; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class SuperclassTest extends TestCase { + + public void testSuperclassInjection() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(Foo.class); + } + }); + + Provider creator = injector.getProvider(Sub.class); + Sub sub = creator.get(); + sub = creator.get(); + sub = creator.get(); + sub = creator.get(); + sub = creator.get(); + assertNotNull(sub.field); + assertNotNull(sub.fromMethod); + } + + static abstract class Super { + @Inject Foo field; + + Foo fromMethod; + @Inject void setC(Foo foo) { + fromMethod = foo; + } + } + + static class Sub extends Super { + } + + static class Foo {} +} diff --git a/src/test/java/com/google/inject/TypeConversionTest.java b/src/test/java/com/google/inject/TypeConversionTest.java new file mode 100644 index 0000000..e318477 --- /dev/null +++ b/src/test/java/com/google/inject/TypeConversionTest.java @@ -0,0 +1,495 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.Iterables; +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.ConvertedConstantBinding; +import com.google.inject.spi.TypeConverter; +import com.google.inject.spi.TypeConverterBinding; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import java.lang.annotation.Retention; +import java.util.Date; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class TypeConversionTest extends TestCase { + + @Retention(RUNTIME) + @BindingAnnotation @interface NumericValue {} + + @Retention(RUNTIME) + @BindingAnnotation @interface BooleanValue {} + + @Retention(RUNTIME) + @BindingAnnotation @interface EnumValue {} + + @Retention(RUNTIME) + @BindingAnnotation @interface ClassName {} + + public static class Foo { + @Inject @BooleanValue Boolean booleanField; + @Inject @BooleanValue boolean primitiveBooleanField; + @Inject @NumericValue Byte byteField; + @Inject @NumericValue byte primitiveByteField; + @Inject @NumericValue Short shortField; + @Inject @NumericValue short primitiveShortField; + @Inject @NumericValue Integer integerField; + @Inject @NumericValue int primitiveIntField; + @Inject @NumericValue Long longField; + @Inject @NumericValue long primitiveLongField; + @Inject @NumericValue Float floatField; + @Inject @NumericValue float primitiveFloatField; + @Inject @NumericValue Double doubleField; + @Inject @NumericValue double primitiveDoubleField; + @Inject @EnumValue Bar enumField; + @Inject @ClassName Class classField; + } + + public enum Bar { + TEE, BAZ, BOB + } + + public void testOneConstantInjection() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(NumericValue.class).to("5"); + bind(Simple.class); + } + }); + + Simple simple = injector.getInstance(Simple.class); + assertEquals(5, simple.i); + } + + static class Simple { + @Inject @NumericValue int i; + } + + public void testConstantInjection() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(NumericValue.class).to("5"); + bindConstant().annotatedWith(BooleanValue.class).to("true"); + bindConstant().annotatedWith(EnumValue.class).to("TEE"); + bindConstant().annotatedWith(ClassName.class).to(Foo.class.getName()); + } + }); + + Foo foo = injector.getInstance(Foo.class); + + checkNumbers( + foo.integerField, + foo.primitiveIntField, + foo.longField, + foo.primitiveLongField, + foo.byteField, + foo.primitiveByteField, + foo.shortField, + foo.primitiveShortField, + foo.floatField, + foo.primitiveFloatField, + foo.doubleField, + foo.primitiveDoubleField + ); + + assertEquals(Bar.TEE, foo.enumField); + assertEquals(Foo.class, foo.classField); + } + + public void testConstantInjectionWithExplicitBindingsRequired() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().requireExplicitBindings(); + bind(Foo.class); + bindConstant().annotatedWith(NumericValue.class).to("5"); + bindConstant().annotatedWith(BooleanValue.class).to("true"); + bindConstant().annotatedWith(EnumValue.class).to("TEE"); + bindConstant().annotatedWith(ClassName.class).to(Foo.class.getName()); + } + }); + + Foo foo = injector.getInstance(Foo.class); + + checkNumbers( + foo.integerField, + foo.primitiveIntField, + foo.longField, + foo.primitiveLongField, + foo.byteField, + foo.primitiveByteField, + foo.shortField, + foo.primitiveShortField, + foo.floatField, + foo.primitiveFloatField, + foo.doubleField, + foo.primitiveDoubleField + ); + + assertEquals(Bar.TEE, foo.enumField); + assertEquals(Foo.class, foo.classField); + } + + void checkNumbers(Number... ns) { + for (Number n : ns) { + assertEquals(5, n.intValue()); + } + } + + static class OuterErrorModule extends AbstractModule { + @Override protected void configure() { + install(new InnerErrorModule()); + } + } + + static class InnerErrorModule extends AbstractModule { + @Override protected void configure() { + bindConstant().annotatedWith(NumericValue.class).to("invalid"); + } + } + + public void testInvalidInteger() throws CreationException { + Injector injector = Guice.createInjector(new OuterErrorModule()); + try { + injector.getInstance(InvalidInteger.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "Error converting 'invalid' (bound at " + InnerErrorModule.class.getName() + + getDeclaringSourcePart(getClass()), + asModuleChain(OuterErrorModule.class, InnerErrorModule.class), + "using TypeConverter which matches identicalTo(class java.lang.Integer)" + + " (bound at [unknown source]).", + "Reason: java.lang.RuntimeException: For input string: \"invalid\""); + } + } + + public static class InvalidInteger { + @Inject @NumericValue Integer integerField; + } + + public void testInvalidCharacter() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(NumericValue.class).to("invalid"); + } + }); + + try { + injector.getInstance(InvalidCharacter.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), "Error converting 'invalid'"); + assertContains(expected.getMessage(), "bound at " + getClass().getName()); + assertContains(expected.getMessage(), "to java.lang.Character"); + } + } + + public static class InvalidCharacter { + @Inject @NumericValue char foo; + } + + public void testInvalidEnum() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(NumericValue.class).to("invalid"); + } + }); + + try { + injector.getInstance(InvalidEnum.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), "Error converting 'invalid'"); + assertContains(expected.getMessage(), "bound at " + getClass().getName()); + assertContains(expected.getMessage(), "to " + Bar.class.getName()); + } + } + + public static class InvalidEnum { + @Inject @NumericValue Bar foo; + } + + public void testToInstanceIsTreatedLikeConstant() throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("5"); + bind(LongHolder.class); + } + }); + + assertEquals(5L, (long) injector.getInstance(LongHolder.class).foo); + } + + static class LongHolder { + @Inject Long foo; + } + + public void testCustomTypeConversion() throws CreationException { + final Date result = new Date(); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)) , mockTypeConverter(result)); + bindConstant().annotatedWith(NumericValue.class).to("Today"); + bind(DateHolder.class); + } + }); + + assertSame(result, injector.getInstance(DateHolder.class).date); + + Binding binding = injector.getBinding(Key.get(Date.class, NumericValue.class)); + assertTrue(binding instanceof ConvertedConstantBinding); + + TypeConverterBinding converterBinding = ((ConvertedConstantBinding)binding).getTypeConverterBinding(); + assertEquals("CustomConverter", converterBinding.getTypeConverter().toString()); + + assertTrue(injector.getTypeConverterBindings().contains(converterBinding)); + } + + static class InvalidCustomValueModule extends AbstractModule { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), failingTypeConverter()); + bindConstant().annotatedWith(NumericValue.class).to("invalid"); + bind(DateHolder.class); + } + } + + public void testInvalidCustomValue() throws CreationException { + Module module = new InvalidCustomValueModule(); + try { + Guice.createInjector(module); + fail(); + } catch (CreationException expected) { + Throwable cause = Iterables.getOnlyElement(expected.getErrorMessages()).getCause(); + assertTrue(cause instanceof UnsupportedOperationException); + assertContains(expected.getMessage(), + "1) Error converting 'invalid' (bound at ", getClass().getName(), + getDeclaringSourcePart(getClass()), "to java.util.Date", + "using BrokenConverter which matches only(java.util.Date) ", + "(bound at " + getClass().getName(), getDeclaringSourcePart(getClass()), + "Reason: java.lang.UnsupportedOperationException: Cannot convert", + "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:"); + } + } + + static class OuterModule extends AbstractModule { + private final Module converterModule; + OuterModule(Module converterModule) { + this.converterModule = converterModule; + } + + @Override protected void configure() { + install(new InnerModule(converterModule)); + } + } + + static class InnerModule extends AbstractModule { + private final Module converterModule; + InnerModule(Module converterModule) { + this.converterModule = converterModule; + } + + @Override protected void configure() { + install(converterModule); + bindConstant().annotatedWith(NumericValue.class).to("foo"); + bind(DateHolder.class); + } + } + + class ConverterNullModule extends AbstractModule { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(null)); + } + } + + public void testNullCustomValue() { + try { + Guice.createInjector(new OuterModule(new ConverterNullModule())); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Received null converting 'foo' (bound at ", + getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterModule.class, InnerModule.class), + "to java.util.Date", + "using CustomConverter which matches only(java.util.Date) ", + "(bound at " + getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterModule.class, InnerModule.class, ConverterNullModule.class), + "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:", + asModuleChain(OuterModule.class, InnerModule.class)); + } + } + + class ConverterCustomModule extends AbstractModule { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(-1)); + } + } + + public void testCustomValueTypeMismatch() { + try { + Guice.createInjector(new OuterModule(new ConverterCustomModule())); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Type mismatch converting 'foo' (bound at ", + getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterModule.class, InnerModule.class), + "to java.util.Date", + "using CustomConverter which matches only(java.util.Date) ", + "(bound at " + getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterModule.class, InnerModule.class, ConverterCustomModule.class), + "Converter returned -1.", + "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:", + asModuleChain(OuterModule.class, InnerModule.class)); + } + } + + public void testStringIsConvertedOnlyOnce() { + final TypeConverter converter = new TypeConverter() { + boolean converted = false; + public Object convert(String value, TypeLiteral toType) { + if (converted) { + throw new AssertionFailedError("converted multiple times!"); + } + converted = true; + return new Date(); + } + }; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), converter); + bindConstant().annotatedWith(NumericValue.class).to("unused"); + } + }); + + Date first = injector.getInstance(Key.get(Date.class, NumericValue.class)); + Date second = injector.getInstance(Key.get(Date.class, NumericValue.class)); + assertSame(first, second); + } + + class OuterAmbiguousModule extends AbstractModule { + @Override protected void configure() { + install(new InnerAmbiguousModule()); + } + } + + class InnerAmbiguousModule extends AbstractModule { + @Override protected void configure() { + install(new Ambiguous1Module()); + install(new Ambiguous2Module()); + bindConstant().annotatedWith(NumericValue.class).to("foo"); + bind(DateHolder.class); + } + } + + class Ambiguous1Module extends AbstractModule { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(new Date())); + } + } + + class Ambiguous2Module extends AbstractModule { + @Override protected void configure() { + convertToTypes(Matchers.only(TypeLiteral.get(Date.class)), mockTypeConverter(new Date())); + } + } + + public void testAmbiguousTypeConversion() { + try { + Guice.createInjector(new OuterAmbiguousModule()); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Multiple converters can convert 'foo' (bound at ", getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterAmbiguousModule.class, InnerAmbiguousModule.class), + "to java.util.Date:", + "CustomConverter which matches only(java.util.Date) (bound at " + + Ambiguous1Module.class.getName() + + getDeclaringSourcePart(getClass()), + asModuleChain( + OuterAmbiguousModule.class, InnerAmbiguousModule.class, Ambiguous1Module.class), + "and", + "CustomConverter which matches only(java.util.Date) (bound at " + + Ambiguous2Module.class.getName() + + getDeclaringSourcePart(getClass()), + asModuleChain( + OuterAmbiguousModule.class, InnerAmbiguousModule.class, Ambiguous2Module.class), + "Please adjust your type converter configuration to avoid overlapping matches.", + "at " + DateHolder.class.getName() + ".date(TypeConversionTest.java:"); + } + } + + TypeConverter mockTypeConverter(final Object result) { + return new TypeConverter() { + public Object convert(String value, TypeLiteral toType) { + return result; + } + + @Override public String toString() { + return "CustomConverter"; + } + }; + } + + private static TypeConverter failingTypeConverter() { + return new TypeConverter() { + public Object convert(String value, TypeLiteral toType) { + throw new UnsupportedOperationException("Cannot convert"); + } + @Override public String toString() { + return "BrokenConverter"; + } + }; + } + + static class DateHolder { + @Inject @NumericValue Date date; + } + + public void testCannotConvertUnannotatedBindings() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("55"); + } + }); + + try { + injector.getInstance(Integer.class); + fail("Converted an unannotated String to an Integer"); + } catch (ConfigurationException expected) { + Asserts.assertContains(expected.getMessage(), + "Could not find a suitable constructor in java.lang.Integer."); + } + } +} diff --git a/src/test/java/com/google/inject/TypeListenerTest.java b/src/test/java/com/google/inject/TypeListenerTest.java new file mode 100644 index 0000000..a19f7c3 --- /dev/null +++ b/src/test/java/com/google/inject/TypeListenerTest.java @@ -0,0 +1,662 @@ +/** + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static com.google.inject.matcher.Matchers.any; +import static com.google.inject.matcher.Matchers.only; +import static com.google.inject.name.Names.named; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.Message; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; + +import junit.framework.TestCase; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class TypeListenerTest extends TestCase { + + private final Matcher onlyAbcd = Matchers.only(new TypeLiteral() {}) + .or(only(new TypeLiteral() {})) + .or(only(new TypeLiteral() {})) + .or(only(new TypeLiteral() {})); + + final TypeListener failingTypeListener = new TypeListener() { + int failures = 0; + + public void hear(TypeLiteral type, TypeEncounter encounter) { + throw new ClassCastException("whoops, failure #" + (++failures)); + } + + @Override public String toString() { + return "clumsy"; + } + }; + + final InjectionListener failingInjectionListener = new InjectionListener() { + int failures = 0; + + public void afterInjection(Object injectee) { + throw new ClassCastException("whoops, failure #" + (++failures)); + } + + @Override public String toString() { + return "goofy"; + } + }; + + final MembersInjector failingMembersInjector = new MembersInjector() { + int failures = 0; + + public void injectMembers(Object instance) { + throw new ClassCastException("whoops, failure #" + (++failures)); + } + + @Override public String toString() { + return "awkward"; + } + }; + + public void testTypeListenersAreFired() { + final AtomicInteger firedCount = new AtomicInteger(); + + final TypeListener typeListener = new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + assertEquals(new TypeLiteral() {}, type); + firedCount.incrementAndGet(); + } + }; + + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, typeListener); + bind(A.class); + } + }); + + assertEquals(1, firedCount.get()); + } + + public void testInstallingInjectionListener() { + final List injectees = Lists.newArrayList(); + final InjectionListener injectionListener = new InjectionListener() { + public void afterInjection(Object injectee) { + injectees.add(injectee); + } + }; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.register(injectionListener); + } + }); + bind(A.class); + } + }); + + assertEquals(ImmutableList.of(), injectees); + + Object a1 = injector.getInstance(A.class); + assertEquals(ImmutableList.of(a1), injectees); + + Object a2 = injector.getInstance(A.class); + assertEquals(ImmutableList.of(a1, a2), injectees); + + Object b1 = injector.getInstance(B.class); + assertEquals(ImmutableList.of(a1, a2, b1), injectees); + + Provider aProvider = injector.getProvider(A.class); + assertEquals(ImmutableList.of(a1, a2, b1), injectees); + A a3 = aProvider.get(); + A a4 = aProvider.get(); + assertEquals(ImmutableList.of(a1, a2, b1, a3, a4), injectees); + } + + class OuterThrowsModule extends AbstractModule { + @Override protected void configure() { + install(new InnerThrowsModule()); + } + } + class InnerThrowsModule extends AbstractModule { + @Override protected void configure() { + bindListener(onlyAbcd, failingTypeListener); + bind(B.class); + bind(C.class); + } + } + public void testTypeListenerThrows() { + try { + Guice.createInjector(new OuterThrowsModule()); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterThrowsModule.class, InnerThrowsModule.class), + "of " + B.class.getName(), + "Reason: java.lang.ClassCastException: whoops, failure #1", + "2) Error notifying TypeListener clumsy (bound at " + getClass().getName(), + getDeclaringSourcePart(getClass()), + asModuleChain(OuterThrowsModule.class, InnerThrowsModule.class), + "of " + C.class.getName(), + "Reason: java.lang.ClassCastException: whoops, failure #2"); + } + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, failingTypeListener); + } + }); + try { + injector.getProvider(B.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(), + getDeclaringSourcePart(getClass()), + "of " + B.class.getName(), + "Reason: java.lang.ClassCastException: whoops, failure #3"); + } + + // getting it again should yield the same exception #3 + try { + injector.getInstance(B.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) Error notifying TypeListener clumsy (bound at " + getClass().getName(), + getDeclaringSourcePart(getClass()), + "of " + B.class.getName(), + "Reason: java.lang.ClassCastException: whoops, failure #3"); + } + + // non-injected types do not participate + assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class)); + } + + public void testInjectionListenerThrows() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.register(failingInjectionListener); + } + }); + bind(B.class); + } + }); + + try { + injector.getInstance(A.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error notifying InjectionListener goofy of " + A.class.getName(), + " Reason: java.lang.ClassCastException: whoops, failure #1"); + } + + // second time through should be a new cause (#2) + try { + injector.getInstance(A.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error notifying InjectionListener goofy of " + A.class.getName(), + " Reason: java.lang.ClassCastException: whoops, failure #2"); + } + + // we should get errors for all types, but only on getInstance() + Provider bProvider = injector.getProvider(B.class); + try { + bProvider.get(); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error notifying InjectionListener goofy of " + B.class.getName(), + " Reason: java.lang.ClassCastException: whoops, failure #3"); + } + + // non-injected types do not participate + assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class)); + } + + public void testInjectMembersTypeListenerFails() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + getMembersInjector(A.class); + bindListener(onlyAbcd, failingTypeListener); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Error notifying TypeListener clumsy (bound at ", + TypeListenerTest.class.getName(), getDeclaringSourcePart(getClass()), + "of " + A.class.getName(), + " Reason: java.lang.ClassCastException: whoops, failure #1"); + } + } + + public void testConstructedTypeListenerIsTheSameAsMembersInjectorListener() { + final AtomicInteger typeEncounters = new AtomicInteger(); + final AtomicInteger injections = new AtomicInteger(); + + final InjectionListener listener = new InjectionListener() { + public void afterInjection(A injectee) { + injections.incrementAndGet(); + assertNotNull(injectee.injector); + } + }; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, new TypeListener() { + @SuppressWarnings("unchecked") + public void hear(TypeLiteral type, TypeEncounter encounter) { + typeEncounters.incrementAndGet(); + encounter.register((InjectionListener) listener); + } + }); + + bind(A.class); + getMembersInjector(A.class); + } + }); + + // creating the injector shouldn't trigger injections + assertEquals(0, injections.getAndSet(0)); + + // constructing an A should trigger an injection + injector.getInstance(A.class); + assertEquals(1, injections.getAndSet(0)); + + // injecting an A should trigger an injection + injector.injectMembers(new A()); + assertEquals(1, injections.getAndSet(0)); + + // getting a provider shouldn't + Provider aProvider = injector.getProvider(A.class); + MembersInjector aMembersInjector = injector.getMembersInjector(A.class); + assertEquals(0, injections.getAndSet(0)); + + // exercise the provider + aProvider.get(); + aProvider.get(); + assertEquals(2, injections.getAndSet(0)); + + // exercise the members injector + aMembersInjector.injectMembers(new A()); + aMembersInjector.injectMembers(new A()); + assertEquals(2, injections.getAndSet(0)); + + // we should only have encountered one type + assertEquals(1, typeEncounters.getAndSet(0)); + } + + public void testLookupsAtInjectorCreateTime() { + final AtomicReference> bProviderReference = new AtomicReference>(); + final AtomicReference> aMembersInjectorReference + = new AtomicReference>(); + + final InjectionListener lookupsTester = new InjectionListener() { + public void afterInjection(Object injectee) { + assertNotNull(bProviderReference.get().get()); + + A a = new A(); + aMembersInjectorReference.get().injectMembers(a); + assertNotNull(a.injector); + } + }; + + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(only(TypeLiteral.get(C.class)), new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + Provider bProvider = encounter.getProvider(B.class); + try { + bProvider.get(); + fail(); + } catch (IllegalStateException expected) { + assertEquals("This Provider cannot be used until the Injector has been created.", + expected.getMessage()); + } + bProviderReference.set(bProvider); + + MembersInjector aMembersInjector = encounter.getMembersInjector(A.class); + try { + aMembersInjector.injectMembers(new A()); + fail(); + } catch (IllegalStateException expected) { + assertEquals( + "This MembersInjector cannot be used until the Injector has been created.", + expected.getMessage()); + } + aMembersInjectorReference.set(aMembersInjector); + + encounter.register(lookupsTester); + } + }); + + // this ensures the type listener fires, and also the afterInjection() listener + bind(C.class).asEagerSingleton(); + } + }); + + lookupsTester.afterInjection(null); + } + + public void testLookupsPostCreate() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(only(TypeLiteral.get(C.class)), new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + assertNotNull(encounter.getProvider(B.class).get()); + + A a = new A(); + encounter.getMembersInjector(A.class).injectMembers(a); + assertNotNull(a.injector); + } + }); + } + }); + + injector.getInstance(C.class); + } + + public void testMembersInjector() { + final MembersInjector membersInjector = new MembersInjector() { + public void injectMembers(D instance) { + instance.userInjected++; + assertEquals(instance.guiceInjected, instance.userInjected); + } + }; + + final InjectionListener injectionListener = new InjectionListener() { + public void afterInjection(D injectee) { + assertTrue(injectee.userInjected > 0); + injectee.listenersNotified++; + assertEquals(injectee.guiceInjected, injectee.listenersNotified); + } + }; + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, new TypeListener() { + @SuppressWarnings("unchecked") + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.register((MembersInjector) membersInjector); + encounter.register((InjectionListener) injectionListener); + } + }); + + D boundThreeTimes = new D(); + bind(D.class).annotatedWith(named("i")).toInstance(boundThreeTimes); + bind(D.class).annotatedWith(named("ii")).toInstance(boundThreeTimes); + bind(D.class).annotatedWith(named("iii")).toInstance(boundThreeTimes); + } + }); + + D boundThreeTimes = injector.getInstance(Key.get(D.class, named("iii"))); + boundThreeTimes.assertAllCounts(1); + + D getInstance = injector.getInstance(D.class); + getInstance.assertAllCounts(1); + + D memberInjection = new D(); + injector.injectMembers(memberInjection); + memberInjection.assertAllCounts(1); + + injector.injectMembers(memberInjection); + injector.injectMembers(memberInjection); + memberInjection.assertAllCounts(3); + + injector.getMembersInjector(D.class).injectMembers(memberInjection); + memberInjection.assertAllCounts(4); + } + + public void testMembersInjectorThrows() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.register(failingMembersInjector); + } + }); + bind(B.class); + } + }); + + try { + injector.getInstance(A.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error injecting " + A.class.getName() + " using awkward.", + "Reason: java.lang.ClassCastException: whoops, failure #1"); + } + + // second time through should be a new cause (#2) + try { + injector.getInstance(A.class); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error injecting " + A.class.getName() + " using awkward.", + "Reason: java.lang.ClassCastException: whoops, failure #2"); + } + + // we should get errors for all types, but only on getInstance() + Provider bProvider = injector.getProvider(B.class); + try { + bProvider.get(); + fail(); + } catch (ProvisionException e) { + assertContains(e.getMessage(), + "1) Error injecting " + B.class.getName() + " using awkward.", + "Reason: java.lang.ClassCastException: whoops, failure #3"); + } + + // non-injected types do not participate + assertSame(Stage.DEVELOPMENT, injector.getInstance(Stage.class)); + } + + /** + * We had a bug where we weren't notifying of types encountered for member injection when those + * types had no members to be injected. Constructed types are always injected because they always + * have at least one injection point: the class constructor. + */ + public void testTypesWithNoInjectableMembersAreNotified() { + final AtomicInteger notificationCount = new AtomicInteger(); + + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(onlyAbcd, new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + notificationCount.incrementAndGet(); + } + }); + + bind(C.class).toInstance(new C()); + } + }); + + assertEquals(1, notificationCount.get()); + } + + public void testEncounterCannotBeUsedAfterHearReturns() { + final AtomicReference> encounterReference = new AtomicReference>(); + + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bindListener(any(), new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounterReference.set(encounter); + } + }); + + bind(C.class); + } + }); + TypeEncounter encounter = encounterReference.get(); + + try { + encounter.register(new InjectionListener() { + public void afterInjection(Object injectee) {} + }); + fail(); + } catch (IllegalStateException expected) { + } + + try { + encounter.addError(new Exception()); + fail(); + } catch (IllegalStateException expected) { + } + + try { + encounter.getMembersInjector(A.class); + fail(); + } catch (IllegalStateException expected) { + } + + try { + encounter.getProvider(B.class); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testAddErrors() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + requestInjection(new Object()); + bindListener(Matchers.any(), new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.addError("There was an error on %s", type); + encounter.addError(new IllegalArgumentException("whoops!")); + encounter.addError(new Message("And another problem")); + encounter.addError(new IllegalStateException()); + } + }); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) There was an error on java.lang.Object", + "2) An exception was caught and reported. Message: whoops!", + "3) And another problem", + "4) An exception was caught and reported. Message: null", + "4 errors"); + } + } + + private static class CountingMembersInjector implements MembersInjector { + public void injectMembers(D instance) { + ++instance.userInjected; + } + } + + private static class CountingInjectionListener implements InjectionListener { + public void afterInjection(D injectee) { + ++injectee.listenersNotified; + } + } + + private static class DuplicatingTypeListener implements TypeListener { + int count = 0; + + @SuppressWarnings({"rawtypes", "unchecked"}) + public void hear(TypeLiteral type, TypeEncounter encounter) { + ++count; + + MembersInjector membersInjector = new CountingMembersInjector(); + encounter.register(membersInjector); + encounter.register(membersInjector); + + InjectionListener injectionListener = new CountingInjectionListener(); + encounter.register(injectionListener); + encounter.register(injectionListener); + } + } + + public void testDeDuplicateTypeListeners() { + final DuplicatingTypeListener typeListener = new DuplicatingTypeListener(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindListener(any(), typeListener); + bindListener(only(new TypeLiteral() {}), typeListener); + } + }); + D d = injector.getInstance(D.class); + d.assertAllCounts(1); + assertEquals(1, typeListener.count); + } + + // TODO: recursively accessing a lookup should fail + + static class A { + @Inject Injector injector; + @Inject Stage stage; + } + + static class B {} + + public static class C { + public String buzz() { + return "buzz"; + } + + public String beep() { + return "beep"; + } + } + + static class D { + int guiceInjected = 0; + int userInjected = 0; + int listenersNotified = 0; + + @Inject void guiceInjected() { + guiceInjected++; + } + + void assertAllCounts(int expected) { + assertEquals(expected, guiceInjected); + assertEquals(expected, userInjected); + assertEquals(expected, listenersNotified); + } + } +} diff --git a/src/test/java/com/google/inject/TypeLiteralInjectionTest.java b/src/test/java/com/google/inject/TypeLiteralInjectionTest.java new file mode 100644 index 0000000..1231a81 --- /dev/null +++ b/src/test/java/com/google/inject/TypeLiteralInjectionTest.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.util.Types.listOf; + +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.util.List; + +/** + * Demonstrates type reification. + * + * @author jessewilson@google.com (Jesse Wilson) + */ +public class TypeLiteralInjectionTest extends TestCase { + + public void testBindingToRawTypeLiteralIsNotAllowed() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(TypeLiteral.class).toInstance(TypeLiteral.get(String.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Binding to core guice framework type is not allowed: TypeLiteral"); + } + } + + public void testBindingToParameterizedTypeLiteralIsNotAllowed() { + try { + Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(new TypeLiteral>() {}) + .toInstance(TypeLiteral.get(String.class)); + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "Binding to core guice framework type is not allowed: TypeLiteral"); + } + } + + public void testInjectTypeLiteralWithRawTypes() { + C c = Guice.createInjector().getInstance(C.class); + assertEquals(TypeLiteral.get(String.class), c.string); + assertEquals(TypeLiteral.get(A.class), c.a); + + try { + Guice.createInjector().getInstance(B.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), TypeLiteral.class.getName() + "> " + + "cannot be used as a key; It is not fully specified."); + } + } + + public void testInjectTypeLiteralWithClassTypes() { + B b = Guice.createInjector().getInstance(new Key>() {}); + assertEquals(TypeLiteral.get(String.class), b.string); + assertEquals(TypeLiteral.get(Integer.class), b.t); + assertEquals(TypeLiteral.get(listOf(Integer.class)), b.listOfT); + assertEquals(TypeLiteral.get(listOf(Types.subtypeOf(Integer.class))), b.listOfWildcardT); + } + + public void testInjectRawTypeLiteral() { + try { + Guice.createInjector().getInstance(TypeLiteral.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "Cannot inject a TypeLiteral that has no type parameter"); + } + } + + static class A { + @Inject TypeLiteral string; + @Inject TypeLiteral> listOfT; + @Inject TypeLiteral> listOfWildcardT; + } + + static class B extends A { + @Inject TypeLiteral t; + } + + static class C { + @Inject TypeLiteral string; + @Inject TypeLiteral a; + T t; + } +} diff --git a/src/test/java/com/google/inject/TypeLiteralTest.java b/src/test/java/com/google/inject/TypeLiteralTest.java new file mode 100644 index 0000000..de8b5da --- /dev/null +++ b/src/test/java/com/google/inject/TypeLiteralTest.java @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertEqualsBothWays; +import static com.google.inject.Asserts.assertNotSerializable; + +import com.google.common.collect.ImmutableList; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.List; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class TypeLiteralTest extends TestCase { + + public void testWithParameterizedType() { + TypeLiteral> a = new TypeLiteral>() {}; + TypeLiteral> b = new TypeLiteral>( + Types.listOf(String.class)) {}; + assertEqualsBothWays(a, b); + } + + public void testEquality() { + TypeLiteral> t1 = new TypeLiteral>() {}; + TypeLiteral> t2 = new TypeLiteral>() {}; + TypeLiteral> t3 = new TypeLiteral>() {}; + TypeLiteral t4 = new TypeLiteral() {}; + + assertEqualsBothWays(t1, t2); + + assertFalse(t2.equals(t3)); + assertFalse(t3.equals(t2)); + + assertFalse(t2.equals(t4)); + assertFalse(t4.equals(t2)); + + TypeLiteral t5 = TypeLiteral.get(String.class); + assertEqualsBothWays(t4, t5); + } + + public List wildcardExtends; + + public void testWithWildcardType() throws NoSuchFieldException, IOException { + TypeLiteral a = TypeLiteral.get(getClass().getField("wildcardExtends").getGenericType()); + TypeLiteral b = TypeLiteral.get(Types.listOf(Types.subtypeOf(CharSequence.class))); + TypeLiteral c = new TypeLiteral>() {}; + assertEqualsBothWays(a, b); + assertEqualsBothWays(b, c); + assertEquals("java.util.List", a.toString()); + assertEquals("java.util.List", b.toString()); + assertEquals("java.util.List", c.toString()); + assertNotSerializable(a); + assertNotSerializable(b); + assertNotSerializable(c); + } + + public void testMissingTypeParameter() { + try { + new TypeLiteral() {}; + fail(); + } catch (RuntimeException e) { /* expected */ } + } + + public void testTypesInvolvingArraysForEquality() { + TypeLiteral stringArray = new TypeLiteral() {}; + assertEquals(stringArray, new TypeLiteral() {}); + + TypeLiteral> listOfStringArray + = new TypeLiteral>() {}; + assertEquals(listOfStringArray, new TypeLiteral>() {}); + } + + public void testEqualityOfGenericArrayAndClassArray() { + TypeLiteral arrayAsClass = TypeLiteral.get(String[].class); + TypeLiteral arrayAsType = new TypeLiteral() {}; + assertEquals(arrayAsClass, arrayAsType); + } + + public void testEqualityOfMultidimensionalGenericArrayAndClassArray() { + TypeLiteral arrayAsClass = TypeLiteral.get(String[][][].class); + TypeLiteral arrayAsType = new TypeLiteral() {}; + assertEquals(arrayAsClass, arrayAsType); + } + + public void testTypeLiteralsMustHaveRawTypes() { + try { + TypeLiteral.get(Types.subtypeOf(Runnable.class)); + fail(); + } catch (IllegalArgumentException expected) { + Asserts.assertContains(expected.getMessage(), "Expected a Class, ParameterizedType, or " + + "GenericArrayType, but is of type " + + "com.google.inject.internal.MoreTypes$WildcardTypeImpl"); + } + } + + /** + * Unlike Key, TypeLiteral retains full type information and differentiates + * between {@code int.class} and {@code Integer.class}. + */ + public void testDifferentiationBetweenWrappersAndPrimitives() { + Class[] primitives = new Class[] { + boolean.class, byte.class, short.class, int.class, long.class, + float.class, double.class, char.class, void.class + }; + Class[] wrappers = new Class[] { + Boolean.class, Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, Character.class, Void.class + }; + + for (int t = 0; t < primitives.length; t++) { + @SuppressWarnings("unchecked") + TypeLiteral primitiveTl = TypeLiteral.get(primitives[t]); + @SuppressWarnings("unchecked") + TypeLiteral wrapperTl = TypeLiteral.get(wrappers[t]); + + assertFalse(primitiveTl.equals(wrapperTl)); + assertEquals(primitives[t], primitiveTl.getType()); + assertEquals(wrappers[t], wrapperTl.getType()); + assertEquals(primitives[t], primitiveTl.getRawType()); + assertEquals(wrappers[t], wrapperTl.getRawType()); + } + } + + public void testSerialization() throws IOException { + assertNotSerializable(new TypeLiteral>() {}); + } + + public void testTypeVariableWithNoBound() { + TypeVariable>[] typeVariables + = HasTypeParameters.class.getTypeParameters(); + + TypeLiteral aTl = TypeLiteral.get(typeVariables[0]); + assertEquals(Object.class, aTl.getRawType()); + assertEquals("A", aTl.toString()); + TypeVariable aTv = (TypeVariable) aTl.getType(); + assertEquals(HasTypeParameters.class, aTv.getGenericDeclaration()); + assertEquals("A", aTv.getName()); + assertEquals(ImmutableList.of(Object.class), ImmutableList.copyOf(aTv.getBounds())); + assertEquals("A", aTv.toString()); + assertEqualsBothWays(aTl, TypeLiteral.get(HasTypeParameters.class.getTypeParameters()[0])); + } + + public void testTypeVariablesWithSingleBound() { + TypeVariable>[] typeVariables + = HasTypeParameters.class.getTypeParameters(); + + TypeLiteral cTl = TypeLiteral.get(typeVariables[2]); + assertEquals(Object.class, cTl.getRawType()); + assertEquals("C", cTl.toString()); + TypeVariable cTv = (TypeVariable) cTl.getType(); + assertEquals(HasTypeParameters.class, cTv.getGenericDeclaration()); + assertEquals("C", cTv.getName()); + assertEquals(ImmutableList.of(Runnable.class), ImmutableList.copyOf(cTv.getBounds())); + assertEquals("C", cTv.toString()); + assertEqualsBothWays(cTl, TypeLiteral.get(HasTypeParameters.class.getTypeParameters()[2])); + } + + public void testTypeVariableWithMultipleBounds() { + TypeVariable>[] typeVariables + = HasTypeParameters.class.getTypeParameters(); + + TypeLiteral bTl = TypeLiteral.get(typeVariables[1]); + assertEquals(Object.class, bTl.getRawType()); + assertEquals("B", bTl.toString()); + TypeVariable bTv = (TypeVariable) bTl.getType(); + assertEquals(HasTypeParameters.class, bTv.getGenericDeclaration()); + assertEquals("B", bTv.getName()); + assertEquals(ImmutableList.of(Types.listOf(typeVariables[0]), Runnable.class), + ImmutableList.copyOf(bTv.getBounds())); + assertEquals("B", bTv.toString()); + assertEqualsBothWays(bTl, TypeLiteral.get(HasTypeParameters.class.getTypeParameters()[1])); + } + + class HasTypeParameters & Runnable, C extends Runnable> { + A a; B b; C c; + } +} diff --git a/src/test/java/com/google/inject/TypeLiteralTypeResolutionTest.java b/src/test/java/com/google/inject/TypeLiteralTypeResolutionTest.java new file mode 100644 index 0000000..64bc4c7 --- /dev/null +++ b/src/test/java/com/google/inject/TypeLiteralTypeResolutionTest.java @@ -0,0 +1,331 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject; + +import static com.google.inject.Asserts.assertEqualsBothWays; +import static com.google.inject.Asserts.assertNotSerializable; +import static com.google.inject.util.Types.arrayOf; +import static com.google.inject.util.Types.listOf; +import static com.google.inject.util.Types.newParameterizedType; +import static com.google.inject.util.Types.newParameterizedTypeWithOwner; +import static com.google.inject.util.Types.setOf; + +import com.google.common.collect.ImmutableList; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This test checks that TypeLiteral can perform type resolution on its members. + * + * @author jessewilson@google.com (Jesse Wilson) + */ +public class TypeLiteralTypeResolutionTest extends TestCase { + Type arrayListOfString = newParameterizedType(ArrayList.class, String.class); + Type hasGenericFieldsOfShort = newParameterizedTypeWithOwner( + getClass(), HasGenericFields.class, Short.class); + Type hasGenericConstructorOfShort = newParameterizedTypeWithOwner( + getClass(), GenericConstructor.class, Short.class); + Type throwerOfNpe = newParameterizedTypeWithOwner( + getClass(), Thrower.class, NullPointerException.class); + Type hasArrayOfShort = newParameterizedTypeWithOwner(getClass(), HasArray.class, Short.class); + Type hasRelatedOfString = newParameterizedTypeWithOwner( + getClass(), HasRelated.class, String.class, String.class); + Type mapK = Map.class.getTypeParameters()[0]; + Type hashMapK = HashMap.class.getTypeParameters()[0]; + Type setEntryKV; + Type entryStringInteger = setOf(newParameterizedTypeWithOwner( + Map.class, Map.Entry.class, String.class, Integer.class)); + Field list; + Field instance; + Constructor newHasGenericConstructor; + Constructor newThrower; + Constructor newString; + Method stringIndexOf; + Method comparableCompareTo; + Method getArray; + Method getSetOfArray; + Method echo; + Method throwS; + + @Override protected void setUp() throws Exception { + super.setUp(); + + list = HasGenericFields.class.getField("list"); + instance = HasGenericFields.class.getField("instance"); + newHasGenericConstructor = GenericConstructor.class.getConstructor(Object.class, Object.class); + newThrower = Thrower.class.getConstructor(); + stringIndexOf = String.class.getMethod("indexOf", String.class); + newString = String.class.getConstructor(String.class); + comparableCompareTo = Comparable.class.getMethod("compareTo", Object.class); + getArray = HasArray.class.getMethod("getArray"); + getSetOfArray = HasArray.class.getMethod("getSetOfArray"); + echo = HasRelated.class.getMethod("echo", Object.class); + throwS = Thrower.class.getMethod("throwS"); + setEntryKV = HashMap.class.getMethod("entrySet").getGenericReturnType(); + } + + public void testDirectInheritance() throws NoSuchMethodException { + TypeLiteral resolver = TypeLiteral.get(arrayListOfString); + assertEquals(listOf(String.class), + resolver.getReturnType(List.class.getMethod("subList", int.class, int.class)).getType()); + assertEquals(ImmutableList.>of(TypeLiteral.get(String.class)), + resolver.getParameterTypes(Collection.class.getMethod("add", Object.class))); + } + + public void testGenericSupertype() { + TypeLiteral resolver = TypeLiteral.get(arrayListOfString); + assertEquals(newParameterizedType(Collection.class, String.class), + resolver.getSupertype(Collection.class).getType()); + assertEquals(newParameterizedType(Iterable.class, String.class), + resolver.getSupertype(Iterable.class).getType()); + assertEquals(newParameterizedType(AbstractList.class, String.class), + resolver.getSupertype(AbstractList.class).getType()); + assertEquals(Object.class, resolver.getSupertype(Object.class).getType()); + } + + public void testRecursiveTypeVariable() { + TypeLiteral resolver = TypeLiteral.get(MyInteger.class); + assertEquals(MyInteger.class, resolver.getParameterTypes(comparableCompareTo).get(0).getType()); + } + + interface MyComparable> extends Comparable {} + + static class MyInteger implements MyComparable { + int value; + public int compareTo(MyInteger o) { + return value - o.value; + } + } + + public void testFields() { + TypeLiteral resolver = TypeLiteral.get(hasGenericFieldsOfShort); + assertEquals(listOf(Short.class), resolver.getFieldType(list).getType()); + assertEquals(Short.class, resolver.getFieldType(instance).getType()); + } + + static class HasGenericFields { + public List list; + public T instance; + } + + public void testGenericConstructor() throws NoSuchMethodException { + TypeLiteral resolver = TypeLiteral.get(hasGenericConstructorOfShort); + assertEquals(Short.class, + resolver.getParameterTypes(newHasGenericConstructor).get(0).getType()); + } + + static class GenericConstructor { + @SuppressWarnings("UnusedDeclaration") + public GenericConstructor(S s, T t) {} + } + + public void testThrowsExceptions() { + TypeLiteral type = TypeLiteral.get(throwerOfNpe); + assertEquals(NullPointerException.class, type.getExceptionTypes(newThrower).get(0).getType()); + assertEquals(NullPointerException.class, type.getExceptionTypes(throwS).get(0).getType()); + } + + static class Thrower { + public Thrower() throws S {} + public void throwS() throws S {} + } + + public void testArrays() { + TypeLiteral resolver = TypeLiteral.get(hasArrayOfShort); + assertEquals(arrayOf(Short.class), resolver.getReturnType(getArray).getType()); + assertEquals(setOf(arrayOf(Short.class)), resolver.getReturnType(getSetOfArray).getType()); + } + + static interface HasArray { + T[] getArray(); + Set getSetOfArray(); + } + + public void testRelatedTypeVariables() { + TypeLiteral resolver = TypeLiteral.get(hasRelatedOfString); + assertEquals(String.class, resolver.getParameterTypes(echo).get(0).getType()); + assertEquals(String.class, resolver.getReturnType(echo).getType()); + } + + interface HasRelated { + T echo(R r); + } + + /** Ensure the cache doesn't cache too much */ + public void testCachingAndReindexing() throws NoSuchMethodException { + TypeLiteral resolver = TypeLiteral.get( + newParameterizedTypeWithOwner(getClass(), HasLists.class, String.class, Short.class)); + assertEquals(listOf(String.class), + resolver.getReturnType(HasLists.class.getMethod("listS")).getType()); + assertEquals(listOf(Short.class), + resolver.getReturnType(HasLists.class.getMethod("listT")).getType()); + } + + interface HasLists { + List listS(); + List listT(); + List> listEntries(); + } + + public void testUnsupportedQueries() throws NoSuchMethodException { + TypeLiteral resolver = TypeLiteral.get(arrayListOfString); + + try { + resolver.getExceptionTypes(stringIndexOf); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("public int java.lang.String.indexOf(java.lang.String) is not defined by a " + + "supertype of java.util.ArrayList", e.getMessage()); + } + try { + resolver.getParameterTypes(stringIndexOf); + fail(); + } catch (Exception e) { + assertEquals("public int java.lang.String.indexOf(java.lang.String) is not defined by a " + + "supertype of java.util.ArrayList", e.getMessage()); + } + try { + resolver.getReturnType(stringIndexOf); + fail(); + } catch (Exception e) { + assertEquals("public int java.lang.String.indexOf(java.lang.String) is not defined by a " + + "supertype of java.util.ArrayList", e.getMessage()); + } + try { + resolver.getSupertype(String.class); + fail(); + } catch (Exception e) { + assertEquals("class java.lang.String is not a supertype of " + + "java.util.ArrayList", e.getMessage()); + } + try { + resolver.getExceptionTypes(newString); + fail(); + } catch (Exception e) { + assertEquals("public java.lang.String(java.lang.String) does not construct " + + "a supertype of java.util.ArrayList", e.getMessage()); + } + try { + resolver.getParameterTypes(newString); + fail(); + } catch (Exception e) { + assertEquals("public java.lang.String(java.lang.String) does not construct " + + "a supertype of java.util.ArrayList", e.getMessage()); + } + } + + public void testResolve() { + TypeLiteral typeResolver = TypeLiteral.get(StringIntegerMap.class); + assertEquals(String.class, typeResolver.resolveType(mapK)); + + typeResolver = new TypeLiteral>() {}; + assertEquals(String.class, typeResolver.resolveType(mapK)); + assertEquals(Types.mapOf(String.class, Integer.class), + typeResolver.getSupertype(Map.class).getType()); + + typeResolver = new TypeLiteral>() {}; + assertEquals(String.class, typeResolver.resolveType(mapK)); + + typeResolver = new TypeLiteral>() {}; + assertEquals(String.class, typeResolver.resolveType(mapK)); + + typeResolver = TypeLiteral.get(StringIntegerHashMap.class); + assertEquals(String.class, typeResolver.resolveType(mapK)); + assertEquals(String.class, typeResolver.resolveType(hashMapK)); + assertEquals(entryStringInteger, typeResolver.resolveType(setEntryKV)); + assertEquals(Object.class, typeResolver.getSupertype(Object.class).getType()); + } + + public void testOnObject() { + TypeLiteral typeResolver = TypeLiteral.get(Object.class); + assertEquals(Object.class, typeResolver.getSupertype(Object.class).getType()); + assertEquals(Object.class, typeResolver.getRawType()); + + // interfaces also resolve Object + typeResolver = TypeLiteral.get(Types.setOf(Integer.class)); + assertEquals(Object.class, typeResolver.getSupertype(Object.class).getType()); + } + + interface StringIntegerMap extends Map {} + interface BetterMap extends Map {} + interface BestMap extends BetterMap {} + static class StringIntegerHashMap extends HashMap {} + + public void testGetSupertype() { + TypeLiteral> listOfString = new TypeLiteral>() {}; + assertEquals(Types.newParameterizedType(AbstractCollection.class, String.class), + listOfString.getSupertype(AbstractCollection.class).getType()); + + TypeLiteral arrayListOfE = TypeLiteral.get(newParameterizedType( + ArrayList.class, ArrayList.class.getTypeParameters())); + assertEquals( + newParameterizedType(AbstractCollection.class, ArrayList.class.getTypeParameters()), + arrayListOfE.getSupertype(AbstractCollection.class).getType()); + } + + public void testGetSupertypeForArraysAsList() { + Class arraysAsListClass = Arrays.asList().getClass(); + Type anotherE = arraysAsListClass.getTypeParameters()[0]; + TypeLiteral type = TypeLiteral.get(newParameterizedType(AbstractList.class, anotherE)); + assertEquals(newParameterizedType(AbstractCollection.class, anotherE), + type.getSupertype(AbstractCollection.class).getType()); + } + + public void testWildcards() throws NoSuchFieldException { + TypeLiteral> ofString = new TypeLiteral>() {}; + + assertEquals(new TypeLiteral>() {}.getType(), + ofString.getFieldType(Parameterized.class.getField("t")).getType()); + assertEquals(new TypeLiteral>() {}.getType(), + ofString.getFieldType(Parameterized.class.getField("extendsT")).getType()); + assertEquals(new TypeLiteral>() {}.getType(), + ofString.getFieldType(Parameterized.class.getField("superT")).getType()); + } + + static class Parameterized { + public List t; + public List extendsT; + public List superT; + } + + // TODO(jessewilson): tests for tricky bounded types like + + public void testEqualsAndHashCode() throws IOException { + TypeLiteral a1 = TypeLiteral.get(arrayListOfString); + TypeLiteral a2 = TypeLiteral.get(arrayListOfString); + TypeLiteral b = TypeLiteral.get(listOf(String.class)); + assertEqualsBothWays(a1, a2); + assertNotSerializable(a1); + assertFalse(a1.equals(b)); + } +} diff --git a/src/test/java/com/google/inject/example/ClientServiceWithDependencyInjection.java b/src/test/java/com/google/inject/example/ClientServiceWithDependencyInjection.java new file mode 100644 index 0000000..838a60b --- /dev/null +++ b/src/test/java/com/google/inject/example/ClientServiceWithDependencyInjection.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.example; + +import static junit.framework.Assert.assertTrue; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ClientServiceWithDependencyInjection { + +// 62 lines + +public interface Service { + void go(); +} + +public static class ServiceImpl implements ClientServiceWithDependencyInjection.Service { + public void go() { + // ... + } +} + +public static class ServiceFactory { + + private ServiceFactory() {} + + private static final Service service = new ServiceImpl(); + + public static Service getInstance() { + return service; + } +} + +public static class Client { + + private final Service service; + + public Client(Service service) { + this.service = service; + } + + public void go() { + service.go(); + } +} + +public static class ClientFactory { + + private ClientFactory() {} + + public static Client getInstance() { + Service service = ServiceFactory.getInstance(); + return new Client(service); + } +} + +public void testClient() { + MockService mock = new MockService(); + Client client = new Client(mock); + client.go(); + assertTrue(mock.isGone()); +} + +public static class MockService implements Service { + + private boolean gone = false; + + public void go() { + gone = true; + } + + public boolean isGone() { + return gone; + } +} + + public static void main(String[] args) { + new ClientServiceWithDependencyInjection().testClient(); + } +} diff --git a/src/test/java/com/google/inject/example/ClientServiceWithFactories.java b/src/test/java/com/google/inject/example/ClientServiceWithFactories.java new file mode 100644 index 0000000..b3829a1 --- /dev/null +++ b/src/test/java/com/google/inject/example/ClientServiceWithFactories.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.example; + +import static junit.framework.Assert.assertTrue; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ClientServiceWithFactories { + +// 58 lines + +public interface Service { + void go(); +} + +public static class ServiceImpl implements Service { + public void go() { + // ... + } +} + +public static class ServiceFactory { + + private ServiceFactory() {} + + private static Service instance = new ServiceImpl(); + + public static Service getInstance() { + return instance; + } + + public static void setInstance(Service service) { + instance = service; + } +} + +public static class Client { + + public void go() { + Service service = ServiceFactory.getInstance(); + service.go(); + } +} + +public void testClient() { + Service previous = ServiceFactory.getInstance(); + try { + final MockService mock = new MockService(); + ServiceFactory.setInstance(mock); + Client client = new Client(); + client.go(); + assertTrue(mock.isGone()); + } + finally { + ServiceFactory.setInstance(previous); + } +} + +public static class MockService implements Service { + + private boolean gone = false; + + public void go() { + gone = true; + } + + public boolean isGone() { + return gone; + } +} + + public static void main(String[] args) { + new ClientServiceWithFactories().testClient(); + } +} diff --git a/src/test/java/com/google/inject/example/ClientServiceWithGuice.java b/src/test/java/com/google/inject/example/ClientServiceWithGuice.java new file mode 100644 index 0000000..c027377 --- /dev/null +++ b/src/test/java/com/google/inject/example/ClientServiceWithGuice.java @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.example; + +import static junit.framework.Assert.assertTrue; + +import com.google.inject.AbstractModule; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Scopes; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ClientServiceWithGuice { + +// 48 lines + +public interface Service { + void go(); +} + +public static class ServiceImpl implements Service { + public void go() { + // ... + } +} + +public static class MyModule extends AbstractModule { + protected void configure() { + bind(Service.class).to(ServiceImpl.class).in(Scopes.SINGLETON); + } +} + +public static class Client { + + private final Service service; + + @Inject + public Client(Service service) { + this.service = service; + } + + public void go() { + service.go(); + } +} + +public void testClient() { + MockService mock = new MockService(); + Client client = new Client(mock); + client.go(); + assertTrue(mock.isGone()); +} + +public static class MockService implements Service { + + private boolean gone = false; + + public void go() { + gone = true; + } + + public boolean isGone() { + return gone; + } +} + +public static void main(String[] args) throws CreationException { + new ClientServiceWithGuice().testClient(); + Injector injector = Guice.createInjector(new MyModule()); + Client client = injector.getInstance(Client.class); +} +} diff --git a/src/test/java/com/google/inject/example/ClientServiceWithGuiceDefaults.java b/src/test/java/com/google/inject/example/ClientServiceWithGuiceDefaults.java new file mode 100644 index 0000000..5a3d9a7 --- /dev/null +++ b/src/test/java/com/google/inject/example/ClientServiceWithGuiceDefaults.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.example; + +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.ImplementedBy; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; + +import junit.framework.Assert; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ClientServiceWithGuiceDefaults { + +// 44 lines + +@ImplementedBy(ServiceImpl.class) +public interface Service { + void go(); +} + +@Singleton +public static class ServiceImpl implements ClientServiceWithGuiceDefaults.Service { + public void go() { + // ... + } +} + +public static class Client { + + private final Service service; + + @Inject + public Client(Service service) { + this.service = service; + } + + public void go() { + service.go(); + } +} + +public void testClient() { + MockService mock = new MockService(); + Client client = new Client(mock); + client.go(); + Assert.assertTrue(mock.isGone()); +} + +public static class MockService implements Service { + + private boolean gone = false; + + public void go() { + gone = true; + } + + public boolean isGone() { + return gone; + } +} + +public static void main(String[] args) throws CreationException { + new ClientServiceWithGuiceDefaults().testClient(); + Injector injector = Guice.createInjector(); + Client client = injector.getProvider(Client.class).get(); +} +} diff --git a/src/test/java/com/google/inject/example/JndiProvider.java b/src/test/java/com/google/inject/example/JndiProvider.java new file mode 100644 index 0000000..fd90dc0 --- /dev/null +++ b/src/test/java/com/google/inject/example/JndiProvider.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.example; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +import javax.naming.Context; +import javax.naming.NamingException; + +class JndiProvider implements Provider { + + @Inject Context context; + final String name; + final Class type; + + JndiProvider(Class type, String name) { + this.name = name; + this.type = type; + } + + public T get() { + try { + return type.cast(context.lookup(name)); + } + catch (NamingException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates a JNDI provider for the given + * type and name. + */ + static Provider fromJndi( + Class type, String name) { + return new JndiProvider(type, name); + } +} diff --git a/src/test/java/com/google/inject/example/JndiProviderClient.java b/src/test/java/com/google/inject/example/JndiProviderClient.java new file mode 100644 index 0000000..897b249 --- /dev/null +++ b/src/test/java/com/google/inject/example/JndiProviderClient.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.example; + +import static com.google.inject.example.JndiProvider.fromJndi; + +import com.google.inject.AbstractModule; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.sql.DataSource; + +class JndiProviderClient { + + public static void main(String[] args) throws CreationException { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { +// Bind Context to the default InitialContext. +bind(Context.class).to(InitialContext.class); + +// Bind to DataSource from JNDI. +bind(DataSource.class) + .toProvider(fromJndi(DataSource.class, "...")); + } + }); + } +} diff --git a/src/test/java/com/google/inject/internal/CycleDetectingLockTest.java b/src/test/java/com/google/inject/internal/CycleDetectingLockTest.java new file mode 100644 index 0000000..0d10824 --- /dev/null +++ b/src/test/java/com/google/inject/internal/CycleDetectingLockTest.java @@ -0,0 +1,101 @@ +package com.google.inject.internal; + +import com.google.inject.internal.CycleDetectingLock.CycleDetectingLockFactory; + +import junit.framework.TestCase; + +import java.util.concurrent.Callable; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +public class CycleDetectingLockTest extends TestCase { + + static final long DEADLOCK_TIMEOUT_SECONDS = 1; + + /** + * Verifies that graph of threads' dependencies is not static and is calculated in runtime using + * information about specific locks. + * + *
+   *   T1: Waits on S1
+   *   T2: Locks B, sends S1, waits on S2
+   *   T1: Locks A, start locking B, sends S2, waits on S3
+   *   T2: Unlocks B, start locking A, sends S3, finishes locking A, unlocks A
+   *   T1: Finishes locking B, unlocks B, unlocks A
+   * 
+ * + *

This should succeed, even though T1 was locked on T2 and T2 is locked on T1 when T2 locks + * A. Incorrect implementation detects a cycle waiting on S3. + */ + + public void testSingletonThreadsRuntimeCircularDependency() throws Exception { + final CyclicBarrier signal1 = new CyclicBarrier(2); + final CyclicBarrier signal2 = new CyclicBarrier(2); + final CyclicBarrier signal3 = new CyclicBarrier(2); + CycleDetectingLockFactory lockFactory = new CycleDetectingLockFactory(); + final CycleDetectingLock lockA = + lockFactory.new ReentrantCycleDetectingLock("A", new ReentrantLock() { + @Override + public void lock() { + if (Thread.currentThread().getName().equals("T2")) { + try { + signal3.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + assertEquals("T1", Thread.currentThread().getName()); + } + super.lock(); + } + }); + final CycleDetectingLock lockB = + lockFactory.new ReentrantCycleDetectingLock("B", new ReentrantLock() { + @Override + public void lock() { + if (Thread.currentThread().getName().equals("T1")) { + try { + signal2.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + signal3.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + assertEquals("T2", Thread.currentThread().getName()); + } + super.lock(); + } + }); + Future firstThreadResult = Executors.newSingleThreadExecutor().submit( + new Callable() { + public Void call() throws Exception { + Thread.currentThread().setName("T1"); + signal1.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertTrue(lockA.lockOrDetectPotentialLocksCycle().isEmpty()); + assertTrue(lockB.lockOrDetectPotentialLocksCycle().isEmpty()); + lockB.unlock(); + lockA.unlock(); + return null; + } + }); + Future secondThreadResult = Executors.newSingleThreadExecutor().submit( + new Callable() { + public Void call() throws Exception { + Thread.currentThread().setName("T2"); + assertTrue(lockB.lockOrDetectPotentialLocksCycle().isEmpty()); + signal1.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + signal2.await(DEADLOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); + lockB.unlock(); + assertTrue(lockA.lockOrDetectPotentialLocksCycle().isEmpty()); + lockA.unlock(); + return null; + } + }); + + firstThreadResult.get(); + secondThreadResult.get(); + } +} diff --git a/src/test/java/com/google/inject/internal/MoreTypesTest.java b/src/test/java/com/google/inject/internal/MoreTypesTest.java new file mode 100644 index 0000000..0ce5504 --- /dev/null +++ b/src/test/java/com/google/inject/internal/MoreTypesTest.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.internal; + +import com.google.inject.TypeLiteral; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Set; + +/** + * @author schmitt@google.com (Peter Schmitt) + */ +public class MoreTypesTest extends TestCase { + + public void testParameterizedTypeToString() { + TypeLiteral> innerString = new TypeLiteral>(){}; + assertEquals("com.google.inject.internal.MoreTypesTest$Inner", + MoreTypes.typeToString(innerString.getType())); + + TypeLiteral>> mapInnerInteger = new TypeLiteral>>() {}; + assertEquals("java.util.Set>", + MoreTypes.typeToString(mapInnerInteger.getType())); + + TypeLiteral, Set>>> mapInnerLongToSetInnerLong = + new TypeLiteral, Set>>>() {}; + assertEquals("java.util.Map, " + + "java.util.Set>>", + MoreTypes.typeToString(mapInnerLongToSetInnerLong.getType())); + } + + public void testEquals_typeVariable() throws Exception { + Type type = getClass().getMethod("testEquals_typeVariable").getTypeParameters()[0]; + assertTrue(MoreTypes.equals(new TypeLiteral() {}.getType(), type)); + } + + public static class Inner {} +} diff --git a/src/test/java/com/google/inject/internal/UniqueAnnotationsTest.java b/src/test/java/com/google/inject/internal/UniqueAnnotationsTest.java new file mode 100644 index 0000000..9676eca --- /dev/null +++ b/src/test/java/com/google/inject/internal/UniqueAnnotationsTest.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.inject.internal; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class UniqueAnnotationsTest extends TestCase { + + @UniqueAnnotations.Internal(31) public Void unused; + + public void testEqualsHashCodeToString() { + Annotation actual = UniqueAnnotations.create(31); + + Annotation expected = getClass().getFields()[0].getAnnotations()[0]; + + assertEquals(expected.toString(), actual.toString()); + assertEquals(expected.hashCode(), actual.hashCode()); + assertEquals(expected, actual); + } +} diff --git a/src/test/java/com/google/inject/internal/WeakKeySetTest.java b/src/test/java/com/google/inject/internal/WeakKeySetTest.java new file mode 100644 index 0000000..f11b393 --- /dev/null +++ b/src/test/java/com/google/inject/internal/WeakKeySetTest.java @@ -0,0 +1,518 @@ +/** + * Copyright (C) 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.internal; + +import static com.google.inject.Asserts.awaitClear; +import static com.google.inject.Asserts.awaitFullGc; +import static com.google.inject.internal.WeakKeySetUtils.assertBlacklisted; +import static com.google.inject.internal.WeakKeySetUtils.assertInSet; +import static com.google.inject.internal.WeakKeySetUtils.assertNotBlacklisted; +import static com.google.inject.internal.WeakKeySetUtils.assertNotInSet; +import static com.google.inject.internal.WeakKeySetUtils.assertSourceNotInSet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Scope; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; +import com.google.inject.spi.ProvisionListenerBinding; +import com.google.inject.spi.ScopeBinding; +import com.google.inject.spi.TypeConverterBinding; +import com.google.inject.spi.TypeListenerBinding; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Tests for {@link WeakKeySet}. + *

+ * Multibinding specific tests can be found in MultibinderTest and MapBinderTest. + * + * @author dweis@google.com (Daniel Weis) + */ +public class WeakKeySetTest extends TestCase { + + private WeakKeySet set; + + @Override + protected void setUp() throws Exception { + set = new WeakKeySet(new Object()); + } + + public void testEviction() { + TestState state = new TestState(); + Key key = Key.get(Integer.class); + Object source = new Object(); + + WeakReference> weakKeyRef = new WeakReference>(key); + + set.add(key, state, source); + assertInSet(set, key, 1, source); + + state = null; + + awaitFullGc(); + + assertNotInSet(set, Key.get(Integer.class)); + + // Ensure there are no hanging references. + key = null; + awaitClear(weakKeyRef); + } + + public void testEviction_nullSource() { + TestState state = new TestState(); + Key key = Key.get(Integer.class); + Object source = null; + + WeakReference> weakKeyRef = new WeakReference>(key); + + set.add(key, state, source); + assertInSet(set, key, 1, source); + + state = null; + + awaitFullGc(); + + assertNotInSet(set, Key.get(Integer.class)); + + // Ensure there are no hanging references. + key = null; + awaitClear(weakKeyRef); + } + + public void testEviction_keyOverlap_2x() { + TestState state1 = new TestState(); + TestState state2 = new TestState(); + Key key1 = Key.get(Integer.class); + Key key2 = Key.get(Integer.class); + Object source1 = new Object(); + Object source2 = new Object(); + + set.add(key1, state1, source1); + assertInSet(set, key1, 1, source1); + + set.add(key2, state2, source2); + assertInSet(set, key2, 2, source1, source2); + + WeakReference> weakKey1Ref = new WeakReference>(key1); + WeakReference> weakKey2Ref = new WeakReference>(key2); + WeakReference weakSource1Ref = new WeakReference(source1); + WeakReference weakSource2Ref = new WeakReference(source2); + + Key key = key1 = key2 = Key.get(Integer.class); + state1 = null; + + awaitFullGc(); + + assertSourceNotInSet(set, key, source1); + assertInSet(set, key, 1, source2); + + source1 = source2 = null; + + awaitClear(weakSource1Ref); + // Key1 will be referenced as the key in the sources backingSet and won't be + // GC'd. + + // Should not be GC'd until state2 goes away. + assertNotNull(weakSource2Ref.get()); + + state2 = null; + + awaitFullGc(); + + assertNotInSet(set, key); + + awaitClear(weakKey2Ref); + awaitClear(weakSource2Ref); + // Now that the backing set is emptied, key1 is released. + awaitClear(weakKey1Ref); + } + + public void testNoEviction_keyOverlap_2x() { + TestState state1 = new TestState(); + TestState state2 = new TestState(); + Key key1 = Key.get(Integer.class); + Key key2 = Key.get(Integer.class); + Object source1 = new Object(); + Object source2 = new Object(); + + set.add(key1, state1, source1); + assertInSet(set, key1, 1, source1); + + set.add(key2, state2, source2); + assertInSet(set, key2, 2, source1, source2); + + WeakReference> weakKey1Ref = new WeakReference>(key1); + WeakReference> weakKey2Ref = new WeakReference>(key2); + + Key key = key1 = key2 = Key.get(Integer.class); + + awaitFullGc(); + assertInSet(set, key, 2, source1, source2); + + // Ensure the keys don't get GC'd when states are still referenced. key1 will be present in the + // as the map key but key2 could be GC'd if the implementation does something wrong. + assertNotNull(weakKey1Ref.get()); + assertNotNull(weakKey2Ref.get()); + } + + public void testEviction_keyAndSourceOverlap_null() { + TestState state1 = new TestState(); + TestState state2 = new TestState(); + Key key1 = Key.get(Integer.class); + Key key2 = Key.get(Integer.class); + Object source = null; + + set.add(key1, state1, source); + assertInSet(set, key1, 1, source); + + set.add(key2, state2, source); + // Same source so still only one value. + assertInSet(set, key2, 1, source); + assertInSet(set, key1, 1, source); + + WeakReference> weakKey1Ref = new WeakReference>(key1); + WeakReference> weakKey2Ref = new WeakReference>(key2); + WeakReference weakSourceRef = new WeakReference(source); + + Key key = key1 = key2 = Key.get(Integer.class); + state1 = null; + + awaitFullGc(); + // Should still have a single source. + assertInSet(set, key, 1, source); + + source = null; + + awaitClear(weakSourceRef); + // Key1 will be referenced as the key in the sources backingSet and won't be + // GC'd. + + state2 = null; + + awaitFullGc(); + assertNotInSet(set, key); + + awaitClear(weakKey2Ref); + awaitClear(weakSourceRef); + // Now that the backing set is emptied, key1 is released. + awaitClear(weakKey1Ref); + } + + public void testEviction_keyAndSourceOverlap_nonNull() { + TestState state1 = new TestState(); + TestState state2 = new TestState(); + Key key1 = Key.get(Integer.class); + Key key2 = Key.get(Integer.class); + Object source = new Object(); + + set.add(key1, state1, source); + assertInSet(set, key1, 1, source); + + set.add(key2, state2, source); + // Same source so still only one value. + assertInSet(set, key2, 1, source); + + WeakReference> weakKey1Ref = new WeakReference>(key1); + WeakReference> weakKey2Ref = new WeakReference>(key2); + WeakReference weakSourceRef = new WeakReference(source); + + Key key = key1 = key2 = Key.get(Integer.class); + state1 = null; + + awaitFullGc(); + + // Same source so still only one value. + assertInSet(set, key, 1, source); + assertInSet(set, key1, 1, source); + + source = null; + + awaitFullGc(); + assertNotNull(weakSourceRef.get()); + // Key1 will be referenced as the key in the sources backingSet and won't be + // GC'd. + + state2 = null; + + awaitFullGc(); + + assertNotInSet(set, key); + + awaitClear(weakKey2Ref); + awaitClear(weakSourceRef); + // Now that the backing set is emptied, key1 is released. + awaitClear(weakKey1Ref); + } + + public void testEviction_keyOverlap_3x() { + TestState state1 = new TestState(); + TestState state2 = new TestState(); + TestState state3 = new TestState(); + Key key1 = Key.get(Integer.class); + Key key2 = Key.get(Integer.class); + Key key3 = Key.get(Integer.class); + Object source1 = new Object(); + Object source2 = new Object(); + Object source3 = new Object(); + + set.add(key1, state1, source1); + assertInSet(set, key1, 1, source1); + + set.add(key2, state2, source2); + assertInSet(set, key1, 2, source1, source2); + + set.add(key3, state3, source3); + assertInSet(set, key1, 3, source1, source2, source3); + + WeakReference> weakKey1Ref = new WeakReference>(key1); + WeakReference> weakKey2Ref = new WeakReference>(key2); + WeakReference> weakKey3Ref = new WeakReference>(key3); + WeakReference weakSource1Ref = new WeakReference(source1); + WeakReference weakSource2Ref = new WeakReference(source2); + WeakReference weakSource3Ref = new WeakReference(source3); + + Key key = key1 = key2 = key3 = Key.get(Integer.class); + state1 = null; + + awaitFullGc(); + assertSourceNotInSet(set, key, source1); + assertInSet(set, key, 2, source2, source3); + + source1 = null; + // Key1 will be referenced as the key in the sources backingSet and won't be + // GC'd. + awaitClear(weakSource1Ref); + + state2 = null; + awaitFullGc(); + assertSourceNotInSet(set, key, source2); + assertInSet(set, key, 1, source3); + + awaitClear(weakKey2Ref); + + source2 = null; + awaitClear(weakSource2Ref); + // Key1 will be referenced as the key in the sources backingSet and won't be + // GC'd. + + state3 = null; + awaitFullGc(); + assertNotInSet(set, key); + + awaitClear(weakKey3Ref); + source3 = null; + awaitClear(weakSource3Ref); + // Now that the backing set is emptied, key1 is released. + awaitClear(weakKey1Ref); + } + + public void testWeakKeySet_integration() { + Injector parentInjector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(4); + } + }); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + + Injector childInjector = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("bar"); + } + }); + WeakReference weakRef = new WeakReference(childInjector); + assertBlacklisted(parentInjector, Key.get(String.class)); + + // Clear the ref, GC, and ensure that we are no longer blacklisting. + childInjector = null; + awaitClear(weakRef); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + } + + public void testWeakKeySet_integration_multipleChildren() { + Injector parentInjector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(4); + } + }); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + assertNotBlacklisted(parentInjector, Key.get(Long.class)); + + Injector childInjector1 = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("foo"); + } + }); + WeakReference weakRef1 = new WeakReference(childInjector1); + assertBlacklisted(parentInjector, Key.get(String.class)); + assertNotBlacklisted(parentInjector, Key.get(Long.class)); + + Injector childInjector2 = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(Long.class).toInstance(6L); + } + }); + WeakReference weakRef2 = new WeakReference(childInjector2); + assertBlacklisted(parentInjector, Key.get(String.class)); + assertBlacklisted(parentInjector, Key.get(Long.class)); + + // Clear ref1, GC, and ensure that we still blacklist. + childInjector1 = null; + awaitClear(weakRef1); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + assertBlacklisted(parentInjector, Key.get(Long.class)); + + // Clear the ref, GC, and ensure that we are no longer blacklisting. + childInjector2 = null; + awaitClear(weakRef2); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + assertNotBlacklisted(parentInjector, Key.get(Long.class)); + } + + public void testWeakKeySet_integration_multipleChildren_overlappingKeys() { + Injector parentInjector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(4); + } + }); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + + Injector childInjector1 = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("foo"); + } + }); + WeakReference weakRef1 = new WeakReference(childInjector1); + assertBlacklisted(parentInjector, Key.get(String.class)); + + Injector childInjector2 = parentInjector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("bar"); + } + }); + WeakReference weakRef2 = new WeakReference(childInjector2); + assertBlacklisted(parentInjector, Key.get(String.class)); + + // Clear ref1, GC, and ensure that we still blacklist. + childInjector1 = null; + awaitClear(weakRef1); + assertBlacklisted(parentInjector, Key.get(String.class)); + + // Clear the ref, GC, and ensure that we are no longer blacklisting. + childInjector2 = null; + awaitClear(weakRef2); + assertNotBlacklisted(parentInjector, Key.get(String.class)); + } + + private static class TestState implements State { + public State parent() { + return new TestState(); + } + + public BindingImpl getExplicitBinding(Key key) { + return null; + } + + public Map, Binding> getExplicitBindingsThisLevel() { + throw new UnsupportedOperationException(); + } + + public void putBinding(Key key, BindingImpl binding) { + throw new UnsupportedOperationException(); + } + + public ScopeBinding getScopeBinding(Class scopingAnnotation) { + return null; + } + + public void putScopeBinding(Class annotationType, ScopeBinding scope) { + throw new UnsupportedOperationException(); + } + + public void addConverter(TypeConverterBinding typeConverterBinding) { + throw new UnsupportedOperationException(); + } + + public TypeConverterBinding getConverter(String stringValue, TypeLiteral type, Errors errors, + Object source) { + throw new UnsupportedOperationException(); + } + + public Iterable getConvertersThisLevel() { + return ImmutableSet.of(); + } + + public void addTypeListener(TypeListenerBinding typeListenerBinding) { + throw new UnsupportedOperationException(); + } + + public List getTypeListenerBindings() { + return ImmutableList.of(); + } + + public void addProvisionListener(ProvisionListenerBinding provisionListenerBinding) { + throw new UnsupportedOperationException(); + } + + public List getProvisionListenerBindings() { + return ImmutableList.of(); + } + + public void addScanner(ModuleAnnotatedMethodScannerBinding scanner) { + throw new UnsupportedOperationException(); + } + + public List getScannerBindings() { + return ImmutableList.of(); + } + + public void blacklist(Key key, State state, Object source) { + } + + public boolean isBlacklisted(Key key) { + return true; + } + + public Set getSourcesForBlacklistedKey(Key key) { + throw new UnsupportedOperationException(); + } + + public Object lock() { + throw new UnsupportedOperationException(); + } + + public Object singletonCreationLock() { + throw new UnsupportedOperationException(); + } + + public Map, Scope> getScopes() { + return ImmutableMap.of(); + } + } +} diff --git a/src/test/java/com/google/inject/internal/WeakKeySetUtils.java b/src/test/java/com/google/inject/internal/WeakKeySetUtils.java new file mode 100644 index 0000000..b023aa1 --- /dev/null +++ b/src/test/java/com/google/inject/internal/WeakKeySetUtils.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.internal; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import com.google.inject.Injector; +import com.google.inject.Key; + +import java.util.Set; + +/** + * Utilities for verifying com.google.inject.internal.WeakKeySet is not leaking memory. + * + * @author dweis@google.com (Daniel Weis) + */ +public final class WeakKeySetUtils { + + private WeakKeySetUtils() {} + + public static void assertBlacklisted(Injector injector, Key key) { + assertBlacklistState(injector, key, true); + } + + public static void assertNotBlacklisted(Injector injector, Key key) { + assertBlacklistState(injector, key, false); + } + + public static void assertNotInSet(WeakKeySet set, Key key) { + // if we're expecting it to not be in the set, loop around and wait for threads to run. + for (int i = 0; i < 10; i++) { + if (!set.contains(key)) { + break; + } + sleep(); + } + assertFalse(set.contains(key)); + assertNull(set.getSources(Key.get(Integer.class))); + } + + public static void assertInSet(WeakKeySet set, Key key, int expectedSources, + Object... sources) { + assertTrue(set.contains(key)); + assertEquals(expectedSources, set.getSources(key).size()); + for (Object source : sources) { + assertTrue("didn't contain source: " + source, set.getSources(key).contains(source)); + } + } + + public static void assertSourceNotInSet(WeakKeySet set, Key key, Object source) { + // if we're expecting it to not be a source, loop around and wait for threads to run. + for (int i = 0; i < 10; i++) { + Set sources = set.getSources(key); + assertNotNull("expected at least one source", source); + if (!sources.contains(source)) { + break; + } + sleep(); + } + Set sources = set.getSources(key); + assertNotNull("expected at least one source", source); + assertFalse(sources.contains(source)); + } + + private static void assertBlacklistState(Injector injector, Key key, boolean isBlacklisted) { + // if we're expecting it to not be blacklisted, loop around and wait for threads to run. + if (!isBlacklisted) { + for (int i = 0; i < 10; i++) { + if (!((InjectorImpl) injector).state.isBlacklisted(key)) { + break; + } + sleep(); + } + } + assertEquals(isBlacklisted, ((InjectorImpl) injector).state.isBlacklisted(key)); + } + + private static void sleep() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + Thread.yield(); + } +} diff --git a/src/test/java/com/google/inject/matcher/MatcherTest.java b/src/test/java/com/google/inject/matcher/MatcherTest.java new file mode 100644 index 0000000..58fbdf2 --- /dev/null +++ b/src/test/java/com/google/inject/matcher/MatcherTest.java @@ -0,0 +1,178 @@ +/** + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.matcher; + +import static com.google.inject.Asserts.assertEqualWhenReserialized; +import static com.google.inject.Asserts.assertEqualsBothWays; +import static com.google.inject.matcher.Matchers.annotatedWith; +import static com.google.inject.matcher.Matchers.any; +import static com.google.inject.matcher.Matchers.identicalTo; +import static com.google.inject.matcher.Matchers.inPackage; +import static com.google.inject.matcher.Matchers.inSubpackage; +import static com.google.inject.matcher.Matchers.not; +import static com.google.inject.matcher.Matchers.only; +import static com.google.inject.matcher.Matchers.returns; +import static com.google.inject.matcher.Matchers.subclassesOf; + +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.AbstractList; + +/** + * @author crazybob@google.com (Bob Lee) + */ + +public class MatcherTest extends TestCase { + + public void testAny() { + assertTrue(any().matches(null)); + assertEquals("any()", any().toString()); + assertEqualsBothWays(any(), any()); + assertFalse(any().equals(not(any()))); + } + + public void testNot() { + assertFalse(not(any()).matches(null)); + assertEquals("not(any())", not(any()).toString()); + assertEqualsBothWays(not(any()), not(any())); + assertFalse(not(any()).equals(any())); + } + + public void testAnd() { + assertTrue(any().and(any()).matches(null)); + assertFalse(any().and(not(any())).matches(null)); + assertEquals("and(any(), any())", any().and(any()).toString()); + assertEqualsBothWays(any().and(any()), any().and(any())); + assertFalse(any().and(any()).equals(not(any()))); + } + + public void testOr() { + assertTrue(any().or(not(any())).matches(null)); + assertFalse(not(any()).or(not(any())).matches(null)); + assertEquals("or(any(), any())", any().or(any()).toString()); + assertEqualsBothWays(any().or(any()), any().or(any())); + assertFalse(any().or(any()).equals(not(any()))); + } + + public void testAnnotatedWith() { + assertTrue(annotatedWith(Foo.class).matches(Bar.class)); + assertFalse(annotatedWith(Foo.class).matches( + MatcherTest.class.getMethods()[0])); + assertEquals("annotatedWith(Foo.class)", annotatedWith(Foo.class).toString()); + assertEqualsBothWays(annotatedWith(Foo.class), annotatedWith(Foo.class)); + assertFalse(annotatedWith(Foo.class).equals(annotatedWith(Named.class))); + + try { + annotatedWith(Baz.class); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testSubclassesOf() { + assertTrue(subclassesOf(Runnable.class).matches(Runnable.class)); + assertTrue(subclassesOf(Runnable.class).matches(MyRunnable.class)); + assertFalse(subclassesOf(Runnable.class).matches(Object.class)); + assertEquals("subclassesOf(Runnable.class)", subclassesOf(Runnable.class).toString()); + assertEqualsBothWays(subclassesOf(Runnable.class), subclassesOf(Runnable.class)); + assertFalse(subclassesOf(Runnable.class).equals(subclassesOf(Object.class))); + } + + public void testOnly() { + assertTrue(only(1000).matches(1000)); + assertFalse(only(1).matches(1000)); + assertEquals("only(1)", only(1).toString()); + assertEqualsBothWays(only(1), only(1)); + assertFalse(only(1).equals(only(2))); + } + + @SuppressWarnings("UnnecessaryBoxing") + public void testIdenticalTo() { + Object o = new Object(); + assertEquals("identicalTo(1)", identicalTo(1).toString()); + assertTrue(identicalTo(o).matches(o)); + assertFalse(identicalTo(o).matches(new Object())); + assertEqualsBothWays(identicalTo(o), identicalTo(o)); + assertFalse(identicalTo(1).equals(identicalTo(new Integer(1)))); + } + + public void testInPackage() { + Package matchersPackage = Matchers.class.getPackage(); + assertEquals("inPackage(com.google.inject.matcher)", inPackage(matchersPackage).toString()); + assertTrue(inPackage(matchersPackage).matches(MatcherTest.class)); + assertFalse(inPackage(matchersPackage).matches(Object.class)); + assertEqualsBothWays(inPackage(matchersPackage), inPackage(matchersPackage)); + assertFalse(inPackage(matchersPackage).equals(inPackage(Object.class.getPackage()))); + } + + public void testInSubpackage() { + String stringPackageName = String.class.getPackage().getName(); + assertEquals("inSubpackage(java.lang)", inSubpackage(stringPackageName).toString()); + assertTrue(inSubpackage(stringPackageName).matches(Object.class)); + assertTrue(inSubpackage(stringPackageName).matches(Method.class)); + assertFalse(inSubpackage(stringPackageName).matches(Matchers.class)); + assertFalse(inSubpackage("jav").matches(Object.class)); + assertEqualsBothWays(inSubpackage(stringPackageName), inSubpackage(stringPackageName)); + assertFalse(inSubpackage(stringPackageName).equals(inSubpackage(Matchers.class.getPackage().getName()))); + } + + public void testReturns() throws NoSuchMethodException { + Matcher predicate = returns(only(String.class)); + assertTrue(predicate.matches( + Object.class.getMethod("toString"))); + assertFalse(predicate.matches( + Object.class.getMethod("hashCode"))); + assertEquals("returns(only(class java.lang.String))", returns(only(String.class)).toString()); + assertEqualsBothWays(predicate, returns(only(String.class))); + assertFalse(predicate.equals(returns(only(Integer.class)))); + } + + public void testSerialization() throws IOException { + assertEqualWhenReserialized(any()); + assertEqualWhenReserialized(not(any())); + assertEqualWhenReserialized(annotatedWith(Named.class)); + assertEqualWhenReserialized(annotatedWith(Names.named("foo"))); + assertEqualWhenReserialized(only("foo")); + assertEqualWhenReserialized(identicalTo(Object.class)); + assertEqualWhenReserialized(inPackage(String.class.getPackage())); + assertEqualWhenReserialized(inSubpackage(String.class.getPackage().getName())); + assertEqualWhenReserialized(returns(any())); + assertEqualWhenReserialized(subclassesOf(AbstractList.class)); + assertEqualWhenReserialized(only("a").or(only("b"))); + assertEqualWhenReserialized(only("a").and(only("b"))); + } + + static abstract class MyRunnable implements Runnable {} + + @Retention(RetentionPolicy.RUNTIME) + @interface Foo {} + + @Foo + static class Bar {} + + @interface Baz {} + + @Baz + static class Car {} +} diff --git a/src/test/java/com/google/inject/name/NamedEquivalanceTest.java b/src/test/java/com/google/inject/name/NamedEquivalanceTest.java new file mode 100644 index 0000000..36dee0c --- /dev/null +++ b/src/test/java/com/google/inject/name/NamedEquivalanceTest.java @@ -0,0 +1,258 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.name; + +import static com.google.inject.Asserts.assertContains; + +import com.google.inject.AbstractModule; +import com.google.inject.ConfigurationException; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provides; + +import junit.framework.TestCase; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.Properties; + +/** + * Tests that {@code javax.inject.Named} and {@code com.google.inject.name.Named} are completely + * interchangeable: bindings for one can be used to inject the other. + * + * @author cgdecker@gmail.com (Colin Decker) + */ +public class NamedEquivalanceTest extends TestCase { + + private static final Module GUICE_BINDING_MODULE = moduleWithAnnotation(Names.named("foo")); + private static final Module JSR330_BINDING_MODULE = moduleWithAnnotation(new JsrNamed("foo")); + private static final Module GUICE_PROVIDER_METHOD_MODULE = getGuiceBindingProviderMethodModule(); + private static final Module JSR330_PROVIDER_METHOD_MODULE = getJsr330BindingProviderMethodModule(); + + public void testKeysCreatedWithDifferentTypesAreEqual() { + assertEquals(keyForAnnotation(new GuiceNamed("foo")), keyForAnnotation(new JsrNamed("foo"))); + assertEquals(keyForAnnotation(Names.named("foo")), keyForAnnotation(new GuiceNamed("foo"))); + assertEquals(keyForAnnotation(Names.named("foo")), keyForAnnotation(new JsrNamed("foo"))); + + assertEquals(keyForAnnotationType(com.google.inject.name.Named.class), + keyForAnnotationType(javax.inject.Named.class)); + } + + private static Key keyForAnnotation(Annotation annotation) { + return Key.get(String.class, annotation); + } + + private static Key keyForAnnotationType(Class annotationType) { + return Key.get(String.class, annotationType); + } + + public void testBindingWithNamesCanInjectBothTypes() { + assertInjectionsSucceed(GUICE_BINDING_MODULE); + } + + public void testBindingWithJsr330AnnotationCanInjectBothTypes() { + assertInjectionsSucceed(JSR330_BINDING_MODULE); + } + + public void testBindingWithGuiceNamedAnnotatedProviderMethodCanInjectBothTypes() { + assertInjectionsSucceed(GUICE_PROVIDER_METHOD_MODULE); + } + + public void testBindingWithJsr330NamedAnnotatedProviderMethodCanInjectBothTypes() { + assertInjectionsSucceed(JSR330_PROVIDER_METHOD_MODULE); + } + + public void testBindingDifferentTypesWithSameValueIsIgnored() { + assertDuplicateBinding(GUICE_BINDING_MODULE, JSR330_BINDING_MODULE, false); + assertDuplicateBinding(JSR330_BINDING_MODULE, GUICE_BINDING_MODULE, false); + } + + public void testBindingDifferentTypesWithSameValueIsAnErrorWithProviderMethods() { + assertDuplicateBinding(GUICE_PROVIDER_METHOD_MODULE, JSR330_PROVIDER_METHOD_MODULE, true); + assertDuplicateBinding(JSR330_PROVIDER_METHOD_MODULE, GUICE_PROVIDER_METHOD_MODULE, true); + } + + public void testBindingDifferentTypesWithSameValueIsAnErrorMixed() { + assertDuplicateBinding(GUICE_BINDING_MODULE, JSR330_PROVIDER_METHOD_MODULE, true); + assertDuplicateBinding(JSR330_BINDING_MODULE, GUICE_PROVIDER_METHOD_MODULE, true); + } + + public void testMissingBindingForGuiceNamedUsesSameTypeInErrorMessage() { + assertMissingBindingErrorMessageUsesType(GuiceNamedClient.class); + } + + public void testMissingBindingForJsr330NamedUsesSameTypeInErrorMessage() { + assertMissingBindingErrorMessageUsesType(Jsr330NamedClient.class); + } + + public void testBindPropertiesWorksWithJsr330() { + assertInjectionsSucceed(new AbstractModule() { + @Override protected void configure() { + Properties properties = new Properties(); + properties.put("foo", "bar"); + Names.bindProperties(binder(), properties); + } + }); + } + + private static void assertMissingBindingErrorMessageUsesType(Class clientType) { + try { + Guice.createInjector().getInstance(clientType); + fail("should have thrown ConfigurationException"); + } catch (ConfigurationException e) { + assertContains(e.getMessage(), + "No implementation for java.lang.String annotated with @com.google.inject.name.Named(value=foo) was bound."); + } + } + + private static void assertDuplicateBinding(Module a, Module b, boolean fails) { + try { + Guice.createInjector(a, b); + if(fails) { + fail("should have thrown CreationException"); + } + } catch (CreationException e) { + if(fails) { + assertContains(e.getMessage(), + "A binding to java.lang.String annotated with @com.google.inject.name.Named(value=foo) was already configured"); + } else { + throw e; + } + } + } + + private static Module moduleWithAnnotation(final Annotation annotation) { + return new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(annotation).to("bar"); + } + }; + } + + private static void assertInjectionsSucceed(Module module) { + Injector injector = Guice.createInjector(module); + assertInjected(injector.getInstance(GuiceNamedClient.class), injector + .getInstance(Jsr330NamedClient.class)); + } + + private static void assertInjected(GuiceNamedClient guiceClient, Jsr330NamedClient jsr330Client) { + assertEquals("bar", guiceClient.foo); + assertEquals("bar", jsr330Client.foo); + } + + private static Module getJsr330BindingProviderMethodModule() { + return new AbstractModule() { + @Override protected void configure() {} + @SuppressWarnings("unused") @Provides @javax.inject.Named("foo") String provideFoo() { + return "bar"; + } + }; + } + + private static Module getGuiceBindingProviderMethodModule() { + return new AbstractModule() { + @Override protected void configure() {} + @SuppressWarnings("unused") @Provides @Named("foo") String provideFoo() { + return "bar"; + } + }; + } + + private static class GuiceNamedClient { + @Inject @Named("foo") String foo; + } + + private static class Jsr330NamedClient { + @Inject @javax.inject.Named("foo") String foo; + } + + private static class JsrNamed implements javax.inject.Named, Serializable { + private final String value; + + public JsrNamed(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + + public int hashCode() { + // This is specified in java.lang.Annotation. + return (127 * "value".hashCode()) ^ value.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof javax.inject.Named)) { + return false; + } + + javax.inject.Named other = (javax.inject.Named) o; + return value.equals(other.value()); + } + + public String toString() { + return "@" + javax.inject.Named.class.getName() + "(value=" + value + ")"; + } + + public Class annotationType() { + return javax.inject.Named.class; + } + + private static final long serialVersionUID = 0; + } + + private static class GuiceNamed implements com.google.inject.name.Named, Serializable { + private final String value; + + public GuiceNamed(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + + public int hashCode() { + // This is specified in java.lang.Annotation. + return (127 * "value".hashCode()) ^ value.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof com.google.inject.name.Named)) { + return false; + } + + com.google.inject.name.Named other = (com.google.inject.name.Named) o; + return value.equals(other.value()); + } + + public String toString() { + return "@" + com.google.inject.name.Named.class.getName() + "(value=" + value + ")"; + } + + public Class annotationType() { + return com.google.inject.name.Named.class; + } + + private static final long serialVersionUID = 0; + } +} diff --git a/src/test/java/com/google/inject/name/NamesTest.java b/src/test/java/com/google/inject/name/NamesTest.java new file mode 100644 index 0000000..0df3513 --- /dev/null +++ b/src/test/java/com/google/inject/name/NamesTest.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.inject.name; + +import static com.google.inject.Asserts.assertEqualWhenReserialized; +import static com.google.inject.Asserts.assertEqualsBothWays; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class NamesTest extends TestCase { + + @Named("foo") private String foo; + private Named namedFoo; + + protected void setUp() throws Exception { + super.setUp(); + namedFoo = getClass().getDeclaredField("foo").getAnnotation(Named.class); + } + + public void testConsistentEqualsAndHashcode() { + Named actual = Names.named("foo"); + assertEqualsBothWays(namedFoo, actual); + assertEquals(namedFoo.toString(), actual.toString()); + } + + public void testNamedIsSerializable() throws IOException { + assertEqualWhenReserialized(Names.named("foo")); + } + + public void testBindPropertiesUsingProperties() { + final Properties teams = new Properties(); + teams.setProperty("SanJose", "Sharks"); + teams.setProperty("Edmonton", "Oilers"); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + Names.bindProperties(binder(), teams); + } + }); + + assertEquals("Sharks", injector.getInstance(Key.get(String.class, Names.named("SanJose")))); + assertEquals("Oilers", injector.getInstance(Key.get(String.class, Names.named("Edmonton")))); + } + + public void testBindPropertiesUsingMap() { + final Map properties = ImmutableMap.of( + "SanJose", "Sharks", "Edmonton", "Oilers"); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + Names.bindProperties(binder(), properties); + } + }); + + assertEquals("Sharks", injector.getInstance(Key.get(String.class, Names.named("SanJose")))); + assertEquals("Oilers", injector.getInstance(Key.get(String.class, Names.named("Edmonton")))); + } + + public void testBindPropertiesIncludesInheritedProperties() { + Properties defaults = new Properties(); + defaults.setProperty("Edmonton", "Eskimos"); + defaults.setProperty("Regina", "Pats"); + + final Properties teams = new Properties(defaults); + teams.setProperty("SanJose", "Sharks"); + teams.setProperty("Edmonton", "Oilers"); + + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + Names.bindProperties(binder(), teams); + } + }); + + assertEquals("Pats", injector.getInstance(Key.get(String.class, Names.named("Regina")))); + assertEquals("Oilers", injector.getInstance(Key.get(String.class, Names.named("Edmonton")))); + assertEquals("Sharks", injector.getInstance(Key.get(String.class, Names.named("SanJose")))); + + try { + injector.getInstance(Key.get(String.class, Names.named("Calgary"))); + fail(); + } catch (RuntimeException expected) { + } + } +} diff --git a/src/test/java/com/google/inject/spi/BindingTargetVisitorTest.java b/src/test/java/com/google/inject/spi/BindingTargetVisitorTest.java new file mode 100644 index 0000000..7740551 --- /dev/null +++ b/src/test/java/com/google/inject/spi/BindingTargetVisitorTest.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.inject.spi; + +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Injector; + +import junit.framework.TestCase; + +/** + * Simple little test that should compile. Ensures that wildcards on the + * generics are correct. + * + * @author phopkins@gmail.com + */ +public class BindingTargetVisitorTest extends TestCase { + public void testBindingTargetVisitorTypeTest() throws Exception { + Injector injector = Guice.createInjector(); + for (Binding binding : injector.getBindings().values()) { + binding.acceptTargetVisitor(new DefaultBindingTargetVisitor() {}); + } + } +} diff --git a/src/test/java/com/google/inject/spi/ElementApplyToTest.java b/src/test/java/com/google/inject/spi/ElementApplyToTest.java new file mode 100644 index 0000000..d483ba4 --- /dev/null +++ b/src/test/java/com/google/inject/spi/ElementApplyToTest.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import com.google.inject.Module; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class ElementApplyToTest extends ElementsTest { + + @Override + protected void checkModule(Module module, ElementVisitor... visitors) { + // convert from module to elements and back + super.checkModule(Elements.getModule(Elements.getElements(module)), visitors); + } +} \ No newline at end of file diff --git a/src/test/java/com/google/inject/spi/ElementSourceTest.java b/src/test/java/com/google/inject/spi/ElementSourceTest.java new file mode 100644 index 0000000..93fbeac --- /dev/null +++ b/src/test/java/com/google/inject/spi/ElementSourceTest.java @@ -0,0 +1,175 @@ +package com.google.inject.spi; + +import static com.google.inject.internal.InternalFlags.getIncludeStackTraceOption; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.Module; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; + +/** + * Tests for {@link ElementSource}. + */ +public class ElementSourceTest extends TestCase { + + private static final StackTraceElement BINDER_INSTALL = + new StackTraceElement("com.google.inject.spi.Elements$RecordingBinder", "install", + "Unknown Source", 234 /* line number*/); + + public void testCallStackSize() { + ModuleSource moduleSource = createModuleSource(); + StackTraceElement[] bindingCallStack = new StackTraceElement[3]; + bindingCallStack[0] = new StackTraceElement( + "com.google.inject.spi.Elements$RecordingBinder", "bind", "Unknown Source", 200); + bindingCallStack[1] = new StackTraceElement( + "com.google.inject.spi.Elements$RecordingBinder", "bind", "Unknown Source", 100); + bindingCallStack[2] = new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$C", "configure", "Unknown Source", 100); + ElementSource elementSource = new ElementSource( + null /* No original element source */, "" /* Don't care */, moduleSource, bindingCallStack); + assertEquals(10 /* call stack size */, elementSource.getStackTrace().length); + } + + public void testGetCallStack_IntegrationTest() throws Exception { + List elements = Elements.getElements(new A()); + for (Element element : elements) { + if (element instanceof Binding) { + Binding binding = (Binding) element; + Class annotationType = binding.getKey().getAnnotationType(); + if (annotationType != null && annotationType.equals(SampleAnnotation.class)) { + ElementSource elementSource = (ElementSource) binding.getSource(); + List moduleClassNames = elementSource.getModuleClassNames(); + // Check module class names + // Module C + assertEquals("com.google.inject.spi.ElementSourceTest$C", moduleClassNames.get(0)); + // Module B + assertEquals("com.google.inject.spi.ElementSourceTest$B", moduleClassNames.get(1)); + // Module A + assertEquals("com.google.inject.spi.ElementSourceTest$A", moduleClassNames.get(2)); + StackTraceElement[] callStack = elementSource.getStackTrace(); + switch(getIncludeStackTraceOption()) { + case OFF: + // Check declaring source + StackTraceElement stackTraceElement = + (StackTraceElement) elementSource.getDeclaringSource(); + assertEquals(new StackTraceElement( + "com.google.inject.spi.ElementSourceTest$C", "configure", null, -1), + stackTraceElement); + // Check call stack + assertEquals(0, callStack.length); + return; + case ONLY_FOR_DECLARING_SOURCE: + // Check call stack + assertEquals(0, callStack.length); + return; + case COMPLETE: + // Check call stack + int skippedCallStackSize = new Throwable().getStackTrace().length - 1; + assertEquals(skippedCallStackSize + 15, elementSource.getStackTrace().length); + assertEquals("com.google.inject.spi.Elements$RecordingBinder", + callStack[0].getClassName()); + assertEquals("com.google.inject.spi.Elements$RecordingBinder", + callStack[1].getClassName()); + assertEquals("com.google.inject.AbstractModule", + callStack[2].getClassName()); + // Module C + assertEquals("com.google.inject.spi.ElementSourceTest$C", + callStack[3].getClassName()); + assertEquals("configure", + callStack[3].getMethodName()); + assertEquals("Unknown Source", + callStack[3].getFileName()); + assertEquals("com.google.inject.AbstractModule", + callStack[4].getClassName()); + assertEquals("com.google.inject.spi.Elements$RecordingBinder", + callStack[5].getClassName()); + // Module B + assertEquals("com.google.inject.spi.ElementSourceTest$B", + callStack[6].getClassName()); + assertEquals("com.google.inject.spi.Elements$RecordingBinder", + callStack[7].getClassName()); + // Module A + assertEquals("com.google.inject.AbstractModule", + callStack[8].getClassName()); + assertEquals("com.google.inject.spi.ElementSourceTest$A", + callStack[9].getClassName()); + assertEquals("com.google.inject.AbstractModule", + callStack[10].getClassName()); + assertEquals("com.google.inject.spi.Elements$RecordingBinder", + callStack[11].getClassName()); + assertEquals("com.google.inject.spi.Elements", + callStack[12].getClassName()); + assertEquals("com.google.inject.spi.Elements", + callStack[13].getClassName()); + assertEquals("com.google.inject.spi.ElementSourceTest", + callStack[14].getClassName()); + // Check modules index + List indexes = elementSource.getModuleConfigurePositionsInStackTrace(); + assertEquals((int) indexes.get(0), 4); + assertEquals((int) indexes.get(1), 6); + assertEquals((int) indexes.get(2), 10); + return; + } + } + } + } + fail("The test should not reach this line."); + } + + private ModuleSource createModuleSource() { + // First module + StackTraceElement[] partialCallStack = new StackTraceElement[1]; + partialCallStack[0] = BINDER_INSTALL; + ModuleSource moduleSource = new ModuleSource(new A(), partialCallStack); + // Second module + partialCallStack = new StackTraceElement[2]; + partialCallStack[0] = BINDER_INSTALL; + partialCallStack[1] = new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$A", "configure", "Unknown Source", 100); + moduleSource = moduleSource.createChild(new B(), partialCallStack); + // Third module + partialCallStack = new StackTraceElement[4]; + partialCallStack[0] = BINDER_INSTALL; + partialCallStack[1] = new StackTraceElement("class1", "method1", "Class1.java", 1); + partialCallStack[2] = new StackTraceElement("class2", "method2", "Class2.java", 2); + partialCallStack[3] = new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$B", "configure", "Unknown Source", 200); + return moduleSource.createChild(new C(), partialCallStack); + } + + private static class A extends AbstractModule { + @Override + public void configure() { + install(new B()); + } + } + + private static class B implements Module { + @Override + public void configure(Binder binder) { + binder.install(new C()); + } + } + + @Retention(RUNTIME) + @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) + @BindingAnnotation + @interface SampleAnnotation { } + + private static class C extends AbstractModule { + @Override + public void configure() { + bind(String.class).annotatedWith(SampleAnnotation.class).toInstance("the value"); + } + } +} diff --git a/src/test/java/com/google/inject/spi/ElementsTest.java b/src/test/java/com/google/inject/spi/ElementsTest.java new file mode 100644 index 0000000..3a106d8 --- /dev/null +++ b/src/test/java/com/google/inject/spi/ElementsTest.java @@ -0,0 +1,1308 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static com.google.inject.Asserts.isIncludeStackTraceComplete; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Module; +import com.google.inject.PrivateBinder; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.binder.AnnotatedConstantBindingBuilder; +import com.google.inject.binder.ConstantBindingBuilder; +import com.google.inject.binder.ScopedBindingBuilder; +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.util.Providers; +import org.junit.Assert; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class ElementsTest extends Assert { + + // Binder fidelity tests + + public void testAddMessageErrorCommand() { + checkModule( + new AbstractModule() { + @Override protected void configure() { + addError("Message %s %d %s", "A", 5, "C"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("Message A 5 C", command.getMessage()); + assertNull(command.getCause()); + assertContains(command.getSources().toString(), + ElementsTest.class.getName(), + getDeclaringSourcePart(ElementsTest.class)); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testAddThrowableErrorCommand() { + checkModule( + new AbstractModule() { + protected void configure() { + addError(new Exception("A")); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("A", command.getCause().getMessage()); + assertEquals(command.getMessage(), + "An exception was caught and reported. Message: A"); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testErrorsAddedWhenExceptionsAreThrown() { + checkModule( + new AbstractModule() { + protected void configure() { + install(new AbstractModule() { + protected void configure() { + throw new RuntimeException("Throwing RuntimeException in AbstractModule.configure()."); + } + }); + + addError("Code after the exception still gets executed"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("Throwing RuntimeException in AbstractModule.configure().", + command.getCause().getMessage()); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("Code after the exception still gets executed", + command.getMessage()); + return null; + } + } + ); + } + + private T getInstance(Binding binding) { + return binding.acceptTargetVisitor(Elements.getInstanceVisitor()); + } + + public void testBindConstantAnnotations() { + checkModule( + new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(SampleAnnotation.class).to("A"); + bindConstant().annotatedWith(Names.named("Bee")).to("B"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey()); + assertEquals("A", getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(String.class, Names.named("Bee")), command.getKey()); + assertEquals("B", getInstance(command)); + return null; + } + } + ); + } + + public void testBindConstantTypes() { + checkModule( + new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(Names.named("String")).to("A"); + bindConstant().annotatedWith(Names.named("int")).to(2); + bindConstant().annotatedWith(Names.named("long")).to(3L); + bindConstant().annotatedWith(Names.named("boolean")).to(false); + bindConstant().annotatedWith(Names.named("double")).to(5.0d); + bindConstant().annotatedWith(Names.named("float")).to(6.0f); + bindConstant().annotatedWith(Names.named("short")).to((short) 7); + bindConstant().annotatedWith(Names.named("char")).to('h'); + bindConstant().annotatedWith(Names.named("byte")).to((byte) 8); + bindConstant().annotatedWith(Names.named("Class")).to(Iterator.class); + bindConstant().annotatedWith(Names.named("Enum")).to(CoinSide.TAILS); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(String.class, Names.named("String")), command.getKey()); + assertEquals("A", getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Integer.class, Names.named("int")), command.getKey()); + assertEquals(2, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Long.class, Names.named("long")), command.getKey()); + assertEquals(3L, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Boolean.class, Names.named("boolean")), command.getKey()); + assertEquals(false, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Double.class, Names.named("double")), command.getKey()); + assertEquals(5.0d, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Float.class, Names.named("float")), command.getKey()); + assertEquals(6.0f, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Short.class, Names.named("short")), command.getKey()); + assertEquals((short) 7, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Character.class, Names.named("char")), command.getKey()); + assertEquals('h', getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Byte.class, Names.named("byte")), command.getKey()); + assertEquals((byte) 8, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(Class.class, Names.named("Class")), command.getKey()); + assertEquals(Iterator.class, getInstance(command)); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(CoinSide.class, Names.named("Enum")), command.getKey()); + assertEquals(CoinSide.TAILS, getInstance(command)); + return null; + } + } + ); + } + + public void testBindKeysNoAnnotations() { + FailingElementVisitor keyChecker = new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class), command.getKey()); + return null; + } + }; + + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("A"); + bind(new TypeLiteral() {}).toInstance("B"); + bind(Key.get(String.class)).toInstance("C"); + } + }, + keyChecker, + keyChecker, + keyChecker + ); + } + + public void testBindKeysWithAnnotationType() { + FailingElementVisitor annotationChecker = new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey()); + return null; + } + }; + + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class).annotatedWith(SampleAnnotation.class).toInstance("A"); + bind(new TypeLiteral() {}).annotatedWith(SampleAnnotation.class).toInstance("B"); + } + }, + annotationChecker, + annotationChecker + ); + } + + public void testBindKeysWithAnnotationInstance() { + FailingElementVisitor annotationChecker = new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class, Names.named("a")), command.getKey()); + return null; + } + }; + + + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class).annotatedWith(Names.named("a")).toInstance("B"); + bind(new TypeLiteral() {}).annotatedWith(Names.named("a")).toInstance("C"); + } + }, + annotationChecker, + annotationChecker + ); + } + + public void testBindToProvider() { + final Provider aProvider = new Provider() { + public String get() { + return "A"; + } + }; + + final javax.inject.Provider intJavaxProvider = new javax.inject.Provider() { + public Integer get() { + return 42; + } + }; + + final javax.inject.Provider doubleJavaxProvider = new javax.inject.Provider() { + @javax.inject.Inject String string; + + public Double get() { + return 42.42; + } + }; + + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class).toProvider(aProvider); + bind(Integer.class).toProvider(intJavaxProvider); + bind(Double.class).toProvider(doubleJavaxProvider); + bind(List.class).toProvider(ListProvider.class); + bind(Collection.class).toProvider(Key.get(ListProvider.class)); + bind(Iterable.class).toProvider(new TypeLiteral>() {}); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof ProviderInstanceBinding); + assertEquals(Key.get(String.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit( + ProviderInstanceBinding binding) { + assertSame(aProvider, binding.getUserSuppliedProvider()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof ProviderInstanceBinding); + assertEquals(Key.get(Integer.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit( + ProviderInstanceBinding binding) { + assertSame(intJavaxProvider, binding.getUserSuppliedProvider()); + assertEquals(42, binding.getUserSuppliedProvider().get()); + // we don't wrap this w/ dependencies if there were none. + assertFalse(binding.getUserSuppliedProvider() instanceof HasDependencies); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof ProviderInstanceBinding); + assertEquals(Key.get(Double.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit( + ProviderInstanceBinding binding) { + assertSame(doubleJavaxProvider, binding.getUserSuppliedProvider()); + assertEquals(42.42, binding.getUserSuppliedProvider().get()); + // we do wrap it with dependencies if there were some. + //assertTrue(binding.getUserSuppliedProvider() instanceof HasDependencies); + Set> deps = + ((HasDependencies) binding.getUserSuppliedProvider()).getDependencies(); + assertEquals(1, deps.size()); + assertEquals(String.class, + deps.iterator().next().getKey().getTypeLiteral().getRawType()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof ProviderKeyBinding); + assertEquals(Key.get(List.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ProviderKeyBinding binding) { + assertEquals(Key.get(ListProvider.class), binding.getProviderKey()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof ProviderKeyBinding); + assertEquals(Key.get(Collection.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ProviderKeyBinding binding) { + assertEquals(Key.get(ListProvider.class), binding.getProviderKey()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof ProviderKeyBinding); + assertEquals(Key.get(Iterable.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ProviderKeyBinding binding) { + assertEquals(new Key>() {}, binding.getProviderKey()); + return null; + } + }); + return null; + } + } + ); + } + + public void testBindToLinkedBinding() { + checkModule( + new AbstractModule() { + protected void configure() { + bind(List.class).to(ArrayList.class); + bind(Map.class).to(new TypeLiteral>() {}); + bind(Set.class).to(Key.get(TreeSet.class, SampleAnnotation.class)); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof LinkedKeyBinding); + assertEquals(Key.get(List.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(LinkedKeyBinding binding) { + assertEquals(Key.get(ArrayList.class), binding.getLinkedKey()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof LinkedKeyBinding); + assertEquals(Key.get(Map.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(LinkedKeyBinding binding) { + assertEquals(Key.get(new TypeLiteral>() {}), + binding.getLinkedKey()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof LinkedKeyBinding); + assertEquals(Key.get(Set.class), command.getKey()); + command.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(LinkedKeyBinding binding) { + assertEquals(Key.get(TreeSet.class, SampleAnnotation.class), + binding.getLinkedKey()); + return null; + } + }); + return null; + } + } + ); + } + + public void testBindToInstance() { + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("A"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertTrue(command instanceof InstanceBinding); + assertEquals(Key.get(String.class), command.getKey()); + assertEquals("A", getInstance(command)); + return null; + } + } + ); + } + + public void testBindInScopes() { + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class); + bind(List.class).to(ArrayList.class).in(Scopes.SINGLETON); + bind(Map.class).to(HashMap.class).in(Singleton.class); + bind(Set.class).to(TreeSet.class).asEagerSingleton(); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitNoScoping() { + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(List.class), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitScope(Scope scope) { + assertEquals(Scopes.SINGLETON, scope); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(Map.class), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitScopeAnnotation(Class annotation) { + assertEquals(Singleton.class, annotation); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(Set.class), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + public Void visitEagerSingleton() { + return null; + } + }); + return null; + } + } + ); + } + + public void testBindToInstanceInScope() { + checkModule( + new AbstractModule() { + protected void configure() { + AnnotatedBindingBuilder b = bind(String.class); + b.toInstance("A"); + b.in(Singleton.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("Setting the scope is not permitted when binding to a single instance.", + command.getMessage()); + assertNull(command.getCause()); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testBindToInstanceScope() { + checkModule( + new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("A"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertEquals(Key.get(String.class), binding.getKey()); + binding.acceptScopingVisitor(new FailingBindingScopingVisitor() { + public Void visitEagerSingleton() { + return null; + } + }); + return null; + } + } + ); + } + + public void testBindScope() { + checkModule( + new AbstractModule() { + protected void configure() { + bindScope(SampleAnnotation.class, Scopes.NO_SCOPE); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(ScopeBinding command) { + assertSame(SampleAnnotation.class, command.getAnnotationType()); + assertSame(Scopes.NO_SCOPE, command.getScope()); + return null; + } + } + ); + } + + public void testBindListener() { + final Matcher typeMatcher = Matchers.only(TypeLiteral.get(String.class)); + final TypeListener listener = new TypeListener() { + public void hear(TypeLiteral type, TypeEncounter encounter) { + throw new UnsupportedOperationException(); + } + }; + + checkModule( + new AbstractModule() { + protected void configure() { + bindListener(typeMatcher, listener); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(TypeListenerBinding binding) { + assertSame(typeMatcher, binding.getTypeMatcher()); + assertSame(listener, binding.getListener()); + return null; + } + } + ); + } + + public void testConvertToTypes() { + final TypeConverter typeConverter = new TypeConverter() { + public Object convert(String value, TypeLiteral toType) { + return value; + } + }; + + checkModule( + new AbstractModule() { + protected void configure() { + convertToTypes(Matchers.any(), typeConverter); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(TypeConverterBinding command) { + assertSame(typeConverter, command.getTypeConverter()); + assertSame(Matchers.any(), command.getTypeMatcher()); + return null; + } + } + ); + } + + public void testGetProvider() { + checkModule( + new AbstractModule() { + protected void configure() { + Provider keyGetProvider + = getProvider(Key.get(String.class, SampleAnnotation.class)); + try { + keyGetProvider.get(); + } catch (IllegalStateException e) { + assertEquals("This Provider cannot be used until the Injector has been created.", + e.getMessage()); + } + + Provider typeGetProvider = getProvider(String.class); + try { + typeGetProvider.get(); + } catch (IllegalStateException e) { + assertEquals("This Provider cannot be used until the Injector has been created.", + e.getMessage()); + } + } + }, + + new FailingElementVisitor() { + @Override public Void visit(ProviderLookup command) { + assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey()); + assertNull(command.getDelegate()); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(ProviderLookup command) { + assertEquals(Key.get(String.class), command.getKey()); + assertNull(command.getDelegate()); + return null; + } + } + ); + } + + public void testElementInitialization() { + final AtomicReference> providerFromBinder + = new AtomicReference>(); + final AtomicReference> membersInjectorFromBinder + = new AtomicReference>(); + + final AtomicReference lastInjected = new AtomicReference(); + final MembersInjector stringInjector = new MembersInjector() { + public void injectMembers(String instance) { + lastInjected.set(instance); + } + }; + + checkModule( + new AbstractModule() { + protected void configure() { + providerFromBinder.set(getProvider(String.class)); + membersInjectorFromBinder.set(getMembersInjector(String.class)); + } + }, + + new FailingElementVisitor() { + public Void visit(ProviderLookup providerLookup) { + @SuppressWarnings("unchecked") // we know that T is a String here + ProviderLookup stringLookup = (ProviderLookup) providerLookup; + stringLookup.initializeDelegate(Providers.of("out")); + + assertEquals("out", providerFromBinder.get().get()); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(MembersInjectorLookup lookup) { + @SuppressWarnings("unchecked") // we know that T is a String here + MembersInjectorLookup stringLookup = (MembersInjectorLookup) lookup; + stringLookup.initializeDelegate(stringInjector); + + membersInjectorFromBinder.get().injectMembers("in"); + assertEquals("in", lastInjected.get()); + return null; + } + }); + } + + public void testGetMembersInjector() { + checkModule( + new AbstractModule() { + protected void configure() { + MembersInjector> typeMembersInjector + = getMembersInjector(new TypeLiteral>() {}); + try { + typeMembersInjector.injectMembers(new A()); + } catch (IllegalStateException e) { + assertEquals( + "This MembersInjector cannot be used until the Injector has been created.", + e.getMessage()); + } + + MembersInjector classMembersInjector = getMembersInjector(String.class); + try { + classMembersInjector.injectMembers("hello"); + } catch (IllegalStateException e) { + assertEquals( + "This MembersInjector cannot be used until the Injector has been created.", + e.getMessage()); + } + } + }, + + new FailingElementVisitor() { + @Override public Void visit(MembersInjectorLookup command) { + assertEquals(new TypeLiteral>() {}, command.getType()); + assertNull(command.getDelegate()); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(MembersInjectorLookup command) { + assertEquals(TypeLiteral.get(String.class), command.getType()); + assertNull(command.getDelegate()); + return null; + } + } + ); + } + + public void testRequestInjection() { + final Object firstObject = new Object(); + final Object secondObject = new Object(); + + checkModule( + new AbstractModule() { + protected void configure() { + requestInjection(firstObject); + requestInjection(secondObject); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(InjectionRequest command) { + assertEquals(firstObject, command.getInstance()); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(InjectionRequest command) { + assertEquals(secondObject, command.getInstance()); + return null; + } + } + ); + } + + public void testRequestStaticInjection() { + checkModule( + new AbstractModule() { + protected void configure() { + requestStaticInjection(ArrayList.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(StaticInjectionRequest command) { + assertEquals(ArrayList.class, command.getType()); + return null; + } + } + ); + } + + public void testNewPrivateBinder() { + final Key collection = Key.get(Collection.class, SampleAnnotation.class); + final Key arrayList = Key.get(ArrayList.class); + final ImmutableSet> collections = ImmutableSet.>of(arrayList, collection); + + final Key a = Key.get(String.class, Names.named("a")); + final Key b = Key.get(String.class, Names.named("b")); + final ImmutableSet> ab = ImmutableSet.of(a, b); + + checkModule( + new AbstractModule() { + protected void configure() { + PrivateBinder one = binder().newPrivateBinder(); + one.expose(ArrayList.class); + one.expose(Collection.class).annotatedWith(SampleAnnotation.class); + one.bind(List.class).to(ArrayList.class); + + PrivateBinder two = binder().withSource("1 FooBar") + .newPrivateBinder().withSource("2 FooBar"); + two.expose(String.class).annotatedWith(Names.named("a")); + two.expose(b); + two.bind(List.class).to(ArrayList.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(PrivateElements one) { + assertEquals(collections, one.getExposedKeys()); + checkElements(one.getElements(), + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertEquals(Key.get(List.class), binding.getKey()); + return null; + } + } + ); + return null; + } + }, + + new ExternalFailureVisitor() { + @Override public Void visit(PrivateElements two) { + assertEquals(ab, two.getExposedKeys()); + assertEquals("1 FooBar", two.getSource().toString()); + checkElements(two.getElements(), + new ExternalFailureVisitor() { + @Override public Void visit(Binding binding) { + assertEquals("2 FooBar", binding.getSource().toString()); + assertEquals(Key.get(List.class), binding.getKey()); + return null; + } + } + ); + return null; + } + } + ); + } + + public void testBindWithMultipleAnnotationsAddsError() { + checkModule( + new AbstractModule() { + protected void configure() { + AnnotatedBindingBuilder abb = bind(String.class); + abb.annotatedWith(SampleAnnotation.class); + abb.annotatedWith(Names.named("A")); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("More than one annotation is specified for this binding.", + command.getMessage()); + assertNull(command.getCause()); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testBindWithMultipleTargetsAddsError() { + checkModule( + new AbstractModule() { + protected void configure() { + AnnotatedBindingBuilder abb = bind(String.class); + abb.toInstance("A"); + abb.toInstance("B"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("Implementation is set more than once.", command.getMessage()); + assertNull(command.getCause()); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testBindWithMultipleScopesAddsError() { + checkModule( + new AbstractModule() { + protected void configure() { + ScopedBindingBuilder sbb = bind(List.class).to(ArrayList.class); + sbb.in(Scopes.NO_SCOPE); + sbb.asEagerSingleton(); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("Scope is set more than once.", command.getMessage()); + assertNull(command.getCause()); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testBindConstantWithMultipleAnnotationsAddsError() { + checkModule( + new AbstractModule() { + protected void configure() { + AnnotatedConstantBindingBuilder cbb = bindConstant(); + cbb.annotatedWith(SampleAnnotation.class).to("A"); + cbb.annotatedWith(Names.named("A")); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message command) { + assertEquals("More than one annotation is specified for this binding.", + command.getMessage()); + assertNull(command.getCause()); + assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testBindConstantWithMultipleTargetsAddsError() { + checkModule( + new AbstractModule() { + protected void configure() { + ConstantBindingBuilder cbb = bindConstant().annotatedWith(SampleAnnotation.class); + cbb.to("A"); + cbb.to("B"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Message message) { + assertEquals("Constant value is set more than once.", message.getMessage()); + assertNull(message.getCause()); + assertContains(message.getSource(), getDeclaringSourcePart(ElementsTest.class)); + return null; + } + } + ); + } + + public void testBindToConstructor() throws NoSuchMethodException, NoSuchFieldException { + final Constructor aConstructor = A.class.getDeclaredConstructor(); + final Constructor bConstructor = B.class.getDeclaredConstructor(Object.class); + final Field field = B.class.getDeclaredField("stage"); + + checkModule( + new AbstractModule() { + protected void configure() { + bind(A.class).toConstructor(aConstructor); + bind(B.class).toConstructor(bConstructor, new TypeLiteral>() {}) + .in(Singleton.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertEquals(new Key() {}, binding.getKey()); + + return binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ConstructorBinding constructorBinding) { + InjectionPoint injectionPoint = constructorBinding.getConstructor(); + assertEquals(aConstructor, injectionPoint.getMember()); + assertEquals(new TypeLiteral() {}, injectionPoint.getDeclaringType()); + return null; + } + }); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertEquals(new Key() {}, binding.getKey()); + binding.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitScopeAnnotation(Class annotation) { + assertEquals(Singleton.class, annotation); + return null; + } + }); + + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ConstructorBinding constructorBinding) { + assertEquals(bConstructor, constructorBinding.getConstructor().getMember()); + assertEquals(Key.get(Integer.class), + getOnlyElement(constructorBinding.getConstructor().getDependencies()).getKey()); + assertEquals(field, + getOnlyElement(constructorBinding.getInjectableMembers()).getMember()); + assertEquals(2, constructorBinding.getDependencies().size()); + return null; + } + }); + return null; + } + } + ); + } + + public void testBindToMalformedConstructor() throws NoSuchMethodException, NoSuchFieldException { + final Constructor constructor = C.class.getDeclaredConstructor(Integer.class); + + checkModule( + new AbstractModule() { + protected void configure() { + bind(C.class).toConstructor(constructor); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertEquals(Key.get(C.class), binding.getKey()); + assertTrue(binding instanceof UntargettedBinding); + return null; + } + }, + + new ExternalFailureVisitor() { + @Override public Void visit(Message message) { + assertContains(message.getMessage(), + C.class.getName() + ".a has more than one annotation ", + Named.class.getName(), SampleAnnotation.class.getName()); + return null; + } + }, + + new ExternalFailureVisitor() { + @Override public Void visit(Message message) { + assertContains(message.getMessage(), + C.class.getName() + ".() has more than one annotation ", + Named.class.getName(), SampleAnnotation.class.getName()); + return null; + } + } + ); + } + + // Business logic tests + + public void testModulesAreInstalledAtMostOnce() { + final AtomicInteger aConfigureCount = new AtomicInteger(0); + final Module a = new AbstractModule() { + public void configure() { + aConfigureCount.incrementAndGet(); + } + }; + + Elements.getElements(a, a); + assertEquals(1, aConfigureCount.get()); + + aConfigureCount.set(0); + Module b = new AbstractModule() { + protected void configure() { + install(a); + install(a); + } + }; + + Elements.getElements(b); + assertEquals(1, aConfigureCount.get()); + } + + /** + * Ensures the module performs the commands consistent with {@code visitors}. + */ + protected void checkModule(Module module, ElementVisitor... visitors) { + List elements = Elements.getElements(module); + assertEquals(elements.size(), visitors.length); + checkElements(elements, visitors); + } + + protected void checkElements(List elements, ElementVisitor... visitors) { + for (int i = 0; i < visitors.length; i++) { + ElementVisitor visitor = visitors[i]; + Element element = elements.get(i); + if (!(element instanceof Message)) { + ElementSource source = (ElementSource) element.getSource(); + assertFalse(source.getModuleClassNames().isEmpty()); + if (isIncludeStackTraceComplete()) { + assertTrue(source.getStackTrace().length > 0); + } else { + assertEquals(0, source.getStackTrace().length); + } + } + if (!(visitor instanceof ExternalFailureVisitor)) { + assertContains(element.getSource().toString(), getDeclaringSourcePart(ElementsTest.class)); + } + element.acceptVisitor(visitor); + } + } + + private static class ListProvider implements Provider { + public List get() { + return new ArrayList(); + } + } + + private static class TProvider implements Provider { + public T get() { + return null; + } + } + + /** + * By extending this interface rather than FailingElementVisitor, the source of the error doesn't + * need to contain the string {@code ElementsTest.java}. + */ + abstract class ExternalFailureVisitor extends FailingElementVisitor {} + + @Retention(RUNTIME) + @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) + @BindingAnnotation + public @interface SampleAnnotation { } + + public enum CoinSide { HEADS, TAILS } + + static class A { + @Inject Stage stage; + } + + static class B { + @Inject Stage stage; + B(T t) {} + } + + static class C { + @Inject @Named("foo") @SampleAnnotation String a; + C(@Named("bar") @SampleAnnotation Integer b) {} + } +} diff --git a/src/test/java/com/google/inject/spi/FailingBindingScopingVisitor.java b/src/test/java/com/google/inject/spi/FailingBindingScopingVisitor.java new file mode 100644 index 0000000..bd08c4b --- /dev/null +++ b/src/test/java/com/google/inject/spi/FailingBindingScopingVisitor.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import com.google.inject.Scope; + +import junit.framework.AssertionFailedError; + +import java.lang.annotation.Annotation; + +public class FailingBindingScopingVisitor implements BindingScopingVisitor { + + public Void visitEagerSingleton() { + throw new AssertionFailedError(); + } + + public Void visitScope(Scope scope) { + throw new AssertionFailedError(); + } + + public Void visitScopeAnnotation(Class scopeAnnotation) { + throw new AssertionFailedError(); + } + + public Void visitNoScoping() { + throw new AssertionFailedError(); + } +} \ No newline at end of file diff --git a/src/test/java/com/google/inject/spi/FailingElementVisitor.java b/src/test/java/com/google/inject/spi/FailingElementVisitor.java new file mode 100644 index 0000000..d40d084 --- /dev/null +++ b/src/test/java/com/google/inject/spi/FailingElementVisitor.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import junit.framework.AssertionFailedError; + +class FailingElementVisitor extends DefaultElementVisitor { + @Override protected Void visitOther(Element element) { + throw new AssertionFailedError(); + } +} diff --git a/src/test/java/com/google/inject/spi/FailingTargetVisitor.java b/src/test/java/com/google/inject/spi/FailingTargetVisitor.java new file mode 100644 index 0000000..68d5f7e --- /dev/null +++ b/src/test/java/com/google/inject/spi/FailingTargetVisitor.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import com.google.inject.Binding; + +import junit.framework.AssertionFailedError; + +public class FailingTargetVisitor extends DefaultBindingTargetVisitor { + @Override protected Void visitOther(Binding binding) { + throw new AssertionFailedError(); + } +} diff --git a/src/test/java/com/google/inject/spi/HasDependenciesTest.java b/src/test/java/com/google/inject/spi/HasDependenciesTest.java new file mode 100644 index 0000000..670f2f4 --- /dev/null +++ b/src/test/java/com/google/inject/spi/HasDependenciesTest.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; + +import junit.framework.TestCase; + +import java.util.Set; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class HasDependenciesTest extends TestCase { + + /** + * When an instance implements HasDependencies, the injected dependencies aren't used. + */ + public void testInstanceWithDependencies() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class).toInstance(new AWithDependencies()); + } + }); + + InstanceBinding binding = (InstanceBinding) injector.getBinding(A.class); + assertEquals(ImmutableSet.>of(Dependency.get(Key.get(Integer.class))), + binding.getDependencies()); + } + + public void testInstanceWithoutDependencies() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class).toInstance(new A()); + } + }); + + InstanceBinding binding = (InstanceBinding) injector.getBinding(A.class); + Dependency onlyDependency = Iterables.getOnlyElement(binding.getDependencies()); + assertEquals(Key.get(String.class), onlyDependency.getKey()); + } + + public void testProvider() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(A.class).toProvider(new ProviderOfA()); + } + }); + + ProviderInstanceBinding binding = (ProviderInstanceBinding) injector.getBinding(A.class); + Dependency onlyDependency = Iterables.getOnlyElement(binding.getDependencies()); + assertEquals(Key.get(String.class), onlyDependency.getKey()); + } + + static class A { + @Inject void injectUnusedDependencies(String unused) {} + } + + static class ProviderOfA implements Provider { + @Inject void injectUnusedDependencies(String unused) {} + + public A get() { + throw new UnsupportedOperationException(); + } + } + + static class AWithDependencies extends A implements HasDependencies { + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(Key.get(Integer.class))); + } + } + + static class ProviderOfAWithDependencies + extends ProviderOfA implements ProviderWithDependencies { + public Set> getDependencies() { + return ImmutableSet.>of(Dependency.get(Key.get(Integer.class))); + } + } +} diff --git a/src/test/java/com/google/inject/spi/InjectionPointTest.java b/src/test/java/com/google/inject/spi/InjectionPointTest.java new file mode 100644 index 0000000..2ba4f5b --- /dev/null +++ b/src/test/java/com/google/inject/spi/InjectionPointTest.java @@ -0,0 +1,366 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.assertEqualsBothWays; +import static com.google.inject.Asserts.assertNotSerializable; +import static com.google.inject.name.Names.named; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.ErrorsException; +import com.google.inject.name.Named; +import com.google.inject.spi.InjectionPoint.Signature; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class InjectionPointTest extends TestCase { + + public @Inject @Named("a") String foo; + public @Inject void bar(@Named("b") String param) {} + + public static class Constructable { + @Inject public Constructable(@Named("c") String param) {} + } + + public void testFieldInjectionPoint() throws NoSuchFieldException, IOException, ErrorsException { + TypeLiteral typeLiteral = TypeLiteral.get(getClass()); + Field fooField = getClass().getField("foo"); + + InjectionPoint injectionPoint = new InjectionPoint(typeLiteral, fooField, false); + assertSame(fooField, injectionPoint.getMember()); + assertFalse(injectionPoint.isOptional()); + assertEquals(getClass().getName() + ".foo", injectionPoint.toString()); + assertEqualsBothWays(injectionPoint, new InjectionPoint(typeLiteral, fooField, false)); + assertNotSerializable(injectionPoint); + + Dependency dependency = getOnlyElement(injectionPoint.getDependencies()); + assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=a)]@" + + getClass().getName() + ".foo", dependency.toString()); + assertEquals(fooField, dependency.getInjectionPoint().getMember()); + assertEquals(-1, dependency.getParameterIndex()); + assertEquals(Key.get(String.class, named("a")), dependency.getKey()); + assertFalse(dependency.isNullable()); + assertNotSerializable(dependency); + assertEqualsBothWays(dependency, + getOnlyElement(new InjectionPoint(typeLiteral, fooField, false).getDependencies())); + } + + public void testMethodInjectionPoint() throws Exception { + TypeLiteral typeLiteral = TypeLiteral.get(getClass()); + + Method barMethod = getClass().getMethod("bar", String.class); + InjectionPoint injectionPoint = new InjectionPoint(typeLiteral, barMethod, false); + assertSame(barMethod, injectionPoint.getMember()); + assertFalse(injectionPoint.isOptional()); + assertEquals(getClass().getName() + ".bar()", injectionPoint.toString()); + assertEqualsBothWays(injectionPoint, new InjectionPoint(typeLiteral, barMethod, false)); + assertNotSerializable(injectionPoint); + + Dependency dependency = getOnlyElement(injectionPoint.getDependencies()); + assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=b)]@" + + getClass().getName() + ".bar()[0]", dependency.toString()); + assertEquals(barMethod, dependency.getInjectionPoint().getMember()); + assertEquals(0, dependency.getParameterIndex()); + assertEquals(Key.get(String.class, named("b")), dependency.getKey()); + assertFalse(dependency.isNullable()); + assertNotSerializable(dependency); + assertEqualsBothWays(dependency, + getOnlyElement(new InjectionPoint(typeLiteral, barMethod, false).getDependencies())); + } + + public void testConstructorInjectionPoint() throws NoSuchMethodException, IOException, + ErrorsException { + TypeLiteral typeLiteral = TypeLiteral.get(Constructable.class); + + Constructor constructor = Constructable.class.getConstructor(String.class); + InjectionPoint injectionPoint = new InjectionPoint(typeLiteral, constructor); + assertSame(constructor, injectionPoint.getMember()); + assertFalse(injectionPoint.isOptional()); + assertEquals(Constructable.class.getName() + ".()", injectionPoint.toString()); + assertEqualsBothWays(injectionPoint, new InjectionPoint(typeLiteral, constructor)); + assertNotSerializable(injectionPoint); + + Dependency dependency = getOnlyElement(injectionPoint.getDependencies()); + assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=c)]@" + + Constructable.class.getName() + ".()[0]", dependency.toString()); + assertEquals(constructor, dependency.getInjectionPoint().getMember()); + assertEquals(0, dependency.getParameterIndex()); + assertEquals(Key.get(String.class, named("c")), dependency.getKey()); + assertFalse(dependency.isNullable()); + assertNotSerializable(dependency); + assertEqualsBothWays(dependency, + getOnlyElement(new InjectionPoint(typeLiteral, constructor).getDependencies())); + } + + public void testUnattachedDependency() throws IOException { + Dependency dependency = Dependency.get(Key.get(String.class, named("d"))); + assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=d)]", + dependency.toString()); + assertNull(dependency.getInjectionPoint()); + assertEquals(-1, dependency.getParameterIndex()); + assertEquals(Key.get(String.class, named("d")), dependency.getKey()); + assertTrue(dependency.isNullable()); + assertNotSerializable(dependency); + assertEqualsBothWays(dependency, Dependency.get(Key.get(String.class, named("d")))); + } + + public void testForConstructor() throws NoSuchMethodException { + Constructor constructor = HashSet.class.getConstructor(); + TypeLiteral> hashSet = new TypeLiteral>() {}; + + InjectionPoint injectionPoint = InjectionPoint.forConstructor(constructor, hashSet); + assertSame(constructor, injectionPoint.getMember()); + assertEquals(ImmutableList.of(), injectionPoint.getDependencies()); + assertFalse(injectionPoint.isOptional()); + + try { + InjectionPoint.forConstructor(constructor, new TypeLiteral>() {}); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), "java.util.LinkedHashSet", + " does not define java.util.HashSet.()", + " while locating java.util.LinkedHashSet"); + } + + try { + InjectionPoint.forConstructor((Constructor) constructor, new TypeLiteral>() {}); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), "java.util.Set", + " does not define java.util.HashSet.()", + " while locating java.util.Set"); + } + } + + public void testForConstructorOf() { + InjectionPoint injectionPoint = InjectionPoint.forConstructorOf(Constructable.class); + assertEquals(Constructable.class.getName() + ".()", injectionPoint.toString()); + } + + public void testAddForInstanceMethodsAndFields() throws Exception { + Method instanceMethod = HasInjections.class.getMethod("instanceMethod", String.class); + Field instanceField = HasInjections.class.getField("instanceField"); + + TypeLiteral type = TypeLiteral.get(HasInjections.class); + assertEquals(ImmutableSet.of( + new InjectionPoint(type, instanceMethod, false), + new InjectionPoint(type, instanceField, false)), + InjectionPoint.forInstanceMethodsAndFields(HasInjections.class)); + } + + public void testAddForStaticMethodsAndFields() throws Exception { + Method staticMethod = HasInjections.class.getMethod("staticMethod", String.class); + Field staticField = HasInjections.class.getField("staticField"); + + Set injectionPoints = InjectionPoint.forStaticMethodsAndFields( + HasInjections.class); + assertEquals(ImmutableSet.of( + new InjectionPoint(TypeLiteral.get(HasInjections.class), staticMethod, false), + new InjectionPoint(TypeLiteral.get(HasInjections.class), staticField, false)), + injectionPoints); + } + + static class HasInjections { + @Inject public static void staticMethod(@Named("a") String a) {} + @Inject @Named("c") public static String staticField; + @Inject public void instanceMethod(@Named("d") String d) {} + @Inject @Named("f") public String instanceField; + } + + public void testAddForParameterizedInjections() { + TypeLiteral type = new TypeLiteral>() {}; + + InjectionPoint constructor = InjectionPoint.forConstructorOf(type); + assertEquals(new Key>() {}, + getOnlyElement(constructor.getDependencies()).getKey()); + + InjectionPoint field = getOnlyElement(InjectionPoint.forInstanceMethodsAndFields(type)); + assertEquals(new Key>() {}, getOnlyElement(field.getDependencies()).getKey()); + } + + static class ParameterizedInjections { + @Inject Set setOfTees; + @Inject public ParameterizedInjections(Map map) {} + } + + public void testSignature() throws Exception { + Signature fooA = new Signature(Foo.class.getDeclaredMethod( + "a", String.class, int.class)); + Signature fooB = new Signature(Foo.class.getDeclaredMethod("b")); + Signature barA = new Signature(Bar.class.getDeclaredMethod( + "a", String.class, int.class)); + Signature barB = new Signature(Bar.class.getDeclaredMethod("b")); + + assertEquals(fooA.hashCode(), barA.hashCode()); + assertEquals(fooB.hashCode(), barB.hashCode()); + assertEquals(fooA, barA); + assertEquals(fooB, barB); + } + + static class Foo { + void a(String s, int i) {} + int b() { + return 0; + } + } + static class Bar { + public void a(String s, int i) {} + void b() {} + } + + public void testOverrideBehavior() { + Set points; + + points = InjectionPoint.forInstanceMethodsAndFields(Super.class); + assertEquals(points.toString(), 6, points.size()); + assertPoints(points, Super.class, "atInject", "gInject", "privateAtAndPublicG", + "privateGAndPublicAt", "atFirstThenG", "gFirstThenAt"); + + points = InjectionPoint.forInstanceMethodsAndFields(Sub.class); + assertEquals(points.toString(), 7, points.size()); + // Superclass will always have is private members injected, + // and 'gInject' was last @Injected in Super, so that remains the owner + assertPoints(points, Super.class, "privateAtAndPublicG", "privateGAndPublicAt", "gInject"); + // Subclass also has the "private" methods, but they do not override + // the superclass' methods, and it now owns the inject2 methods. + assertPoints(points, Sub.class, "privateAtAndPublicG", "privateGAndPublicAt", + "atFirstThenG", "gFirstThenAt"); + + points = InjectionPoint.forInstanceMethodsAndFields(SubSub.class); + assertEquals(points.toString(), 6, points.size()); + // Superclass still has all the injection points it did before.. + assertPoints(points, Super.class, "privateAtAndPublicG", "privateGAndPublicAt", "gInject"); + // Subclass is missing the privateGAndPublicAt because it first became public with + // javax.inject.Inject and was overrode without an annotation, which means it + // disappears. (It was guice @Inject in Super, but it was private there, so it doesn't + // effect the annotations of the subclasses.) + assertPoints(points, Sub.class, "privateAtAndPublicG", "atFirstThenG", "gFirstThenAt"); + } + + /** + * This test serves two purposes: + * 1) It makes sure that the bridge methods javax generates don't stop + * us from injecting superclass methods in the case of javax.inject.Inject. + * This would happen prior to java8 (where javac didn't copy annotations + * from the superclass into the subclass method when it generated the + * bridge methods). + * + * 2) It makes sure that the methods we're going to inject have the correct + * generic types. Java8 copies the annotations from super to subclasses, + * but it doesn't copy the generic type information. Guice would naively + * consider the subclass an injectable method and eject the superclass + * from the 'overrideIndex', leaving only a class with improper generic types. + */ + public void testSyntheticBridgeMethodsInSubclasses() { + Set points; + + points = InjectionPoint.forInstanceMethodsAndFields(RestrictedSuper.class); + assertPointDependencies(points, new TypeLiteral>() {}); + assertEquals(points.toString(), 2, points.size()); + assertPoints(points, RestrictedSuper.class, "jInject", "gInject"); + + points = InjectionPoint.forInstanceMethodsAndFields(ExposedSub.class); + assertPointDependencies(points, new TypeLiteral>() {}); + assertEquals(points.toString(), 2, points.size()); + assertPoints(points, RestrictedSuper.class, "jInject", "gInject"); + } + + private void assertPoints(Iterable points, Class clazz, + String... methodNames) { + Set methods = new HashSet(); + for (InjectionPoint point : points) { + if (point.getDeclaringType().getRawType() == clazz) { + methods.add(point.getMember().getName()); + } + } + assertEquals(points.toString(), ImmutableSet.copyOf(methodNames), methods); + } + + /** Asserts that each injection point has the specified dependencies, in the given order. */ + private void assertPointDependencies(Iterable points, + TypeLiteral... literals) { + for (InjectionPoint point : points) { + assertEquals(literals.length, point.getDependencies().size()); + for (Dependency dep : point.getDependencies()) { + assertEquals(literals[dep.getParameterIndex()], dep.getKey().getTypeLiteral()); + } + } + } + + static class Super { + @javax.inject.Inject public void atInject() {} + @com.google.inject.Inject public void gInject() {} + + @javax.inject.Inject private void privateAtAndPublicG() {} + @com.google.inject.Inject private void privateGAndPublicAt() {} + + @javax.inject.Inject public void atFirstThenG() {} + @com.google.inject.Inject public void gFirstThenAt() {} + } + + static class Sub extends Super { + public void atInject() {} + public void gInject() {} + + @com.google.inject.Inject public void privateAtAndPublicG() {} + @javax.inject.Inject public void privateGAndPublicAt() {} + + @com.google.inject.Inject + @Override + public void atFirstThenG() {} + + @javax.inject.Inject + @Override + public void gFirstThenAt() {} + } + + static class SubSub extends Sub { + @Override public void privateAtAndPublicG() {} + @Override public void privateGAndPublicAt() {} + + @Override public void atFirstThenG() {} + @Override public void gFirstThenAt() {} + } + + static class RestrictedSuper { + @com.google.inject.Inject public void gInject(Provider p) {} + @javax.inject.Inject public void jInject(Provider p) {} + } + + public static class ExposedSub extends RestrictedSuper { + // The subclass may generate bridge/synthetic methods to increase the visibility + // of the superclass methods, since the superclass was package-private but this is public. + } +} diff --git a/src/test/java/com/google/inject/spi/InjectorSpiTest.java b/src/test/java/com/google/inject/spi/InjectorSpiTest.java new file mode 100644 index 0000000..a973fe9 --- /dev/null +++ b/src/test/java/com/google/inject/spi/InjectorSpiTest.java @@ -0,0 +1,74 @@ +package com.google.inject.spi; + +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; + +import junit.framework.TestCase; + +import java.util.Map; + +/** + * @author sberlin@gmail.com (Sam Berlin) + */ +public class InjectorSpiTest extends TestCase { + + public void testExistingBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(Foo.class); + bind(Baz.class); + } + }); + // Sanity check -- ensure we return the proper binding for all existing bindings. + for(Map.Entry, Binding> entry : injector.getAllBindings().entrySet()) { + assertSame(entry.getValue(), injector.getExistingBinding(entry.getKey())); + } + + // Now run through specifics... + Binding binding; + + // 1) non-Provider Foo.class + binding = injector.getExistingBinding(Key.get(Foo.class)); + assertNotNull(binding); + assertEquals(Foo.class, binding.getKey().getTypeLiteral().getRawType()); + + // 2) Provider class (should already exist, because Baz @Injects it). + // the assertTrue is a bit stricter than necessary, but makes sure this works for pre-existing Provider bindings + assertTrue(injector.getAllBindings().containsKey(Key.get(new TypeLiteral>() {}))); + binding = injector.getExistingBinding(Key.get(new TypeLiteral>() {})); + assertNotNull(binding); + assertEquals(Provider.class, binding.getKey().getTypeLiteral().getRawType()); + assertEquals(Foo.class, ((Provider)binding.getProvider().get()).get().getClass()); + + // 3) non-Provider Baz.class + binding = injector.getExistingBinding(Key.get(Baz.class)); + assertNotNull(binding); + assertEquals(Baz.class, binding.getKey().getTypeLiteral().getRawType()); + + // 4) Provider class (should not already exist, because nothing used it yet). + // the assertFalse is a bit stricter than necessary, but makes sure this works for non-pre-existing Provider bindings + assertFalse(injector.getAllBindings().containsKey(Key.get(new TypeLiteral>() {}))); + binding = injector.getExistingBinding(Key.get(new TypeLiteral>() {})); + assertNotNull(binding); + assertEquals(Provider.class, binding.getKey().getTypeLiteral().getRawType()); + assertEquals(Baz.class, ((Provider)binding.getProvider().get()).get().getClass()); + + // 5) non-Provider Bar, doesn't exist. + assertNull(injector.getExistingBinding(Key.get(Bar.class))); + + // 6) Provider Bar, doesn't exist. + assertNull(injector.getExistingBinding(Key.get(new TypeLiteral>() {}))); + } + + private static class Foo {} + private static class Bar {} + private static class Baz { @SuppressWarnings("unused") @Inject Provider fooP; } + +} diff --git a/src/test/java/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java b/src/test/java/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java new file mode 100644 index 0000000..6c797b1 --- /dev/null +++ b/src/test/java/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java @@ -0,0 +1,394 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.CreationException; +import com.google.inject.Exposed; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.PrivateModule; +import com.google.inject.internal.util.StackTraceElements; +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Set; + +/** Tests for {@link ModuleAnnotatedMethodScanner} usage. */ +public class ModuleAnnotatedMethodScannerTest extends TestCase { + + public void testScanning() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() {} + + @TestProvides @Named("foo") String foo() { + return "foo"; + } + + @TestProvides @Named("foo2") String foo2() { + return "foo2"; + } + }; + Injector injector = Guice.createInjector(module, NamedMunger.module()); + + // assert no bindings named "foo" or "foo2" exist -- they were munged. + assertMungedBinding(injector, String.class, "foo", "foo"); + assertMungedBinding(injector, String.class, "foo2", "foo2"); + + Binding fooBinding = injector.getBinding(Key.get(String.class, named("foo-munged"))); + Binding foo2Binding = injector.getBinding(Key.get(String.class, named("foo2-munged"))); + // Validate the provider has a sane toString + assertEquals(methodName(TestProvides.class, "foo", module), + fooBinding.getProvider().toString()); + assertEquals(methodName(TestProvides.class, "foo2", module), + foo2Binding.getProvider().toString()); + } + + public void testSkipSources() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + binder().skipSources(getClass()).install(new AbstractModule() { + @Override protected void configure() {} + + @TestProvides @Named("foo") String foo() { return "foo"; } + }); + } + }; + Injector injector = Guice.createInjector(module, NamedMunger.module()); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testWithSource() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + binder().withSource("source").install(new AbstractModule() { + @Override protected void configure() {} + + @TestProvides @Named("foo") String foo() { return "foo"; } + }); + } + }; + Injector injector = Guice.createInjector(module, NamedMunger.module()); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testMoreThanOneClaimedAnnotationFails() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() {} + + @TestProvides @TestProvides2 String foo() { + return "foo"; + } + }; + try { + Guice.createInjector(module, NamedMunger.module()); + fail(); + } catch(CreationException expected) { + assertEquals(1, expected.getErrorMessages().size()); + assertContains(expected.getMessage(), + "More than one annotation claimed by NamedMunger on method " + + module.getClass().getName() + ".foo(). Methods can only have " + + "one annotation claimed per scanner."); + } + } + + private String methodName(Class annotation, String method, Object container) + throws Exception { + return "@" + annotation.getName() + " " + + StackTraceElements.forMember(container.getClass().getDeclaredMethod(method)); + } + + @Documented @Target(METHOD) @Retention(RUNTIME) + private @interface TestProvides {} + + @Documented @Target(METHOD) @Retention(RUNTIME) + private @interface TestProvides2 {} + + private static class NamedMunger extends ModuleAnnotatedMethodScanner { + static Module module() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(new NamedMunger()); + } + }; + } + + @Override + public String toString() { + return "NamedMunger"; + } + + @Override + public Set> annotationClasses() { + return ImmutableSet.of(TestProvides.class, TestProvides2.class); + } + + @Override + public Key prepareMethod(Binder binder, Annotation annotation, Key key, + InjectionPoint injectionPoint) { + return Key.get(key.getTypeLiteral(), + Names.named(((Named) key.getAnnotation()).value() + "-munged")); + } + } + + private void assertMungedBinding(Injector injector, Class clazz, String originalName, + Object expectedValue) { + assertNull(injector.getExistingBinding(Key.get(clazz, named(originalName)))); + Binding fooBinding = injector.getBinding(Key.get(clazz, named(originalName + "-munged"))); + assertEquals(expectedValue, fooBinding.getProvider().get()); + } + + public void testFailingScanner() { + try { + Guice.createInjector(new SomeModule(), FailingScanner.module()); + fail(); + } catch (CreationException expected) { + Message m = Iterables.getOnlyElement(expected.getErrorMessages()); + assertEquals( + "An exception was caught and reported. Message: Failing in the scanner.", + m.getMessage()); + assertEquals(IllegalStateException.class, m.getCause().getClass()); + ElementSource source = (ElementSource) Iterables.getOnlyElement(m.getSources()); + assertEquals(SomeModule.class.getName(), + Iterables.getOnlyElement(source.getModuleClassNames())); + assertEquals(String.class.getName() + " " + SomeModule.class.getName() + ".aString()", + source.toString()); + } + } + + public static class FailingScanner extends ModuleAnnotatedMethodScanner { + static Module module() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(new FailingScanner()); + } + }; + } + + @Override public Set> annotationClasses() { + return ImmutableSet.of(TestProvides.class); + } + + @Override public Key prepareMethod( + Binder binder, Annotation rawAnnotation, Key key, InjectionPoint injectionPoint) { + throw new IllegalStateException("Failing in the scanner."); + } + } + + static class SomeModule extends AbstractModule { + @TestProvides String aString() { + return "Foo"; + } + + @Override protected void configure() {} + } + + public void testChildInjectorInheritsScanner() { + Injector parent = Guice.createInjector(NamedMunger.module()); + Injector child = parent.createChildInjector(new AbstractModule() { + @Override protected void configure() {} + + @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + assertMungedBinding(child, String.class, "foo", "foo"); + } + + public void testChildInjectorScannersDontImpactSiblings() { + Module module = new AbstractModule() { + @Override + protected void configure() {} + + @TestProvides @Named("foo") String foo() { + return "foo"; + } + }; + Injector parent = Guice.createInjector(); + Injector child = parent.createChildInjector(NamedMunger.module(), module); + assertMungedBinding(child, String.class, "foo", "foo"); + + // no foo nor foo-munged in sibling, since scanner never saw it. + Injector sibling = parent.createChildInjector(module); + assertNull(sibling.getExistingBinding(Key.get(String.class, named("foo")))); + assertNull(sibling.getExistingBinding(Key.get(String.class, named("foo-munged")))); + } + + public void testPrivateModuleInheritScanner_usingPrivateModule() { + Injector injector = Guice.createInjector(NamedMunger.module(), new PrivateModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModule_skipSourcesWithinPrivateModule() { + Injector injector = Guice.createInjector(NamedMunger.module(), new PrivateModule() { + @Override protected void configure() { + binder().skipSources(getClass()).install(new AbstractModule() { + @Override protected void configure() {} + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModule_skipSourcesForPrivateModule() { + Injector injector = Guice.createInjector(NamedMunger.module(), new AbstractModule() { + @Override protected void configure() { + binder().skipSources(getClass()).install(new PrivateModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + }}); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleInheritScanner_usingPrivateBinder() { + Injector injector = Guice.createInjector(NamedMunger.module(), new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleInheritScanner_skipSourcesFromPrivateBinder() { + Injector injector = Guice.createInjector(NamedMunger.module(), new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().skipSources(getClass()).install(new AbstractModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleInheritScanner_skipSourcesFromPrivateBinder2() { + Injector injector = Guice.createInjector(NamedMunger.module(), new AbstractModule() { + @Override protected void configure() { + binder().skipSources(getClass()).newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleScannersDontImpactSiblings_usingPrivateModule() { + Injector injector = Guice.createInjector(new PrivateModule() { + @Override protected void configure() { + install(NamedMunger.module()); + } + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }, new PrivateModule() { + @Override protected void configure() {} + + // ignored! (because the scanner doesn't run over this module) + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleScannersDontImpactSiblings_usingPrivateBinder() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() { + install(NamedMunger.module()); + } + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }, new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() {} + + // ignored! (because the scanner doesn't run over this module) + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + }}); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleWithinPrivateModule() { + Injector injector = Guice.createInjector(NamedMunger.module(), new PrivateModule() { + @Override protected void configure() { + expose(Key.get(String.class, named("foo-munged"))); + install(new PrivateModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } +} diff --git a/src/test/java/com/google/inject/spi/ModuleRewriterTest.java b/src/test/java/com/google/inject/spi/ModuleRewriterTest.java new file mode 100644 index 0000000..06581da --- /dev/null +++ b/src/test/java/com/google/inject/spi/ModuleRewriterTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import com.google.common.collect.Lists; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.name.Names; + +import junit.framework.TestCase; + +import java.util.List; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class ModuleRewriterTest extends TestCase { + + public void testRewriteBindings() { + // create a module the binds String.class and CharSequence.class + Module module = new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("Pizza"); + bind(CharSequence.class).toInstance("Wine"); + } + }; + + // record the elements from that module + List elements = Elements.getElements(module); + + // create a rewriter that rewrites the binding to 'Wine' with a binding to 'Beer' + List rewritten = Lists.newArrayList(); + for (Element element : elements) { + element = element.acceptVisitor(new DefaultElementVisitor() { + @Override public Element visit(Binding binding) { + T target = binding.acceptTargetVisitor(Elements.getInstanceVisitor()); + if ("Wine".equals(target)) { + return null; + } + else { + return binding; + } + } + }); + if (element != null) { + rewritten.add(element); + } + } + + // create a module from the original list of elements and the rewriter + Module rewrittenModule = Elements.getModule(rewritten); + + // the wine binding is dropped + Injector injector = Guice.createInjector(rewrittenModule); + try { + injector.getInstance(CharSequence.class); + fail(); + } catch (ConfigurationException expected) { + } + } + + public void testGetProviderAvailableAtInjectMembersTime() { + Module module = new AbstractModule() { + public void configure() { + final Provider stringProvider = getProvider(String.class); + + bind(String.class).annotatedWith(Names.named("2")).toProvider(new Provider() { + private String value; + + @Inject void initialize() { + value = stringProvider.get(); + } + + public String get() { + return value; + } + }); + + bind(String.class).toInstance("A"); + } + }; + + // the module works fine normally + Injector injector = Guice.createInjector(module); + assertEquals("A", injector.getInstance(Key.get(String.class, Names.named("2")))); + + // and it should also work fine if we rewrite it + List elements = Elements.getElements(module); + Module replayed = Elements.getModule(elements); + Injector replayedInjector = Guice.createInjector(replayed); + assertEquals("A", replayedInjector.getInstance(Key.get(String.class, Names.named("2")))); + } +} diff --git a/src/test/java/com/google/inject/spi/ModuleSourceTest.java b/src/test/java/com/google/inject/spi/ModuleSourceTest.java new file mode 100644 index 0000000..b72d976 --- /dev/null +++ b/src/test/java/com/google/inject/spi/ModuleSourceTest.java @@ -0,0 +1,125 @@ +package com.google.inject.spi; + +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Module; + +import junit.framework.TestCase; + +/** + * Tests for {@link ModuleSource}. + */ +public class ModuleSourceTest extends TestCase { + + private static final StackTraceElement BINDER_INSTALL = + new StackTraceElement("com.google.inject.spi.Elements$RecordingBinder", "install", + "Unknown Source", 235 /* line number*/); + + public void testOneModule() { + ModuleSource moduleSource = createWithSizeOne(); + checkSizeOne(moduleSource); + } + + public void testTwoModules() { + ModuleSource moduleSource = createWithSizeTwo(); + checkSizeTwo(moduleSource); + moduleSource = moduleSource.getParent(); + checkSizeOne(moduleSource); + } + + public void testThreeModules() { + ModuleSource moduleSource = createWithSizeThree(); + checkSizeThree(moduleSource); + moduleSource = moduleSource.getParent(); + checkSizeTwo(moduleSource); + moduleSource = moduleSource.getParent(); + checkSizeOne(moduleSource); + } + + private void checkSizeOne(ModuleSource moduleSource) { + assertEquals(1, moduleSource.size()); + assertEquals(1, moduleSource.getStackTraceSize()); + // Check call stack + StackTraceElement[] callStack = moduleSource.getStackTrace(); + assertEquals(BINDER_INSTALL, callStack[0]); + } + + private void checkSizeTwo(ModuleSource moduleSource) { + assertEquals(2, moduleSource.size()); + assertEquals(3, moduleSource.getStackTraceSize()); + // Check call stack + StackTraceElement[] callStack = moduleSource.getStackTrace(); + assertEquals(BINDER_INSTALL, callStack[0]); + assertEquals( + new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$A", "configure", "Unknown Source", 100), + callStack[1]); + assertEquals(BINDER_INSTALL, callStack[2]); + } + + private void checkSizeThree(ModuleSource moduleSource) { + assertEquals(3, moduleSource.size()); + assertEquals(7, moduleSource.getStackTraceSize()); + // Check call stack + StackTraceElement[] callStack = moduleSource.getStackTrace(); + assertEquals(BINDER_INSTALL, callStack[0]); + assertEquals(new StackTraceElement("class1", "method1", "Unknown Source", 1), callStack[1]); + assertEquals(new StackTraceElement("class2", "method2", "Unknown Source", 2), callStack[2]); + assertEquals( + new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$B", "configure", "Unknown Source", 200), + callStack[3]); + assertEquals(BINDER_INSTALL, callStack[4]); + + assertEquals( + new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$A", "configure", "Unknown Source", 100), + callStack[5]); + assertEquals(BINDER_INSTALL, callStack[6]); + } + + private ModuleSource createWithSizeOne() { + StackTraceElement[] partialCallStack = new StackTraceElement[1]; + partialCallStack[0] = BINDER_INSTALL; + return new ModuleSource(new A(), partialCallStack); + } + + private ModuleSource createWithSizeTwo() { + ModuleSource moduleSource = createWithSizeOne(); + StackTraceElement[] partialCallStack = new StackTraceElement[2]; + partialCallStack[0] = BINDER_INSTALL; + partialCallStack[1] = new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$A", "configure", "moduleSourceTest.java", 100); + return moduleSource.createChild(new B(), partialCallStack); + } + + private ModuleSource createWithSizeThree() { + ModuleSource moduleSource = createWithSizeTwo(); + StackTraceElement[] partialCallStack = new StackTraceElement[4]; + partialCallStack[0] = BINDER_INSTALL; + partialCallStack[1] = new StackTraceElement("class1", "method1", "Class1.java", 1); + partialCallStack[2] = new StackTraceElement("class2", "method2", "Class2.java", 2); + partialCallStack[3] = new StackTraceElement( + "com.google.inject.spi.moduleSourceTest$B", "configure", "moduleSourceTest.java", 200); + return moduleSource.createChild(new C(), partialCallStack); + } + + private static class A extends AbstractModule { + @Override + public void configure() { + install(new B()); + } + } + + private static class B implements Module { + @Override + public void configure(Binder binder) { + binder.install(new C()); + } + } + + private static class C extends AbstractModule { + @Override + public void configure() {} + } +} diff --git a/src/test/java/com/google/inject/spi/ProviderMethodsTest.java b/src/test/java/com/google/inject/spi/ProviderMethodsTest.java new file mode 100644 index 0000000..1265990 --- /dev/null +++ b/src/test/java/com/google/inject/spi/ProviderMethodsTest.java @@ -0,0 +1,905 @@ +/** + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import static com.google.inject.Asserts.assertContains; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.Errors; +import com.google.inject.internal.InternalFlags; +import com.google.inject.internal.ProviderMethod; +import com.google.inject.internal.ProviderMethodsModule; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.util.Providers; +import com.google.inject.util.Types; + +import junit.framework.TestCase; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * @author crazybob@google.com (Bob Lee) + */ +public class ProviderMethodsTest extends TestCase implements Module { + + @SuppressWarnings("unchecked") + public void testProviderMethods() { + Injector injector = Guice.createInjector(this); + + Bob bob = injector.getInstance(Bob.class); + assertEquals("A Bob", bob.getName()); + + Bob clone = injector.getInstance(Bob.class); + assertEquals("A Bob", clone.getName()); + + assertNotSame(bob, clone); + assertSame(bob.getDaughter(), clone.getDaughter()); + + Key soleBobKey = Key.get(Bob.class, Sole.class); + assertSame( + injector.getInstance(soleBobKey), + injector.getInstance(soleBobKey) + ); + } + + public void configure(Binder binder) {} + + interface Bob { + String getName(); + Dagny getDaughter(); + } + + interface Dagny { + int getAge(); + } + + @Provides + Bob provideBob(final Dagny dagny) { + return new Bob() { + public String getName() { + return "A Bob"; + } + + public Dagny getDaughter() { + return dagny; + } + }; + } + + @Provides + @Singleton + @Sole + Bob provideSoleBob(final Dagny dagny) { + return new Bob() { + public String getName() { + return "Only Bob"; + } + + public Dagny getDaughter() { + return dagny; + } + }; + } + + @Provides + @Singleton + Dagny provideDagny() { + return new Dagny() { + public int getAge() { + return 1; + } + }; + } + + @Retention(RUNTIME) + @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) + @BindingAnnotation + @interface Sole {} + + + +// We'll have to make getProvider() support circular dependencies before this +// will work. +// +// public void testCircularDependency() { +// Injector injector = Guice.createInjector(new Module() { +// public void configure(Binder binder) { +// binder.install(ProviderMethods.from(ProviderMethodsTest.this)); +// } +// }); +// +// Foo foo = injector.getInstance(Foo.class); +// assertEquals(5, foo.getI()); +// assertEquals(10, foo.getBar().getI()); +// assertEquals(5, foo.getBar().getFoo().getI()); +// } +// +// interface Foo { +// Bar getBar(); +// int getI(); +// } +// +// interface Bar { +// Foo getFoo(); +// int getI(); +// } +// +// @Provides Foo newFoo(final Bar bar) { +// return new Foo() { +// +// public Bar getBar() { +// return bar; +// } +// +// public int getI() { +// return 5; +// } +// }; +// } +// +// @Provides Bar newBar(final Foo foo) { +// return new Bar() { +// +// public Foo getFoo() { +// return foo; +// } +// +// public int getI() { +// return 10; +// } +// }; +// } + + + public void testMultipleBindingAnnotations() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() {} + + @Provides @Named("A") @Blue + public String provideString() { + return "a"; + } + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "more than one annotation annotated with @BindingAnnotation:", + "Named", + "Blue", + "at " + getClass().getName(), + ".provideString("); + } + + } + + @Retention(RUNTIME) + @BindingAnnotation @interface Blue {} + + public void testGenericProviderMethods() { + Injector injector = Guice.createInjector( + new ProvideTs("A", "B") {}, new ProvideTs(1, 2) {}); + + assertEquals("A", injector.getInstance(Key.get(String.class, Names.named("First")))); + assertEquals("B", injector.getInstance(Key.get(String.class, Names.named("Second")))); + assertEquals(ImmutableSet.of("A", "B"), + injector.getInstance(Key.get(Types.setOf(String.class)))); + + assertEquals(1, injector.getInstance(Key.get(Integer.class, Names.named("First"))).intValue()); + assertEquals(2, injector.getInstance(Key.get(Integer.class, Names.named("Second"))).intValue()); + assertEquals(ImmutableSet.of(1, 2), + injector.getInstance(Key.get(Types.setOf(Integer.class)))); + } + + abstract class ProvideTs extends AbstractModule { + final T first; + final T second; + + protected ProvideTs(T first, T second) { + this.first = first; + this.second = second; + } + + @Override protected void configure() {} + + @Named("First") @Provides T provideFirst() { + return first; + } + + @Named("Second") @Provides T provideSecond() { + return second; + } + + @Provides Set provideBoth(@Named("First") T first, @Named("Second") T second) { + return ImmutableSet.of(first, second); + } + } + + public void testAutomaticProviderMethods() { + Injector injector = Guice.createInjector((Module) new AbstractModule() { + @Override protected void configure() { } + private int next = 1; + + @Provides @Named("count") + public Integer provideCount() { + return next++; + } + }); + + assertEquals(1, injector.getInstance(Key.get(Integer.class, Names.named("count"))).intValue()); + assertEquals(2, injector.getInstance(Key.get(Integer.class, Names.named("count"))).intValue()); + assertEquals(3, injector.getInstance(Key.get(Integer.class, Names.named("count"))).intValue()); + } + + /** + * If the user installs provider methods for the module manually, that shouldn't cause a double + * binding of the provider methods' types. + */ + public void testAutomaticProviderMethodsDoNotCauseDoubleBinding() { + Module installsSelf = new AbstractModule() { + @Override protected void configure() { + install(this); + bind(Integer.class).toInstance(5); + } + @Provides public String provideString(Integer count) { + return "A" + count; + } + }; + + Injector injector = Guice.createInjector(installsSelf); + assertEquals("A5", injector.getInstance(String.class)); + } + + public void testWildcardProviderMethods() { + final List strings = ImmutableList.of("A", "B", "C"); + final List numbers = ImmutableList.of(1, 2, 3); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + @SuppressWarnings("unchecked") + Key> listOfSupertypesOfInteger = (Key>) + Key.get(Types.listOf(Types.supertypeOf(Integer.class))); + bind(listOfSupertypesOfInteger).toInstance(numbers); + } + @Provides public List provideCharSequences() { + return strings; + } + @Provides public Class provideType() { + return Float.class; + } + }); + + assertSame(strings, injector.getInstance(HasWildcardInjection.class).charSequences); + assertSame(numbers, injector.getInstance(HasWildcardInjection.class).numbers); + assertSame(Float.class, injector.getInstance(HasWildcardInjection.class).type); + } + + static class HasWildcardInjection { + @Inject List charSequences; + @Inject List numbers; + @Inject Class type; + } + + public void testProviderMethodDependenciesAreExposed() throws Exception { + Module module = new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(50); + bindConstant().annotatedWith(Names.named("units")).to("Kg"); + } + @Provides @Named("weight") String provideWeight(Integer count, @Named("units") String units) { + return count + units; + } + }; + Injector injector = Guice.createInjector(module); + + ProviderInstanceBinding binding = (ProviderInstanceBinding) injector.getBinding( + Key.get(String.class, Names.named("weight"))); + Method method = + module.getClass().getDeclaredMethod("provideWeight", Integer.class, String.class); + InjectionPoint point = new InjectionPoint(TypeLiteral.get(module.getClass()), method, false); + assertEquals(ImmutableSet.>of( + new Dependency(point, Key.get(Integer.class), false, 0), + new Dependency(point, Key.get(String.class, Names.named("units")), false, 1)), + binding.getDependencies()); + } + + public void testNonModuleProviderMethods() { + final Object methodsObject = new Object() { + @Provides @Named("foo") String provideFoo() { + return "foo-value"; + } + }; + + Module module = new AbstractModule() { + @Override protected void configure() { + install(ProviderMethodsModule.forObject(methodsObject)); + } + }; + + Injector injector = Guice.createInjector(module); + + Key key = Key.get(String.class, Names.named("foo")); + assertEquals("foo-value", injector.getInstance(key)); + + // Test the provider method object itself. This makes sure getInstance works, since GIN uses it + List elements = Elements.getElements(module); + assertEquals(1, elements.size()); + + Element element = elements.get(0); + assertTrue(element + " instanceof ProviderInstanceBinding", + element instanceof ProviderInstanceBinding); + + ProviderInstanceBinding binding = (ProviderInstanceBinding) element; + javax.inject.Provider provider = binding.getUserSuppliedProvider(); + assertTrue(provider instanceof ProviderMethod); + assertEquals(methodsObject, ((ProviderMethod) provider).getInstance()); + } + + public void testVoidProviderMethods() { + try { + Guice.createInjector(new AbstractModule() { + @Override protected void configure() {} + + @Provides void provideFoo() {} + }); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "1) Provider methods must return a value. Do not return void.", + getClass().getName(), + ".provideFoo("); + } + } + + /*public void testInjectsJustOneLogger() { + AtomicReference loggerRef = new AtomicReference(); + Injector injector = Guice.createInjector(new FooModule(loggerRef)); + + assertNull(loggerRef.get()); + injector.getInstance(Integer.class); + Logger lastLogger = loggerRef.getAndSet(null); + assertNotNull(lastLogger); + injector.getInstance(Integer.class); + assertSame(lastLogger, loggerRef.get()); + + assertEquals(FooModule.class.getName(), lastLogger.getName()); + } + + private static class FooModule extends AbstractModule { + private final AtomicReference loggerRef; + + public FooModule(AtomicReference loggerRef) { + this.loggerRef = loggerRef; + } + + @Override protected void configure() {} + + @SuppressWarnings("unused") + @Provides Integer foo(Logger logger) { + loggerRef.set(logger); + return 42; + } + }*/ + + public void testSpi() throws Exception { + Module m1 = new AbstractModule() { + @Override protected void configure() {} + @Provides @Named("foo") String provideFoo(Integer dep) { return "foo"; } + }; + Module m2 = new AbstractModule() { + @Override protected void configure() {} + @Provides Integer provideInt(@Named("foo") String dep) { return 42; } + }; + Injector injector = Guice.createInjector(m1, m2); + + Binding stringBinding = + injector.getBinding(Key.get(String.class, Names.named("foo"))); + ProvidesMethodBinding stringMethod = + stringBinding.acceptTargetVisitor(new BindingCapturer()); + assertEquals(m1, stringMethod.getEnclosingInstance()); + assertEquals(m1.getClass().getDeclaredMethod("provideFoo", Integer.class), + stringMethod.getMethod()); + assertEquals(((HasDependencies) stringBinding).getDependencies(), + stringMethod.getDependencies()); + assertEquals(Key.get(String.class, Names.named("foo")), stringMethod.getKey()); + + Binding intBinding = injector.getBinding(Integer.class); + ProvidesMethodBinding intMethod = + intBinding.acceptTargetVisitor(new BindingCapturer()); + assertEquals(m2, intMethod.getEnclosingInstance()); + assertEquals(m2.getClass().getDeclaredMethod("provideInt", String.class), + intMethod.getMethod()); + assertEquals(((HasDependencies) intBinding).getDependencies(), + intMethod.getDependencies()); + assertEquals(Key.get(Integer.class), intMethod.getKey()); + + } + + private static class BindingCapturer extends DefaultBindingTargetVisitor> + implements ProvidesMethodTargetVisitor> { + + @SuppressWarnings("unchecked") + public ProvidesMethodBinding visit( + ProvidesMethodBinding providesMethodBinding) { + return (ProvidesMethodBinding)providesMethodBinding; + } + + @Override protected ProvidesMethodBinding visitOther(Binding binding) { + throw new IllegalStateException("unexpected visit of: " + binding); + } + } + + public void testProvidesMethodVisibility() { + Injector injector = Guice.createInjector(new VisibilityModule()); + + assertEquals(42, injector.getInstance(Integer.class).intValue()); + assertEquals(42L, injector.getInstance(Long.class).longValue()); + assertEquals(42D, injector.getInstance(Double.class).doubleValue()); + assertEquals(42F, injector.getInstance(Float.class).floatValue()); + } + + private static class VisibilityModule extends AbstractModule { + @Override protected void configure() {} + + @SuppressWarnings("unused") + @Provides Integer foo() { + return 42; + } + + @SuppressWarnings("unused") + @Provides private Long bar() { + return 42L; + } + + @SuppressWarnings("unused") + @Provides protected Double baz() { + return 42D; + } + + @SuppressWarnings("unused") + @Provides public Float quux() { + return 42F; + } + } + + public void testProvidesMethodInheritenceHierarchy() { + try { + Guice.createInjector(new Sub1Module(), new Sub2Module()); + fail("Expected injector creation failure"); + } catch (CreationException expected) { + // both of our super class bindings cause errors + assertContains(expected.getMessage(), + "A binding to java.lang.Long was already configured", + "A binding to java.lang.Integer was already configured"); + } + } + + public void testProvidesMethodsDefinedInSuperClass() { + Injector injector = Guice.createInjector(new Sub1Module()); + assertEquals(42, injector.getInstance(Integer.class).intValue()); + assertEquals(42L, injector.getInstance(Long.class).longValue()); + assertEquals(42D, injector.getInstance(Double.class).doubleValue()); + } + + private static class BaseModule extends AbstractModule { + @Override protected void configure() {} + + @Provides Integer foo() { + return 42; + } + + @Provides Long bar() { + return 42L; + } + } + + private static class Sub1Module extends BaseModule { + @Provides Double baz() { + return 42D; + } + } + + private static class Sub2Module extends BaseModule { + @Provides Float quux() { + return 42F; + } + } + + static class SuperClassModule extends AbstractModule { + @Override protected void configure() {} + @Provides Number providerMethod() { + return 1D; + } + @Provides @Named("rawlist") List rawProvider(@Named("list") List f) { + return f; + } + + @Provides @Named("unrawlist") List rawParameterProvider(@Named("rawlist") List f) { + return f; + } + + @Provides @Named("list") List annotatedGenericProviderMethod() { + return new ArrayList(); + } + @Provides @Named("collection") Collection annotatedGenericParameterProviderMethod( + @Named("list") List foo) { + return foo; + } + @Provides private String privateProviderMethod() { + return "hello"; + } + } + + public void testOverrideProviderMethod_overrideHasProvides() { + class SubClassModule extends SuperClassModule { + @Override @Provides Number providerMethod() { + return 2D; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".providerMethod()", + "overridden by: " + SubClassModule.class.getName() + ".providerMethod()"); + } + } + + public void testOverrideProviderMethod_overrideHasProvides_withNewAnnotation() { + class SubClassModule extends SuperClassModule { + @Override @Provides @Named("foo") Number providerMethod() { + return 2D; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".providerMethod()", + "overridden by: " + SubClassModule.class.getName() + ".providerMethod()"); + } + } + + public void testOverrideProviderMethod_overrideDoesntHaveProvides() { + class SubClassModule extends SuperClassModule { + @Override Number providerMethod() { + return 2D; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".providerMethod()", + "overridden by: " + SubClassModule.class.getName() + ".providerMethod()"); + } + } + public void testOverrideProviderMethod_overrideDoesntHaveProvides_withNewAnnotation() { + class SubClassModule extends SuperClassModule { + @Override @Named("foo") Number providerMethod() { + return 2D; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".providerMethod()", + "overridden by: " + SubClassModule.class.getName() + ".providerMethod()"); + } + } + + + public void testOverrideProviderMethod_covariantOverrideDoesntHaveProvides() { + class SubClassModule extends SuperClassModule { + @Override Double providerMethod() { + return 2D; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".providerMethod()", + "overridden by: " + SubClassModule.class.getName() + ".providerMethod()"); + } + } + + public void testOverrideProviderMethod_covariantOverrideHasProvides() { + class SubClassModule extends SuperClassModule { + @Override @Provides Double providerMethod() { + return 2D; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".providerMethod()", + "overridden by: " + SubClassModule.class.getName() + ".providerMethod()"); + } + } + + public void testOverrideProviderMethod_fakeOverridePrivateMethod() { + class SubClassModule extends SuperClassModule { + // not actually an override, just looks like it + String privateProviderMethod() { + return "sub"; + } + } + assertEquals("hello", Guice.createInjector(new SubClassModule()).getInstance(String.class)); + } + + public void testOverrideProviderMethod_subclassRawTypes_returnType() { + class SubClassModule extends SuperClassModule { + @Override List annotatedGenericProviderMethod() { + return super.annotatedGenericProviderMethod(); + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + + ".annotatedGenericProviderMethod()", + "overridden by: " + SubClassModule.class.getName() + ".annotatedGenericProviderMethod()"); + } + } + + public void testOverrideProviderMethod_subclassRawTypes_parameterType() { + class SubClassModule extends SuperClassModule { + @Override Collection annotatedGenericParameterProviderMethod(List foo) { + return super.annotatedGenericParameterProviderMethod(foo); + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + + ".annotatedGenericParameterProviderMethod()", + "overridden by: " + SubClassModule.class.getName() + + ".annotatedGenericParameterProviderMethod()"); + } + } + + public void testOverrideProviderMethod_superclassRawTypes_returnType() { + class SubClassModule extends SuperClassModule { + // remove the rawtype from the override + @Override List rawProvider(List f) { + return f; + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + SuperClassModule.class.getName() + ".rawProvider()", + "overridden by: " + SubClassModule.class.getName() + ".rawProvider()"); + } + } + + abstract static class GenericSuperModule extends AbstractModule { + @Provides String provide(T thing) { + return thing.toString(); + } + } + + // This is a tricky case where signatures don't match, but it is an override (facilitated via a + // bridge method) + public void testOverrideProviderMethod_erasureBasedOverrides() { + class SubClassModule extends GenericSuperModule { + @Override String provide(Integer thing) { + return thing.toString(); + } + + @Override protected void configure() { + bind(Integer.class).toInstance(3); + } + } + try { + Guice.createInjector(new SubClassModule()); + fail(); + } catch (CreationException e) { + assertContains(e.getMessage(), + "Overriding @Provides methods is not allowed.", + "@Provides method: " + GenericSuperModule.class.getName() + ".provide()", + "overridden by: " + SubClassModule.class.getName() + ".provide()"); + } + } + + class RestrictedSuper extends AbstractModule { + @Provides public String provideFoo() { return "foo"; } + @Override protected void configure() {} + } + + public class ExposedSub extends RestrictedSuper {} + + public void testOverrideProviderMethod_increasedVisibility() { + // ensure we don't detect the synthetic provideFoo method in ExposedSub as an override (it is, + // but since it is synthetic it would be annoying to throw an error on it). + assertEquals("foo", Guice.createInjector(new ExposedSub()).getInstance(String.class)); + } + + interface ProviderInterface { + T getT(); + } + + static class ModuleImpl extends AbstractModule implements ProviderInterface { + @Override protected void configure() {} + @Provides public String getT() { + return "string"; + } + @Provides public Object getObject() { + return new Object(); + } + /* javac will synthesize a bridge method for getT with the types erased, equivalent to: + * @Provides public Object getT() { ... } + */ + } + + public void testIgnoreSyntheticBridgeMethods() { + Guice.createInjector(new ModuleImpl()); + } + + public void testNullability() throws Exception { + Module module = new AbstractModule() { + @Override + protected void configure() { + bind(String.class).toProvider(Providers.of(null)); + } + + @SuppressWarnings("unused") + @Provides + Integer fail(String foo) { + return 1; + } + + @SuppressWarnings("unused") + @Provides + Long succeed(@Nullable String foo) { + return 2L; + } + }; + Injector injector = Guice.createInjector(module); + InjectionPoint fooPoint = InjectionPoint.forMethod( + module.getClass().getDeclaredMethod("fail", String.class), + TypeLiteral.get(module.getClass())); + Dependency fooDependency = Iterables.getOnlyElement(fooPoint.getDependencies()); + + runNullableTest(injector, fooDependency, module); + + injector.getInstance(Long.class); + } + + private void runNullableTest(Injector injector, Dependency dependency, Module module) { + switch (InternalFlags.getNullableProvidesOption()) { + case ERROR: + validateNullableFails(injector, module); + break; + case IGNORE: + validateNullableIgnored(injector); + break; + case WARN: + validateNullableWarns(injector, dependency); + break; + } + } + + private void validateNullableFails(Injector injector, Module module) { + try { + injector.getInstance(Integer.class); + fail(); + } catch (ProvisionException expected) { + assertContains(expected.getMessage(), + "1) null returned by binding at " + module.getClass().getName() + ".configure(", + "but parameter 0 of " + module.getClass().getName() + ".fail() is not @Nullable", + "while locating java.lang.String", + "for parameter 0 at " + module.getClass().getName() + ".fail(", + "while locating java.lang.Integer"); + + assertEquals(1, expected.getErrorMessages().size()); + } + } + + private void validateNullableIgnored(Injector injector) { + injector.getInstance(Integer.class); // no exception + } + + private void validateNullableWarns(Injector injector, Dependency dependency) { + /*final List logRecords = Lists.newArrayList(); + final Handler fakeHandler = new Handler() { + @Override + public void publish(LogRecord logRecord) { + logRecords.add(logRecord); + } + @Override + public void flush() {} + @Override + public void close() throws SecurityException {} + }; + //Logger.getLogger(Guice.class.getName()).addHandler(fakeHandler); + try { + injector.getInstance(Integer.class); // no exception, but assert it does log. + LogRecord record = Iterables.getOnlyElement(logRecords); + assertEquals( + "Guice injected null into parameter {0} of {1} (a {2}), please mark it @Nullable." + + " Use -Dguice_check_nullable_provides_params=ERROR to turn this into an" + + " error.", + record.getMessage()); + assertEquals(dependency.getParameterIndex(), record.getParameters()[0]); + assertEquals(Errors.convert(dependency.getInjectionPoint().getMember()), + record.getParameters()[1]); + assertEquals(Errors.convert(dependency.getKey()), record.getParameters()[2]); + } finally { + //Logger.getLogger(Guice.class.getName()).removeHandler(fakeHandler); + } + */ + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Nullable {} +} diff --git a/src/test/java/com/google/inject/spi/SpiBindingsTest.java b/src/test/java/com/google/inject/spi/SpiBindingsTest.java new file mode 100644 index 0000000..82e969e --- /dev/null +++ b/src/test/java/com/google/inject/spi/SpiBindingsTest.java @@ -0,0 +1,463 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.spi; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.getDeclaringSourcePart; +import static com.google.inject.Asserts.isIncludeStackTraceComplete; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import com.google.inject.name.Names; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class SpiBindingsTest extends TestCase { + + public void testBindConstant() { + checkInjector( + new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(Names.named("one")).to(1); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof InstanceBinding); + assertEquals(Key.get(Integer.class, Names.named("one")), binding.getKey()); + return null; + } + } + ); + } + + public void testToInstanceBinding() { + checkInjector( + new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("A"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof InstanceBinding); + checkBindingSource(binding); + assertEquals(Key.get(String.class), binding.getKey()); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(InstanceBinding binding) { + assertEquals("A", binding.getInstance()); + return null; + } + }); + binding.acceptScopingVisitor(new FailingBindingScopingVisitor() { + public Void visitEagerSingleton() { + return null; + } + }); + return null; + } + } + ); + } + + public void testToProviderBinding() { + final Provider stringProvider = new StringProvider(); + + checkInjector( + new AbstractModule() { + protected void configure() { + bind(String.class).toProvider(stringProvider); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof ProviderInstanceBinding); + checkBindingSource(binding); + assertEquals(Key.get(String.class), binding.getKey()); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit( + ProviderInstanceBinding binding) { + assertSame(stringProvider, binding.getUserSuppliedProvider()); + return null; + } + }); + return null; + } + } + ); + } + + public void testToProviderKeyBinding() { + checkInjector( + new AbstractModule() { + protected void configure() { + bind(String.class).toProvider(StringProvider.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof ProviderKeyBinding); + checkBindingSource(binding); + assertEquals(Key.get(String.class), binding.getKey()); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ProviderKeyBinding binding) { + assertEquals(Key.get(StringProvider.class), binding.getProviderKey()); + return null; + } + }); + return null; + } + } + ); + } + + public void testToKeyBinding() { + final Key aKey = Key.get(String.class, Names.named("a")); + final Key bKey = Key.get(String.class, Names.named("b")); + + checkInjector( + new AbstractModule() { + protected void configure() { + bind(aKey).to(bKey); + bind(bKey).toInstance("B"); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof LinkedKeyBinding); + checkBindingSource(binding); + assertEquals(aKey, binding.getKey()); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(LinkedKeyBinding binding) { + assertEquals(bKey, binding.getLinkedKey()); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertEquals(bKey, binding.getKey()); + return null; + } + } + ); + } + + public void testToConstructorBinding() { + checkInjector( + new AbstractModule() { + protected void configure() { + bind(D.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof ConstructorBinding); + checkBindingSource(binding); + assertEquals(Key.get(D.class), binding.getKey()); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(ConstructorBinding binding) { + Constructor expected = D.class.getDeclaredConstructors()[0]; + assertEquals(expected, binding.getConstructor().getMember()); + assertEquals(ImmutableSet.of(), binding.getInjectableMembers()); + return null; + } + }); + return null; + } + } + ); + } + + public void testConstantBinding() { + checkInjector( + new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(Names.named("one")).to(1); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding binding) { + assertTrue(binding instanceof InstanceBinding); + checkBindingSource(binding); + assertEquals(Key.get(Integer.class, Names.named("one")), binding.getKey()); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit(InstanceBinding binding) { + assertEquals(1, binding.getInstance()); + return null; + } + }); + return null; + } + } + ); + } + + public void testConvertedConstantBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bindConstant().annotatedWith(Names.named("one")).to("1"); + } + }); + + Binding binding = injector.getBinding(Key.get(Integer.class, Names.named("one"))); + assertEquals(Key.get(Integer.class, Names.named("one")), binding.getKey()); + checkBindingSource(binding); + assertTrue(binding instanceof ConvertedConstantBinding); + binding.acceptTargetVisitor(new FailingTargetVisitor() { + @Override public Void visit( + ConvertedConstantBinding binding) { + assertEquals((Integer) 1, binding.getValue()); + assertEquals(Key.get(String.class, Names.named("one")), binding.getSourceKey()); + return null; + } + }); + } + + public void testProviderBinding() { + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toInstance("A"); + } + }); + + Key> providerOfStringKey = new Key>() {}; + Binding> binding = injector.getBinding(providerOfStringKey); + assertEquals(providerOfStringKey, binding.getKey()); + checkBindingSource(binding); + assertTrue(binding instanceof ProviderBinding); + binding.acceptTargetVisitor(new FailingTargetVisitor>() { + @Override public Void visit( + ProviderBinding> binding) { + assertEquals(Key.get(String.class), binding.getProvidedKey()); + return null; + } + }); + } + + public void testScopes() { + checkInjector( + new AbstractModule() { + protected void configure() { + bind(String.class).annotatedWith(Names.named("a")) + .toProvider(StringProvider.class).in(Singleton.class); + bind(String.class).annotatedWith(Names.named("b")) + .toProvider(StringProvider.class).in(Scopes.SINGLETON); + bind(String.class).annotatedWith(Names.named("c")) + .toProvider(StringProvider.class).asEagerSingleton(); + bind(String.class).annotatedWith(Names.named("d")) + .toProvider(StringProvider.class); + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class, Names.named("a")), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitScope(Scope scope) { + // even though we bound with an annotation, the injector always uses instances + assertSame(Scopes.SINGLETON, scope); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class, Names.named("b")), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitScope(Scope scope) { + assertSame(Scopes.SINGLETON, scope); + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class, Names.named("c")), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitEagerSingleton() { + return null; + } + }); + return null; + } + }, + + new FailingElementVisitor() { + @Override public Void visit(Binding command) { + assertEquals(Key.get(String.class, Names.named("d")), command.getKey()); + command.acceptScopingVisitor(new FailingBindingScopingVisitor() { + @Override public Void visitNoScoping() { + return null; + } + }); + return null; + } + } + ); + } + + public void testExtensionSpi() { + final AtomicBoolean visiting = new AtomicBoolean(false); + + final Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + bind(String.class).toProvider(new ProviderWithExtensionVisitor() { + public V acceptExtensionVisitor(BindingTargetVisitor visitor, + ProviderInstanceBinding binding) { + assertSame(this, binding.getUserSuppliedProvider()); + // We can't always check for FailingSpiTargetVisitor, + // because constructing the injector visits here, and we need + // to process the binding as normal + if(visiting.get()) { + assertTrue("visitor: " + visitor, visitor instanceof FailingSpiTargetVisitor); + return (V)"visited"; + } else { + return visitor.visit(binding); + } + } + + public String get() { + return "FooBar"; + } + }); + } + }); + + visiting.set(true); + + // Check for Provider binding -- that is still a ProviderBinding. + Key> providerOfStringKey = new Key>() {}; + Binding> providerBinding = injector.getBinding(providerOfStringKey); + assertEquals(providerOfStringKey, providerBinding.getKey()); + checkBindingSource(providerBinding); + assertTrue("binding: " + providerBinding, providerBinding instanceof ProviderBinding); + providerBinding.acceptTargetVisitor(new FailingTargetVisitor>() { + @Override public Void visit(ProviderBinding> binding) { + assertEquals(Key.get(String.class), binding.getProvidedKey()); + return null; + } + }); + + // Check for String binding -- that one is ProviderInstanceBinding, and gets hooked + Binding binding = injector.getBinding(String.class); + assertEquals(Key.get(String.class), binding.getKey()); + checkBindingSource(binding); + assertTrue(binding instanceof ProviderInstanceBinding); + assertEquals("visited", binding.acceptTargetVisitor(new FailingSpiTargetVisitor())); + } + + private static class FailingSpiTargetVisitor extends DefaultBindingTargetVisitor { + @Override + protected String visitOther(Binding binding) { + throw new AssertionFailedError(); + } + } + + public void checkBindingSource(Binding binding) { + assertContains(binding.getSource().toString(), getDeclaringSourcePart(getClass())); + ElementSource source = (ElementSource) binding.getSource(); + assertFalse(source.getModuleClassNames().isEmpty()); + if (isIncludeStackTraceComplete()) { + assertTrue(source.getStackTrace().length > 0); + } else { + assertEquals(0, source.getStackTrace().length); + } + } + + public void checkInjector(Module module, ElementVisitor... visitors) { + Injector injector = Guice.createInjector(module); + + List> bindings = Lists.newArrayList(injector.getBindings().values()); + for (Iterator> i = bindings.iterator(); i.hasNext(); ) { + if (BUILT_IN_BINDINGS.contains(i.next().getKey())) { + i.remove(); + } + } + + Collections.sort(bindings, orderByKey); + + assertEquals(bindings.size(), visitors.length); + + for (int i = 0; i < visitors.length; i++) { + ElementVisitor visitor = visitors[i]; + Binding binding = bindings.get(i); + binding.acceptVisitor(visitor); + } + } + + private final ImmutableSet> BUILT_IN_BINDINGS = ImmutableSet.of( + Key.get(Injector.class), Key.get(Stage.class), Key.get(Logger.class)); + + private final Comparator> orderByKey = new Comparator>() { + public int compare(Binding a, Binding b) { + return a.getKey().toString().compareTo(b.getKey().toString()); + } + }; + + private static class StringProvider implements Provider { + public String get() { + return "A"; + } + } + + private static class C { } + + private static class D extends C { + @Inject public D(Injector unused) { } + } +} diff --git a/src/test/java/com/google/inject/spi/ToolStageInjectorTest.java b/src/test/java/com/google/inject/spi/ToolStageInjectorTest.java new file mode 100644 index 0000000..7f63f1c --- /dev/null +++ b/src/test/java/com/google/inject/spi/ToolStageInjectorTest.java @@ -0,0 +1,165 @@ +package com.google.inject.spi; + +import com.google.inject.AbstractModule; +import com.google.inject.Asserts; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.Stage; +import com.google.inject.spi.Toolable; + +import junit.framework.TestCase; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ToolStageInjectorTest extends TestCase { + + @Override + protected void setUp() throws Exception { + Foo.s = null; + Foo.sm = null; + } + + public void testToolStageInjectorRestrictions() { + Injector injector = Guice.createInjector(Stage.TOOL); + try { + injector.injectMembers(new Object()); + fail("Non-SPI Injector methods must throw an exception in the TOOL stage."); + } catch (UnsupportedOperationException expected) { + } + + try { + injector.getInstance(Injector.class); + fail("Non-SPI Injector methods must throw an exception in the TOOL stage."); + } catch (UnsupportedOperationException expected) { + } + + try { + injector.getInstance(Key.get(Injector.class)); + fail("Non-SPI Injector methods must throw an exception in the TOOL stage."); + } catch (UnsupportedOperationException expected) { + } + + try { + injector.getProvider(Injector.class); + fail("Non-SPI Injector methods must throw an exception in the TOOL stage."); + } catch (UnsupportedOperationException expected) { + } + + try { + injector.getProvider(Key.get(Injector.class)); + fail("Non-SPI Injector methods must throw an exception in the TOOL stage."); + } catch (UnsupportedOperationException expected) { + } + } + + public void testToolStageDoesntInjectInstances() { + final Foo foo = new Foo(); + Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(Foo.class); + requestInjection(foo); + } + }); + assertNull(Foo.s); + assertNull(Foo.sm); + assertNull(foo.f); + assertNull(foo.m); + } + + public void testToolStageDoesntInjectProviders() { + final Foo foo = new Foo(); + Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(Foo.class); + bind(Object.class).toProvider(foo); + } + }); + assertNull(Foo.s); + assertNull(Foo.sm); + assertNull(foo.f); + assertNull(foo.m); + } + + public void testToolStageWarnsOfMissingObjectGraph() { + final Bar bar = new Bar(); + try { + Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(Bar.class); + requestInjection(bar); + } + }); + fail("expected exception"); + } catch(CreationException expected) { + Asserts.assertContains(expected.toString(), "No implementation for java.util.Collection was bound.", + "No implementation for java.util.Map was bound.", + "No implementation for java.util.List was bound.", + "No implementation for java.util.Set was bound."); + } + } + + public void testToolStageInjectsTooledMethods() { + final Tooled tooled = new Tooled(); + Guice.createInjector(Stage.TOOL, new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(Tooled.class); + bind(Object.class).toProvider(tooled); + } + }); + assertNull(Tooled.s); + assertNotNull(Tooled.sm); + assertNull(tooled.f); + assertNotNull(tooled.m); + } + + @SuppressWarnings("unchecked") + private static class Bar { + @SuppressWarnings("unused") @Inject private static List list; + @SuppressWarnings("unused") @Inject private Set set; + @SuppressWarnings("unused") @Inject void method(Collection c) {} + @SuppressWarnings("unused") @Inject static void staticMethod(Map map) {} + } + + private static class Foo implements Provider { + @Inject private static S s; + @Inject private F f; + private M m; + @SuppressWarnings("unused") @Inject void method(M m) { this.m = m; } + private static SM sm; + @SuppressWarnings("unused") @Inject static void staticMethod(SM sm) { Tooled.sm = sm; } + + public Object get() { + return null; + } + } + + private static class Tooled implements Provider { + @Inject private static S s; + @Inject private F f; + private M m; + @Toolable @SuppressWarnings("unused") @Inject void method(M m) { this.m = m; } + private static SM sm; + @Toolable @SuppressWarnings("unused") @Inject static void staticMethod(SM sm) { Tooled.sm = sm; } + + public Object get() { + return null; + } + } + + private static class S {} + private static class F {} + private static class M {} + private static class SM {} + +} diff --git a/src/test/java/com/google/inject/util/NoopOverrideTest.java b/src/test/java/com/google/inject/util/NoopOverrideTest.java new file mode 100644 index 0000000..9f65bf4 --- /dev/null +++ b/src/test/java/com/google/inject/util/NoopOverrideTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.util; + +import com.google.inject.Module; +import com.google.inject.spi.ElementVisitor; +import com.google.inject.spi.ElementsTest; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class NoopOverrideTest extends ElementsTest { + + protected void checkModule(Module module, ElementVisitor... visitors) { + Module overridden = Modules.override(module).with(Modules.EMPTY_MODULE); + super.checkModule(overridden, visitors); + } +} \ No newline at end of file diff --git a/src/test/java/com/google/inject/util/OverrideModuleTest.java b/src/test/java/com/google/inject/util/OverrideModuleTest.java new file mode 100644 index 0000000..16d7a2c --- /dev/null +++ b/src/test/java/com/google/inject/util/OverrideModuleTest.java @@ -0,0 +1,731 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.util; + +import static com.google.inject.Asserts.asModuleChain; +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Guice.createInjector; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.CreationException; +import com.google.inject.Exposed; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.PrivateModule; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.Scope; +import com.google.inject.ScopeAnnotation; +import com.google.inject.Stage; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Date; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author sberlin@gmail.com (Sam Berlin) + */ +public class OverrideModuleTest extends TestCase { + + private static final Key key2 = Key.get(String.class, named("2")); + private static final Key key3 = Key.get(String.class, named("3")); + + private static final Module EMPTY_MODULE = new Module() { + public void configure(Binder binder) {} + }; + + public void testOverride() { + Injector injector = createInjector(Modules.override(newModule("A")).with(newModule("B"))); + assertEquals("B", injector.getInstance(String.class)); + } + + public void testOverrideMultiple() { + Module module = Modules.override(newModule("A"), newModule(1), newModule(0.5f)) + .with(newModule("B"), newModule(2), newModule(1.5d)); + Injector injector = createInjector(module); + assertEquals("B", injector.getInstance(String.class)); + assertEquals(2, injector.getInstance(Integer.class).intValue()); + assertEquals(0.5f, injector.getInstance(Float.class)); + assertEquals(1.5d, injector.getInstance(Double.class)); + } + + public void testOverrideUnmatchedTolerated() { + Injector injector = createInjector(Modules.override(EMPTY_MODULE).with(newModule("B"))); + assertEquals("B", injector.getInstance(String.class)); + } + + public void testOverrideConstant() { + Module original = new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(named("Test")).to("A"); + } + }; + + Module replacements = new AbstractModule() { + @Override protected void configure() { + bindConstant().annotatedWith(named("Test")).to("B"); + } + }; + + Injector injector = createInjector(Modules.override(original).with(replacements)); + assertEquals("B", injector.getInstance(Key.get(String.class, named("Test")))); + } + + public void testGetProviderInModule() { + Module original = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("A"); + bind(key2).toProvider(getProvider(String.class)); + } + }; + + Injector injector = createInjector(Modules.override(original).with(EMPTY_MODULE)); + assertEquals("A", injector.getInstance(String.class)); + assertEquals("A", injector.getInstance(key2)); + } + + public void testOverrideWhatGetProviderProvided() { + Module original = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("A"); + bind(key2).toProvider(getProvider(String.class)); + } + }; + + Module replacements = newModule("B"); + + Injector injector = createInjector(Modules.override(original).with(replacements)); + assertEquals("B", injector.getInstance(String.class)); + assertEquals("B", injector.getInstance(key2)); + } + + public void testOverrideUsingOriginalsGetProvider() { + Module original = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("A"); + bind(key2).toInstance("B"); + } + }; + + Module replacements = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toProvider(getProvider(key2)); + } + }; + + Injector injector = createInjector(Modules.override(original).with(replacements)); + assertEquals("B", injector.getInstance(String.class)); + assertEquals("B", injector.getInstance(key2)); + } + + public void testOverrideOfOverride() { + Module original = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("A1"); + bind(key2).toInstance("A2"); + bind(key3).toInstance("A3"); + } + }; + + Module replacements1 = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("B1"); + bind(key2).toInstance("B2"); + } + }; + + Module overrides = Modules.override(original).with(replacements1); + + Module replacements2 = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("C1"); + bind(key3).toInstance("C3"); + } + }; + + Injector injector = createInjector(Modules.override(overrides).with(replacements2)); + assertEquals("C1", injector.getInstance(String.class)); + assertEquals("B2", injector.getInstance(key2)); + assertEquals("C3", injector.getInstance(key3)); + } + + static class OuterReplacementsModule extends AbstractModule { + @Override protected void configure() { + install(new InnerReplacementsModule()); + } + } + static class InnerReplacementsModule extends AbstractModule { + @Override protected void configure() { + bind(String.class).toInstance("B"); + bind(String.class).toInstance("C"); + } + } + public void testOverridesTwiceFails() { + Module original = newModule("A"); + Module replacements = new OuterReplacementsModule(); + Module module = Modules.override(original).with(replacements); + try { + createInjector(module); + fail(); + } catch (CreationException expected) { + assertContains(expected.getMessage(), + "A binding to java.lang.String was already configured at " + + InnerReplacementsModule.class.getName(), + asModuleChain(Modules.OverrideModule.class, + OuterReplacementsModule.class, InnerReplacementsModule.class), + "at " + InnerReplacementsModule.class.getName(), + asModuleChain(Modules.OverrideModule.class, + OuterReplacementsModule.class, InnerReplacementsModule.class)); + } + } + + public void testOverridesDoesntFixTwiceBoundInOriginal() { + Module original = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("A"); + bind(String.class).toInstance("B"); + } + }; + + Module replacements = new AbstractModule() { + @Override protected void configure() { + bind(String.class).toInstance("C"); + } + }; + + Module module = Modules.override(original).with(replacements); + try { + createInjector(module); + fail(); + } catch (CreationException expected) { + // The replacement comes first because we replace A with C, + // then we encounter B and freak out. + assertContains(expected.getMessage(), + "1) A binding to java.lang.String was already configured at " + + replacements.getClass().getName(), + asModuleChain(Modules.OverrideModule.class, replacements.getClass()), + "at " + original.getClass().getName(), + asModuleChain(Modules.OverrideModule.class, original.getClass())); + } + } + + public void testStandardScopeAnnotation() { + final SingleUseScope scope = new SingleUseScope(); + + Module module = new AbstractModule() { + @Override protected void configure() { + bindScope(TestScopeAnnotation.class, scope); + bind(String.class).in(TestScopeAnnotation.class); + } + }; + assertFalse(scope.used); + + Guice.createInjector(module); + assertTrue(scope.used); + } + + public void testOverrideUntargettedBinding() { + Module original = new AbstractModule() { + @Override protected void configure() { + bind(Date.class); + } + }; + + Module replacements = new AbstractModule() { + @Override protected void configure() { + bind(Date.class).toInstance(new Date(0)); + } + }; + + Injector injector = createInjector(Modules.override(original).with(replacements)); + assertEquals(0, injector.getInstance(Date.class).getTime()); + } + + public void testOverrideScopeAnnotation() { + final Scope scope = new Scope() { + public Provider scope(Key key, Provider unscoped) { + throw new AssertionError("Should not be called"); + } + }; + + final SingleUseScope replacementScope = new SingleUseScope(); + + Module original = new AbstractModule() { + @Override protected void configure() { + bindScope(TestScopeAnnotation.class, scope); + bind(Date.class).in(TestScopeAnnotation.class); + } + }; + + Module replacements = new AbstractModule() { + @Override protected void configure() { + bindScope(TestScopeAnnotation.class, replacementScope); + } + }; + + Injector injector = createInjector(Modules.override(original).with(replacements)); + injector.getInstance(Date.class); + assertTrue(replacementScope.used); + } + + public void testFailsIfOverridenScopeInstanceHasBeenUsed() { + final Scope scope = new Scope() { + public Provider scope(Key key, Provider unscoped) { + return unscoped; + } + + @Override public String toString() { + return "ORIGINAL SCOPE"; + } + }; + + final Module original = new AbstractModule() { + @Override protected void configure() { + bindScope(TestScopeAnnotation.class, scope); + bind(Date.class).in(scope); + bind(String.class).in(scope); + } + }; + Module originalWrapper = new AbstractModule() { + @Override protected void configure() { + install(original); + } + }; + + Module replacements = new AbstractModule() { + @Override protected void configure() { + bindScope(TestScopeAnnotation.class, new SingleUseScope()); + } + }; + + try { + createInjector(Modules.override(originalWrapper).with(replacements)); + fail("Exception expected"); + } catch (CreationException e) { + assertContains(e.getMessage(), + "1) The scope for @TestScopeAnnotation is bound directly and cannot be overridden.", + "original binding at " + original.getClass().getName() + ".configure(", + asModuleChain(originalWrapper.getClass(), original.getClass()), + "bound directly at " + original.getClass().getName() + ".configure(", + asModuleChain(originalWrapper.getClass(), original.getClass()), + "bound directly at " + original.getClass().getName() + ".configure(", + asModuleChain(originalWrapper.getClass(), original.getClass()), + "at ", replacements.getClass().getName() + ".configure(", + asModuleChain(Modules.OverrideModule.class, replacements.getClass())); + } + } + + public void testOverrideIsLazy() { + final AtomicReference value = new AtomicReference("A"); + Module overridden = Modules.override(new AbstractModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(named("original")).toInstance(value.get()); + } + }).with(new AbstractModule() { + @Override protected void configure() { + bind(String.class).annotatedWith(named("override")).toInstance(value.get()); + } + }); + + // the value.get() call should be deferred until Guice.createInjector + value.set("B"); + Injector injector = Guice.createInjector(overridden); + assertEquals("B", injector.getInstance(Key.get(String.class, named("original")))); + assertEquals("B", injector.getInstance(Key.get(String.class, named("override")))); + } + + public void testOverridePrivateModuleOverPrivateModule() { + Module exposes5and6 = new AbstractModule() { + @Override protected void configure() { + install(new PrivateModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(5); + expose(Integer.class); + + bind(Character.class).toInstance('E'); + } + }); + + install(new PrivateModule() { + @Override protected void configure() { + bind(Long.class).toInstance(6L); + expose(Long.class); + + bind(Character.class).toInstance('F'); + } + }); + } + }; + + AbstractModule exposes15 = new AbstractModule() { + @Override protected void configure() { + install(new PrivateModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(15); + expose(Integer.class); + + bind(Character.class).toInstance('G'); + } + }); + + install(new PrivateModule() { + @Override protected void configure() { + bind(Character.class).toInstance('H'); + } + }); + } + }; + + // override forwards + Injector injector = Guice.createInjector(Modules.override(exposes5and6).with(exposes15)); + assertEquals(15, injector.getInstance(Integer.class).intValue()); + assertEquals(6L, injector.getInstance(Long.class).longValue()); + + // and in reverse order + Injector reverse = Guice.createInjector(Modules.override(exposes15).with(exposes5and6)); + assertEquals(5, reverse.getInstance(Integer.class).intValue()); + assertEquals(6L, reverse.getInstance(Long.class).longValue()); + } + + public void testOverrideModuleAndPrivateModule() { + Module exposes5 = new PrivateModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(5); + expose(Integer.class); + } + }; + + Module binds15 = new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(15); + } + }; + + Injector injector = Guice.createInjector(Modules.override(exposes5).with(binds15)); + assertEquals(15, injector.getInstance(Integer.class).intValue()); + + Injector reverse = Guice.createInjector(Modules.override(binds15).with(exposes5)); + assertEquals(5, reverse.getInstance(Integer.class).intValue()); + } + + public void testOverrideDeepExpose() { + final AtomicReference> charAProvider + = new AtomicReference>(); + + Module exposes5 = new PrivateModule() { + @Override protected void configure() { + install(new PrivateModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(5); + expose(Integer.class); + charAProvider.set(getProvider(Character.class)); + bind(Character.class).toInstance('A'); + } + }); + expose(Integer.class); + } + }; + + Injector injector = Guice.createInjector(Modules.override(exposes5).with(EMPTY_MODULE)); + assertEquals(5, injector.getInstance(Integer.class).intValue()); + assertEquals('A', charAProvider.getAndSet(null).get().charValue()); + + injector = Guice.createInjector(Modules.override(EMPTY_MODULE).with(exposes5)); + assertEquals(5, injector.getInstance(Integer.class).intValue()); + assertEquals('A', charAProvider.getAndSet(null).get().charValue()); + + final AtomicReference> charBProvider + = new AtomicReference>(); + + Module binds15 = new AbstractModule() { + @Override protected void configure() { + bind(Integer.class).toInstance(15); + + install(new PrivateModule() { + @Override protected void configure() { + charBProvider.set(getProvider(Character.class)); + bind(Character.class).toInstance('B'); + } + }); + } + }; + + injector = Guice.createInjector(Modules.override(binds15).with(exposes5)); + assertEquals(5, injector.getInstance(Integer.class).intValue()); + assertEquals('A', charAProvider.getAndSet(null).get().charValue()); + assertEquals('B', charBProvider.getAndSet(null).get().charValue()); + + injector = Guice.createInjector(Modules.override(exposes5).with(binds15)); + assertEquals(15, injector.getInstance(Integer.class).intValue()); + assertEquals('A', charAProvider.getAndSet(null).get().charValue()); + assertEquals('B', charBProvider.getAndSet(null).get().charValue()); + } + + @Retention(RUNTIME) + @Target(TYPE) + @ScopeAnnotation + private static @interface TestScopeAnnotation {} + + private static class SingleUseScope implements Scope { + boolean used = false; + public Provider scope(Key key, Provider unscoped) { + assertFalse(used); + used = true; + return unscoped; + } + } + + static class NewModule extends AbstractModule { + private final T bound; + NewModule(T bound) { + this.bound = bound; + } + @Override protected void configure() { + @SuppressWarnings("unchecked") + Class type = (Class)bound.getClass(); + bind(type).toInstance(bound); + } + } + + private static Module newModule(final T bound) { + return new NewModule(bound); + } + + private static final String RESULT = "RESULT"; + private static final String PRIVATE_INPUT = "PRIVATE_INPUT"; + private static final String OVERRIDDEN_INPUT = "FOO"; + private static final String OVERRIDDEN_RESULT = "Size: 3"; + private static final Key RESULT_KEY = Key.get(String.class, named(RESULT)); + private static final Key INPUT_KEY = Key.get(String.class, named(PRIVATE_INPUT)); + + public void testExposedBindingOverride() throws Exception { + Injector inj = Guice.createInjector( + Modules.override(new ExampleModule()).with( + new AbstractModule() { + @Override protected void configure() { + bind(RESULT_KEY).toInstance(OVERRIDDEN_RESULT); + } + })); + assertEquals(inj.getInstance(RESULT_KEY), OVERRIDDEN_RESULT); + } + + public void testPrivateBindingOverride() throws Exception { + Injector inj = Guice.createInjector( + Modules.override(new ExampleModule()).with( + new AbstractModule() { + @Override protected void configure() { + bind(INPUT_KEY).toInstance(OVERRIDDEN_INPUT); + } + })); + assertEquals(inj.getInstance(RESULT_KEY), OVERRIDDEN_RESULT); + } + + public static class ExampleModule extends PrivateModule { + @Provides @Exposed @Named(RESULT) + public String provideResult(@Named(PRIVATE_INPUT) String input) { + return "Size: " + input.length(); + } + + @Provides @Named(PRIVATE_INPUT) + public String provideInput() { + return "Hello World"; + } + + @Override protected void configure() { + } + } + + public void testEqualsNotCalledByDefaultOnInstance() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnEquals = true; + Guice.createInjector(Modules.override(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(HashEqualsTester.class).toInstance(a); + } + }).with()); + } + + public void testEqualsNotCalledByDefaultOnProvider() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnEquals = true; + Guice.createInjector(Modules.override(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(Object.class).toProvider(a); + } + }).with()); + } + + public void testHashcodeNeverCalledOnInstance() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnHashcode = true; + a.equality = "test"; + + final HashEqualsTester b = new HashEqualsTester(); + b.throwOnHashcode = true; + b.equality = "test"; + Guice.createInjector(Modules.override(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(HashEqualsTester.class).toInstance(a); + bind(HashEqualsTester.class).toInstance(b); + } + }).with()); + } + + public void testHashcodeNeverCalledOnProviderInstance() { + final HashEqualsTester a = new HashEqualsTester(); + a.throwOnHashcode = true; + a.equality = "test"; + + final HashEqualsTester b = new HashEqualsTester(); + b.throwOnHashcode = true; + b.equality = "test"; + Guice.createInjector(Modules.override(new AbstractModule() { + @Override + protected void configure() { + bind(String.class); + bind(Object.class).toProvider(a); + bind(Object.class).toProvider(b); + } + }).with()); + } + + private static class HashEqualsTester implements Provider { + private String equality; + private boolean throwOnEquals; + private boolean throwOnHashcode; + + @Override + public boolean equals(Object obj) { + if (throwOnEquals) { + throw new RuntimeException(); + } else if (obj instanceof HashEqualsTester) { + HashEqualsTester o = (HashEqualsTester)obj; + if(o.throwOnEquals) { + throw new RuntimeException(); + } + if(equality == null && o.equality == null) { + return this == o; + } else { + return Objects.equal(equality, o.equality); + } + } else { + return false; + } + } + + @Override + public int hashCode() { + if(throwOnHashcode) { + throw new RuntimeException(); + } else { + return super.hashCode(); + } + } + + public Object get() { + return new Object(); + } + } + + public void testCorrectStage() { + final Stage stage = Stage.PRODUCTION; + Module module = Modules.override(new AbstractModule() { + @Override + protected void configure() { + if (currentStage() != Stage.PRODUCTION) { + addError("Wronge stage in overridden module:" + currentStage()); + } + } + }).with(new AbstractModule() { + @Override + protected void configure() { + if (currentStage() != Stage.PRODUCTION) { + addError("Wronge stage in overriding module:" + currentStage()); + } + } + }); + Guice.createInjector(stage, module); + } + + public void testOverridesApplyOriginalScanners() { + Injector injector = + Guice.createInjector(Modules.override(NamedMunger.module()).with(new AbstractModule() { + @Override protected void configure() {} + @TestProvides @Named("test") String provideString() { return "foo"; } + })); + + assertNull(injector.getExistingBinding(Key.get(String.class, named("test")))); + Binding binding = injector.getBinding(Key.get(String.class, named("test-munged"))); + assertEquals("foo", binding.getProvider().get()); + } + + @Documented @Target(METHOD) @Retention(RUNTIME) + private @interface TestProvides {} + + private static class NamedMunger extends ModuleAnnotatedMethodScanner { + static Module module() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(new NamedMunger()); + } + }; + } + + @Override + public String toString() { + return "NamedMunger"; + } + + @Override + public Set> annotationClasses() { + return ImmutableSet.of(TestProvides.class); + } + + @Override + public Key prepareMethod(Binder binder, Annotation annotation, Key key, + InjectionPoint injectionPoint) { + return Key.get(key.getTypeLiteral(), + Names.named(((Named) key.getAnnotation()).value() + "-munged")); + } + } +} diff --git a/src/test/java/com/google/inject/util/ProvidersTest.java b/src/test/java/com/google/inject/util/ProvidersTest.java new file mode 100644 index 0000000..6891e57 --- /dev/null +++ b/src/test/java/com/google/inject/util/ProvidersTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.inject.util; + +import com.google.common.base.Objects; +import com.google.common.testing.EqualsTester; +import com.google.inject.Provider; + +import junit.framework.TestCase; + +import javax.inject.Inject; + +/** + * Unit tests for {@link Providers}. + * + * @author Kevin Bourrillion (kevinb9n@gmail.com) + */ +public class ProvidersTest extends TestCase { + + public void testOfInstance() { + String foo = "foo"; + Provider p = Providers.of(foo); + assertSame(foo, p.get()); + assertSame(foo, p.get()); + } + + public void testOfNull() { + Provider p = Providers.of(null); + assertNull(p.get()); + } + + public void testOfEquality() { + new EqualsTester() + .addEqualityGroup( + Providers.of(null), + Providers.of(null)) + .addEqualityGroup( + Providers.of("Hello"), + Providers.of("Hello")) + .testEquals(); + } + + private static class JavaxProvider implements javax.inject.Provider { + private final int value; + + public JavaxProvider(int value) { + this.value = value; + } + + public Integer get() { + return value; + } + + @Override public int hashCode() { + return Objects.hashCode(value); + } + + @Override public boolean equals(Object obj) { + return (obj instanceof JavaxProvider) && (value == ((JavaxProvider) obj).value); + } + } + + private static class JavaxProviderWithDependencies implements javax.inject.Provider { + private int value; + + @Inject void setValue(int value) { + this.value = value; + } + + public Integer get() { + return value; + } + + @Override public int hashCode() { + return 42; + } + + @Override public boolean equals(Object obj) { + return (obj instanceof JavaxProviderWithDependencies); + } + } +} diff --git a/src/test/java/com/google/inject/util/TypesTest.java b/src/test/java/com/google/inject/util/TypesTest.java new file mode 100644 index 0000000..db015de --- /dev/null +++ b/src/test/java/com/google/inject/util/TypesTest.java @@ -0,0 +1,215 @@ +/** + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.inject.util; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.assertEqualWhenReserialized; +import static com.google.inject.Asserts.assertEqualsBothWays; +import static com.google.inject.util.Types.subtypeOf; +import static com.google.inject.util.Types.supertypeOf; + +import com.google.inject.TypeLiteral; +import com.google.inject.internal.MoreTypes; + +import junit.framework.Assert; +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author jessewilson@google.com (Jesse Wilson) + */ +public class TypesTest extends TestCase { + + // generic types for comparison + Map a; + Inner b; + List[][]> c; + List d; + Set e; + Outer.Inner f; + + private ParameterizedType mapStringInteger; + private ParameterizedType innerFloatDouble; + private ParameterizedType listSetStringArray; + private ParameterizedType listString; + private ParameterizedType setString; + private ParameterizedType outerInner; + private GenericArrayType setStringArray; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mapStringInteger = (ParameterizedType) getClass().getDeclaredField("a").getGenericType(); + innerFloatDouble = (ParameterizedType) getClass().getDeclaredField("b").getGenericType(); + listSetStringArray = (ParameterizedType) getClass().getDeclaredField("c").getGenericType(); + listString = (ParameterizedType) getClass().getDeclaredField("d").getGenericType(); + setString = (ParameterizedType) getClass().getDeclaredField("e").getGenericType(); + outerInner = (ParameterizedType) getClass().getDeclaredField("f").getGenericType(); + setStringArray = (GenericArrayType) listSetStringArray.getActualTypeArguments()[0]; + } + + public void testListSetMap() { + assertEqualsBothWays(mapStringInteger, Types.mapOf(String.class, Integer.class)); + assertEqualsBothWays(listString, Types.listOf(String.class)); + assertEqualsBothWays(setString, Types.setOf(String.class)); + } + + public void testDefensiveCopies() { + Type[] arguments = new Type[] { String.class, Integer.class }; + ParameterizedType parameterizedType = Types.newParameterizedType(Map.class, arguments); + arguments[0] = null; + assertEquals(String.class, parameterizedType.getActualTypeArguments()[0]); + parameterizedType.getActualTypeArguments()[1] = null; + assertEquals(Integer.class, parameterizedType.getActualTypeArguments()[1]); + } + + public void testTypeWithOwnerType() { + ParameterizedType actual = Types.newParameterizedTypeWithOwner( + TypesTest.class, Inner.class, Float.class, Double.class); + assertEquals(TypesTest.class, actual.getOwnerType()); + assertEqualsBothWays(innerFloatDouble, actual); + // The JDK prints this out as: + // com.google.inject.util.TypesTest.com.google.inject.util.TypesTest$Inner + // and we think that's wrong, so the assertEquals comparison is worthless. :-( +// assertEquals(innerFloatDouble.toString(), actual.toString()); + + // We think the correct comparison is: + assertEquals("com.google.inject.util.TypesTest$Inner", actual.toString()); + } + + public void testTypeParametersMustNotBePrimitives() { + try { + Types.newParameterizedType(Map.class, String.class, int.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), + "Primitive types are not allowed in type parameters: int"); + } + } + + public List wildcardExtends; + public List wildcardSuper; + public List wildcardObject; + + public void testWildcardTypes() throws NoSuchFieldException, IOException { + assertEqualsBothWays(getWildcard("wildcardSuper"), supertypeOf(CharSequence.class)); + assertEqualsBothWays(getWildcard("wildcardExtends"), subtypeOf(CharSequence.class)); + assertEqualsBothWays(getWildcard("wildcardObject"), subtypeOf(Object.class)); + + assertEquals("? super java.lang.CharSequence", supertypeOf(CharSequence.class).toString()); + assertEquals("? extends java.lang.CharSequence", subtypeOf(CharSequence.class).toString()); + assertEquals("?", subtypeOf(Object.class).toString()); + + assertEqualWhenReserialized(supertypeOf(CharSequence.class)); + assertEqualWhenReserialized(subtypeOf(CharSequence.class)); + } + + public void testWildcardBoundsMustNotBePrimitives() { + try { + supertypeOf(int.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), + "Primitive types are not allowed in wildcard bounds: int"); + } + + try { + subtypeOf(int.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), + "Primitive types are not allowed in wildcard bounds: int"); + } + } + + private WildcardType getWildcard(String fieldName) throws NoSuchFieldException { + ParameterizedType type = (ParameterizedType) getClass().getField(fieldName).getGenericType(); + return (WildcardType) type.getActualTypeArguments()[0]; + } + + public void testEqualsAndHashcode() { + ParameterizedType parameterizedType + = Types.newParameterizedType(Map.class, String.class, Integer.class); + assertEqualsBothWays(mapStringInteger, parameterizedType); + assertEquals(mapStringInteger.toString(), parameterizedType.toString()); + + GenericArrayType genericArrayType = Types.arrayOf(Types.arrayOf( + Types.newParameterizedType(Set.class, String.class))); + assertEqualsBothWays(setStringArray, genericArrayType); + assertEquals(setStringArray.toString(), genericArrayType.toString()); + } + + public void testToString() { + Assert.assertEquals("java.lang.String", MoreTypes.typeToString(String.class)); + assertEquals("java.util.Set[][]", MoreTypes.typeToString(setStringArray)); + assertEquals("java.util.Map", + MoreTypes.typeToString(mapStringInteger)); + assertEquals("java.util.List[][]>", + MoreTypes.typeToString(listSetStringArray)); + assertEquals(innerFloatDouble.toString(), + MoreTypes.typeToString(innerFloatDouble)); + } + + static class Owning {} + + /** + * Ensure that owning types are required when necessary, and forbidden + * otherwise. + */ + public void testCanonicalizeRequiresOwnerTypes() { + try { + Types.newParameterizedType(Owning.class, String.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), + "No owner type for enclosed " + Owning.class); + } + + try { + Types.newParameterizedTypeWithOwner(Object.class, Set.class, String.class); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), + "Owner type for unenclosed " + Set.class); + } + } + + @SuppressWarnings("UnusedDeclaration") + class Inner {} + + public void testInnerParameterizedEvenWithZeroArgs() { + TypeLiteral.Inner> type = new TypeLiteral.Inner>() {}; + assertEqualsBothWays(outerInner, type.getType()); + + ParameterizedType parameterizedType = (ParameterizedType) type.getType(); + assertEquals(0, parameterizedType.getActualTypeArguments().length); + assertEquals(new TypeLiteral>() {}.getType(), parameterizedType.getOwnerType()); + assertEquals(Outer.Inner.class, parameterizedType.getRawType()); + } + + static class Outer { + class Inner {} + } +}