diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 5cdc7d1..0000000 --- a/build.gradle +++ /dev/null @@ -1,4 +0,0 @@ -plugins { - id "io.micronaut.build.internal.docs" - id "io.micronaut.build.internal.quality-reporting" -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..2fbc3d3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("io.micronaut.build.internal.docs") + id("io.micronaut.build.internal.quality-reporting") +} + diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.guice-module.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.guice-module.gradle index b49f716..21d4ac8 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.guice-module.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.guice-module.gradle @@ -2,3 +2,9 @@ plugins { id 'io.micronaut.build.internal.guice-base' id "io.micronaut.build.internal.module" } + +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f401fc..e0bb762 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,30 +1,13 @@ -# -# This file is used to declare the list of libraries -# which are used as dependencies in the project. -# See https://docs.gradle.org/7.4.2/userguide/platforms.html#sub:central-declaration-of-dependencies -# -# For Micronaut, we have 3 kinds of dependencies: -# - managed dependencies, which are exposed to consumers via a BOM (or version catalog) -# - managed BOMs, which are imported into the BOM that we generate -# - all other dependencies, which are implementation details -# -# If a library needs to appear in the BOM of the project, then it must be -# declared with the "managed-" prefix. -# If a BOM needs to be imported in the BOM of the project, then it must be -# declared with the "boms-" prefix. -# Both managed dependencies and BOMs need to have their version declared via -# a managed version (a version which alias starts with "managed-" - [versions] -micronaut = "4.4.8" +micronaut = "4.4.3" micronaut-docs = "2.0.0" micronaut-test = "4.2.1" groovy = "4.0.17" spock = "2.3-groovy-4.0" # Managed versions appear in the BOM -# managed-somelib = "1.0" -# managed-somebom = "1.1" +managed-guice = "7.0.0" +managed-guava = "33.2.0-jre" [libraries] # Core @@ -32,19 +15,8 @@ micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'mi # # Managed dependencies appear in the BOM -# -# managed-somelib = { module = "group:artifact", version.ref = "managed-somelib" } - -# -# Imported BOMs, also appearing in the generated BOM -# -# boms-somebom = { module = "com.foo:somebom", version.ref = "managed-somebom" } - -# Other libraries used by the project but non managed - -# micronaut-bom = { module = "io.micronaut:micronaut-bom", version.ref = "micronaut" } -# jdoctor = { module = "me.champeau.jdoctor:jdoctor-core", version.ref="jdoctor" } - -[bundles] - -[plugins] +managed-guice = { module = "com.google.inject:guice", version.ref = "managed-guice" } +managed-guava = { module = "com.google.guava:guava", version.ref = "managed-guava" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" } diff --git a/micronaut-guice-annotation/build.gradle.kts b/micronaut-guice-annotation/build.gradle.kts new file mode 100644 index 0000000..67791f5 --- /dev/null +++ b/micronaut-guice-annotation/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("io.micronaut.build.internal.guice-module") +} + +dependencies { + implementation(libs.managed.guice) { + exclude(group="com.google.guava", module = "guava") + } +} diff --git a/micronaut-guice-annotation/src/main/java/io/micronaut/guice/annotation/Guice.java b/micronaut-guice-annotation/src/main/java/io/micronaut/guice/annotation/Guice.java new file mode 100644 index 0000000..1c0373b --- /dev/null +++ b/micronaut-guice-annotation/src/main/java/io/micronaut/guice/annotation/Guice.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.annotation; + +import com.google.inject.Module; +import io.micronaut.context.annotation.AliasFor; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be applied to the application entry point + * that allows the import of Guice modules. + * + *

Micronaut will import the modules and run them at startup when the application starts + * registering the provided beans using the Guice DSL.

+ * + *

Note all features of Guice are supported, there exist the following limitations:

+ * + *
    + *
  1. Guice Scopes are not supported
  2. + *
  3. Guice AOP/Interceptors are not supported
  4. + *
  5. Guice private modules are not supported
  6. + *
  7. Static Injection is not supported
  8. + *
  9. Guice TypeConverters are not supported (use {@link io.micronaut.core.convert.TypeConverter} instead.
  10. + *
  11. Guice Listeners are not supported (use {@link io.micronaut.context.event.BeanCreatedEventListener} instead.
  12. + *
  13. None of the {@code com.google.inject.spi} API is supported
  14. + *
+ * + *

Note that if you create a runtime binding to a class with {@link com.google.inject.binder.LinkedBindingBuilder#to(Class)} that has no injection annotations you may need to import the bean first + * to allow the bean to be instantiated without reflection. This can be done with {@link io.micronaut.context.annotation.Import}

+ * + *

Otherwise it is recommended to as a minimum use the {@link jakarta.inject.Inject} annotation on the constructor to avoid this need.

+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface Guice { + /** + * Import the given Guice modules. + * + *

The modules are imported in the order defined by the array.

+ * + * @return An array of module types + */ + Class[] modules(); + + /** + * Import the given Guice classes. + * + * @return An array of classes to import + */ + Class[] classes() default {}; + + /** + * Import the given named Guice classes. + * + * @return An array of class names to import + */ + @AliasFor(member = "classes") + String[] classNames() default {}; + + /** + * The environment where the modules should be active (Defaults to all environments). + * + * @return The environments. + */ + String[] environments() default {}; +} diff --git a/micronaut-guice-annotation/src/main/java/io/micronaut/guice/annotation/internal/GuiceAnnotation.java b/micronaut-guice-annotation/src/main/java/io/micronaut/guice/annotation/internal/GuiceAnnotation.java new file mode 100644 index 0000000..a5b8283 --- /dev/null +++ b/micronaut-guice-annotation/src/main/java/io/micronaut/guice/annotation/internal/GuiceAnnotation.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.annotation.internal; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Internal meta-annotation for identifying Guice annotated beans. + * + *

A Guice annotated bean is a bean that is meta annotated with the annotation {@link com.google.inject.ScopeAnnotation}.

+ */ +@Retention(RetentionPolicy.SOURCE) +@Internal +public @interface GuiceAnnotation { + @NonNull AnnotationValue ANNOTATION_VALUE = AnnotationValue.builder(GuiceAnnotation.class).build(); +} diff --git a/micronaut-guice-bom/build.gradle b/micronaut-guice-bom/build.gradle deleted file mode 100644 index 92fe279..0000000 --- a/micronaut-guice-bom/build.gradle +++ /dev/null @@ -1,4 +0,0 @@ -plugins { - id 'io.micronaut.build.internal.guice-base' - id "io.micronaut.build.internal.bom" -} diff --git a/micronaut-guice-bom/build.gradle.kts b/micronaut-guice-bom/build.gradle.kts new file mode 100644 index 0000000..a7d7761 --- /dev/null +++ b/micronaut-guice-bom/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("io.micronaut.build.internal.guice-base") + id("io.micronaut.build.internal.bom") +} + +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/micronaut-guice-processor/build.gradle.kts b/micronaut-guice-processor/build.gradle.kts new file mode 100644 index 0000000..e2a21eb --- /dev/null +++ b/micronaut-guice-processor/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("io.micronaut.build.internal.guice-module") +} + +dependencies { + implementation(projects.micronautGuiceAnnotation) + implementation(mn.micronaut.core.processor) + implementation(libs.managed.guice) { + exclude(group="com.google.guava", module = "guava") + } + testImplementation(mn.micronaut.inject.java.test) + testImplementation(projects.micronautGuice) +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/BindingAnnotationTransformer.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/BindingAnnotationTransformer.java new file mode 100644 index 0000000..01ddcf7 --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/BindingAnnotationTransformer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.BindingAnnotation; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.annotation.TypedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.List; + +/** + * Transforms {@link com.google.inject.BindingAnnotation} to {@link jakarta.inject.Qualifier}. + */ +public class BindingAnnotationTransformer + implements TypedAnnotationTransformer { + @Override + public Class annotationType() { + return BindingAnnotation.class; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(AnnotationUtil.QUALIFIER) + .build(), + GuiceAnnotation.ANNOTATION_VALUE + ); + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/GuiceBeanVisitor.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/GuiceBeanVisitor.java new file mode 100644 index 0000000..f459cbb --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/GuiceBeanVisitor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.RestrictedBindingSource; +import io.micronaut.context.annotation.Bean; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.Set; + +/** + * Guice beans have a only 1 binding type. This visitor resolves that. + */ +public class GuiceBeanVisitor + implements TypeElementVisitor { + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public Set getSupportedAnnotationNames() { + return Set.of(GuiceAnnotation.class.getName(), "com.google.inject.*"); + } + + @Override + public void visitConstructor(ConstructorElement element, VisitorContext context) { + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + if (element.hasDeclaredAnnotation(RestrictedBindingSource.class)) { + throw new ProcessingException(element, "The @RestrictedBindingSource annotation is not supported"); + } + if (element.hasStereotype(GuiceAnnotation.class)) { + exposeOnlyType(element); + } + } + + private static void exposeOnlyType(ClassElement element) { + if (!element.isPresent(Bean.class, "typed")) { + element.annotate(Bean.class, builder -> + builder.member("typed", new AnnotationClassValue<>(element.getName())) + ); + } + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ImplementedByTransformer.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ImplementedByTransformer.java new file mode 100644 index 0000000..f0e322c --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ImplementedByTransformer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.ImplementedBy; +import io.micronaut.context.annotation.DefaultImplementation; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.annotation.TypedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.List; + + +/** + * Transforms {@link com.google.inject.ImplementedBy} to {@link DefaultImplementation}. + */ +public class ImplementedByTransformer + implements TypedAnnotationTransformer { + @Override + public Class annotationType() { + return ImplementedBy.class; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + AnnotationClassValue t = annotation.stringValue().map(AnnotationClassValue::new).orElse(null); + if (t != null) { + return List.of( + AnnotationValue.builder(DefaultImplementation.class) + .member(AnnotationMetadata.VALUE_MEMBER, t) + .build(), + GuiceAnnotation.ANNOTATION_VALUE + ); + } + return List.of(); + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ImportModuleVisitor.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ImportModuleVisitor.java new file mode 100644 index 0000000..3dd8962 --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ImportModuleVisitor.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.Module; +import com.google.inject.Provides; +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.beans.BeanElementBuilder; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.List; +import java.util.Set; + +public class ImportModuleVisitor + implements TypeElementVisitor { + + public static final String MEMBER_ENVS = "environments"; + public static final String MEMBER_MODULES = "modules"; + public static final String MEMBER_CLASSES = "classes"; + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + @NonNull String[] moduleNames = element.stringValues(Guice.class, MEMBER_MODULES); + @NonNull String[] classNames = element.stringValues(Guice.class, MEMBER_CLASSES); + @NonNull String[] envs = element.stringValues(Guice.class, MEMBER_ENVS); + for (String className : classNames) { + ClassElement classElement = context.getClassElement(className).orElse(null); + if (classElement == null) { + throw new ProcessingException(element, "Guice class import [" + className + "] must be on the compilation classpath"); + } else { + BeanElementBuilder builder = element.addAssociatedBean(classElement); + builder.inject(); + builder.typed(classElement); + } + } + for (int i = 0; i < moduleNames.length; i++) { + String className = moduleNames[i]; + ClassElement moduleElement = context.getClassElement(className).orElse(null); + if (moduleElement == null) { + throw new ProcessingException(element, "Guice module [" + className + "] must be on the compilation classpath"); + } + int order = i; + MethodElement primaryConstructor = + moduleElement.getPrimaryConstructor().orElse(null); + if (primaryConstructor == null) { + throw new ProcessingException(element, """ + Cannot import Guice module [" + moduleElement.getName() + "], since it has multiple constructors or no accessible constructor. + Consider defining a single public accessible constructor or if there are multiple adding @Inject to one of them. + """); + } else { + + BeanElementBuilder beanElementBuilder = element.addAssociatedBean( + moduleElement + ).annotate(Order.class, builder -> + builder.value(order) // retain load order + ); + if (ArrayUtils.isNotEmpty(envs)) { + beanElementBuilder.annotate(Requires.class, env -> env.member("env", envs)); + } + beanElementBuilder.createWith(primaryConstructor); + ElementQuery producesMethodQuery = ElementQuery.ALL_METHODS + .annotated(am -> am.hasAnnotation(Provides.class)) + .onlyDeclared() + .onlyConcrete(); + List methodElements = moduleElement.getEnclosedElements(producesMethodQuery); + for (MethodElement methodElement : methodElements) { + if (!methodElement.isPublic()) { + throw new ProcessingException(methodElement, "Method's annotated with @Produces must be public"); + } + if (methodElement.getReturnType().isVoid()) { + throw new ProcessingException(methodElement, "Method's annotated with @Produces cannot return 'void'"); + } + if (!methodElement.getReturnType().isPublic()) { + throw new ProcessingException(methodElement, "Method's annotated with @Produces must return a publicly accessible type"); + } + } + beanElementBuilder.produceBeans(producesMethodQuery, childBuilder -> { + MethodElement methodElement = (MethodElement) childBuilder.getProducingElement(); + childBuilder.typed(methodElement.getGenericReturnType()); + childBuilder.annotate(Primary.class); + AnnotationMetadata annotationMetadata = methodElement.getAnnotationMetadata(); + Set annotationNames = annotationMetadata.getAnnotationNames(); + for (String annotationName : annotationNames) { + if (!annotationName.equals(Provides.class.getName())) { + annotationMetadata.findAnnotation(annotationName) + .ifPresent(childBuilder::annotate); + } + } + if (ArrayUtils.isNotEmpty(envs)) { + beanElementBuilder.annotate(Requires.class, env -> env.member("env", envs)); + } + }); + + beanElementBuilder.typed(ClassElement.of(Module.class), moduleElement); + } + } + } + + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public Set getSupportedAnnotationNames() { + return Set.of(Guice.class.getName()); + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/InjectTransformer.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/InjectTransformer.java new file mode 100644 index 0000000..d0cf4d9 --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/InjectTransformer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.Inject; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.annotation.TypedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.List; + +/** + * Transforms {@link com.google.inject.Inject} to {@link jakarta.inject.Inject}. + */ +public class InjectTransformer + implements TypedAnnotationTransformer { + @Override + public Class annotationType() { + return Inject.class; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(AnnotationUtil.INJECT) + .member("required", annotation.booleanValue("optional").map(optional -> !optional).orElse(true)) + .build(), + GuiceAnnotation.ANNOTATION_VALUE + ); + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/NamedAnnotationTransformer.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/NamedAnnotationTransformer.java new file mode 100644 index 0000000..bdf3eeb --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/NamedAnnotationTransformer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.name.Named; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.util.StringUtils; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.annotation.TypedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; +import java.lang.annotation.Annotation; +import java.util.List; + +public class NamedAnnotationTransformer + implements TypedAnnotationTransformer { + @Override + public Class annotationType() { + return Named.class; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + String name = annotation.stringValue().orElse(null); + AnnotationValueBuilder builder = AnnotationValue.builder(AnnotationUtil.NAMED); + if (StringUtils.isNotEmpty(name)) { + builder.value(name); + } + return List.of( + builder.build(), + GuiceAnnotation.ANNOTATION_VALUE + ); + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ScopeAnnotationMapper.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ScopeAnnotationMapper.java new file mode 100644 index 0000000..696110e --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/ScopeAnnotationMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.ScopeAnnotation; +import io.micronaut.context.annotation.Bean; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.List; + +/** + * Transforms {@link com.google.inject.ScopeAnnotation} to {@link jakarta.inject.Scope}. + */ +public class ScopeAnnotationMapper + implements TypedAnnotationMapper { + + @Override + public Class annotationType() { + return ScopeAnnotation.class; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(AnnotationUtil.SCOPE).build(), + GuiceAnnotation.ANNOTATION_VALUE, + AnnotationValue.builder(Bean.class).build() + ); + } +} diff --git a/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/SingletonTransformer.java b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/SingletonTransformer.java new file mode 100644 index 0000000..ceacbb0 --- /dev/null +++ b/micronaut-guice-processor/src/main/java/io/micronaut/guice/processor/SingletonTransformer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice.processor; + +import com.google.inject.Singleton; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.guice.annotation.internal.GuiceAnnotation; +import io.micronaut.inject.annotation.TypedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; +import java.util.List; + +/** + * Transforms {@link com.google.inject.Singleton} to {@link jakarta.inject.Singleton}. + */ +public class SingletonTransformer + implements TypedAnnotationTransformer { + @Override + public Class annotationType() { + return Singleton.class; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of( + AnnotationValue.builder(AnnotationUtil.SINGLETON) + .stereotypes(annotation.getStereotypes()) + .build(), + GuiceAnnotation.ANNOTATION_VALUE + ); + } +} diff --git a/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper new file mode 100644 index 0000000..34e03ad --- /dev/null +++ b/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -0,0 +1 @@ +io.micronaut.guice.processor.ScopeAnnotationMapper diff --git a/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer new file mode 100644 index 0000000..0ae2763 --- /dev/null +++ b/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -0,0 +1,5 @@ +io.micronaut.guice.processor.InjectTransformer +io.micronaut.guice.processor.ImplementedByTransformer +io.micronaut.guice.processor.SingletonTransformer +io.micronaut.guice.processor.NamedAnnotationTransformer +io.micronaut.guice.processor.BindingAnnotationTransformer diff --git a/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 0000000..b84b0a3 --- /dev/null +++ b/micronaut-guice-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1,2 @@ +io.micronaut.guice.processor.ImportModuleVisitor +io.micronaut.guice.processor.GuiceBeanVisitor diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/BindingAnnotationSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/BindingAnnotationSpec.groovy new file mode 100644 index 0000000..b5f1b03 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/BindingAnnotationSpec.groovy @@ -0,0 +1,51 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class BindingAnnotationSpec + extends AbstractTypeElementSpec { + void "test binding annotation"() { + given: + def ctx = buildContext( ''' +package test; + +import com.google.inject.BindingAnnotation;import com.google.inject.ImplementedBy; +import com.google.inject.Inject;import com.google.inject.Singleton;import io.micronaut.context.annotation.Bean;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy; + +class Test { + @Inject @One public TestInterface one; + @Inject @Two public TestInterface two; +} +interface TestInterface { +} + +@Singleton +@One +@Bean(typed = TestInterface.class) +class TestImpl implements TestInterface { + +} + +@Singleton +@Two +@Bean(typed = TestInterface.class) +class TestImpl2 implements TestInterface { + +} + +@BindingAnnotation +@Retention(RetentionPolicy.RUNTIME) +@interface One {} + +@BindingAnnotation +@Retention(RetentionPolicy.RUNTIME) +@interface Two {} +''') + def cls = ctx.classLoader.loadClass('test.Test') + def bean = ctx.getBean(cls) + + expect: + bean.one.getClass().simpleName == 'TestImpl' + bean.two.getClass().simpleName == 'TestImpl2' + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ImplementedBySpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ImplementedBySpec.groovy new file mode 100644 index 0000000..ed4c121 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ImplementedBySpec.groovy @@ -0,0 +1,37 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.annotation.DefaultImplementation +import spock.lang.PendingFeature + +class ImplementedBySpec + extends AbstractTypeElementSpec { + + @PendingFeature(reason = "requires Micronaut 4.5 - see https://github.com/micronaut-projects/micronaut-core/pull/10820") + void "test implemented by"() { + given: + def ctx = buildContext( ''' +package test; + +import com.google.inject.ImplementedBy; +import com.google.inject.Singleton; + +@ImplementedBy(TestImpl.class) +interface Test { +} + +@Singleton +class TestImpl implements Test { + +} + +@Singleton +class TestImpl2 implements Test { + +} +''') + def cls = ctx.classLoader.loadClass('test.Test') + expect: + ctx.getBean(cls).class.simpleName == 'TestImpl' + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ImportModulesSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ImportModulesSpec.groovy new file mode 100644 index 0000000..c1400f7 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ImportModulesSpec.groovy @@ -0,0 +1,299 @@ +package io.micronaut.guice.processor + +import com.google.inject.Module +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class ImportModulesSpec + extends AbstractTypeElementSpec { + + void "test bind instance"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import io.micronaut.guice.annotation.Guice; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(String.class).toInstance("test"); + } +} + +@Guice(modules= SimpleModule.class) +class Test { + @Inject public String foo; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.foo == 'test' + } + + void "test bind provider"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import io.micronaut.guice.annotation.Guice; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(String.class).toProvider(() -> "test"); + } +} + +@Guice(modules= SimpleModule.class) +class Test { + @Inject public String foo; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.foo == 'test' + } + + void "test bind instance with annotation binding"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.BindingAnnotation;import com.google.inject.Inject; +import io.micronaut.guice.annotation.Guice; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(String.class).annotatedWith(One.class).toInstance("test1"); + bind(String.class).annotatedWith(Two.class).toInstance("test2"); + } +} + +@Guice(modules= SimpleModule.class) +class Test { + @Inject @One public String foo; + @Inject @Two public String bar; +} + +@Retention(RetentionPolicy.RUNTIME) +@BindingAnnotation +@interface One {} + +@Retention(RetentionPolicy.RUNTIME) +@BindingAnnotation +@interface Two {} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.foo == 'test1' + bean.bar == 'test2' + } + + + void "test bind interface to impl"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.micronaut.guice.annotation.Guice; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(ITest.class).to(TestImpl.class); + } +} + +interface ITest {} + +@Singleton +class TestImpl implements ITest { + +} + +@Guice(modules= SimpleModule.class) +class Test { + @Inject public ITest test; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.test != null + bean.test.getClass().simpleName == 'TestImpl' + } + + void "test bind interface to impl - jakarta"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import io.micronaut.guice.annotation.Guice; +import jakarta.inject.Singleton; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(ITest.class).to(TestImpl.class); + } +} + +interface ITest {} + +@Singleton +class TestImpl implements ITest { + +} + +@Guice(modules= SimpleModule.class) +class Test { + @Inject public ITest test; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.test != null + bean.test.getClass().simpleName == 'TestImpl' + } + + void "test bind interface to impl - import"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import io.micronaut.context.annotation.Import; +import io.micronaut.guice.annotation.Guice; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(ITest.class).to(TestImpl.class); + } +} + +interface ITest {} + +class TestImpl implements ITest { + +} + +@Guice(modules= SimpleModule.class) +@Import(classes = TestImpl.class) +class Test { + @Inject public ITest test1; + @Inject public ITest test2; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.test1 != null + bean.test2.getClass().simpleName == 'TestImpl' + bean.test1 != bean.test2 + } + + void "test bind interface to impl - import as singleton"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import io.micronaut.context.annotation.Import; +import io.micronaut.guice.annotation.Guice; +import jakarta.inject.Singleton; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(ITest.class).to(TestImpl.class).in(Singleton.class); + } +} + +interface ITest {} + +class TestImpl implements ITest { + +} + +@Guice(modules= SimpleModule.class) +@Import(classes = TestImpl.class) +class Test { + @Inject public ITest test1; + @Inject public ITest test2; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.test1 != null + bean.test2.getClass().simpleName == 'TestImpl' + bean.test1 == bean.test2 + } + + void "test bind interface to impl - import as singleton 2"() { + given: + def context = buildContext("test.Test", ''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.micronaut.context.annotation.Import; +import io.micronaut.guice.annotation.Guice; + +class SimpleModule extends AbstractModule { + @Override protected void configure() { + bind(ITest.class).to(TestImpl.class).in(Singleton.class); + } +} + +interface ITest {} + +class TestImpl implements ITest { + +} + +@Guice(modules= SimpleModule.class) +@Import(classes = TestImpl.class) +class Test { + @Inject public ITest test1; + @Inject public ITest test2; +} +''', true) + + + expect: + context.getBean(Module.class) + def bean = getBean(context, 'test.Test') + bean.test1 != null + bean.test2.getClass().simpleName == 'TestImpl' + bean.test1 == bean.test2 + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/InjectSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/InjectSpec.groovy new file mode 100644 index 0000000..785b0c1 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/InjectSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class InjectSpec extends AbstractTypeElementSpec { + + void "test inject annotation"() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; +import com.google.inject.Inject; + +class Test { + @Inject String whatever; +} +''') + expect: + definition != null + definition.injectedFields.size() == 1 + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/NamedSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/NamedSpec.groovy new file mode 100644 index 0000000..2d2a431 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/NamedSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.inject.qualifiers.Qualifiers + +class NamedSpec + extends AbstractTypeElementSpec { + + void "test named"() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + + +class Test { + @Named("foo") + @Inject + String foo; +} + +''') + expect: + definition != null + !definition.isSingleton() + definition.injectedFields.size() == 1 + def qualifier = Qualifiers.forArgument(definition.injectedFields[0].asArgument()) + qualifier == Qualifiers.byName("foo") + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ProvidesSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ProvidesSpec.groovy new file mode 100644 index 0000000..217c1a4 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ProvidesSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class ProvidesSpec extends AbstractTypeElementSpec { + + void "test produces annotation"() { + given: + def ctx = buildContext(''' +package test; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Provides; +import io.micronaut.guice.annotation.Guice; + +class SimpleModule extends AbstractModule { + @Provides + public String test() { + return "good"; + } +} + +@Guice(modules= SimpleModule.class) +class Test { + @Inject public String foo; +} +''') + expect: + getBean(ctx, 'test.Test').foo == 'good' + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ScopeSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ScopeSpec.groovy new file mode 100644 index 0000000..ca11e66 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/ScopeSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class ScopeSpec + extends AbstractTypeElementSpec { + + void "test scope annotation"() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; + +import com.google.inject.ScopeAnnotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@MyScope +class Test { +} + +@ScopeAnnotation +@Retention(RetentionPolicy.RUNTIME) +@interface MyScope { + +} +''') + expect: + definition != null + !definition.isSingleton() + definition.getScopeName().get() == 'test.MyScope' + } +} diff --git a/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/SingletonSpec.groovy b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/SingletonSpec.groovy new file mode 100644 index 0000000..383c338 --- /dev/null +++ b/micronaut-guice-processor/src/test/groovy/io/micronaut/guice/processor/SingletonSpec.groovy @@ -0,0 +1,44 @@ +package io.micronaut.guice.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec + +class SingletonSpec + extends AbstractTypeElementSpec { + + void "test singleton annotation"() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; + +import com.google.inject.Singleton; + +@Singleton +class Test { +} +''') + expect: + definition != null + definition.isSingleton() + } + + void "test singleton annotation has only one binding"() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; + +import com.google.inject.Singleton; + +@Singleton +class Test implements ITest { +} + +interface ITest { + +} +''') + expect: + definition != null + definition.isSingleton() + definition.exposedTypes == [definition.beanType] as Set + } +} diff --git a/micronaut-guice/build.gradle b/micronaut-guice/build.gradle deleted file mode 100644 index f982748..0000000 --- a/micronaut-guice/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'io.micronaut.build.internal.guice-module' -} diff --git a/micronaut-guice/build.gradle.kts b/micronaut-guice/build.gradle.kts new file mode 100644 index 0000000..c7f0af5 --- /dev/null +++ b/micronaut-guice/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("io.micronaut.build.internal.guice-module") +} + +dependencies { + implementation(mn.micronaut.context) + implementation(projects.micronautGuiceAnnotation) + implementation(libs.managed.guice) { + exclude(group="com.google.guava", module = "guava") + } + runtimeOnly(libs.managed.guava) + testAnnotationProcessor(projects.micronautGuiceProcessor) + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mnTest.micronaut.test.junit5) + testImplementation(mnTest.mockito.junit.jupiter) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +//tasks { +// compileTestJava { +// options.isFork = true +// options.forkOptions.jvmArgs = listOf("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005") +// } +//} diff --git a/micronaut-guice/src/main/java/io/micronaut/guice/GuiceModuleBinder.java b/micronaut-guice/src/main/java/io/micronaut/guice/GuiceModuleBinder.java new file mode 100644 index 0000000..3da34cd --- /dev/null +++ b/micronaut-guice/src/main/java/io/micronaut/guice/GuiceModuleBinder.java @@ -0,0 +1,641 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 io.micronaut.guice; + +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.BindingAnnotation; +import com.google.inject.CreationException; +import com.google.inject.ImplementedBy; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +import com.google.inject.Module; +import com.google.inject.PrivateBinder; +import com.google.inject.ProvidedBy; +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.LinkedBindingBuilder; +import com.google.inject.binder.ScopedBindingBuilder; +import com.google.inject.matcher.Matcher; +import com.google.inject.name.Named; +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 io.micronaut.context.ApplicationContext; +import io.micronaut.context.BeanProvider; +import io.micronaut.context.RuntimeBeanDefinition; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.env.Environment; +import io.micronaut.context.event.StartupEvent; +import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.context.exceptions.NoSuchBeanException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.order.Ordered; +import io.micronaut.core.reflect.InstantiationUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; +import io.micronaut.inject.qualifiers.PrimaryQualifier; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.runtime.event.annotation.EventListener; +import jakarta.inject.Qualifier; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import org.aopalliance.intercept.MethodInterceptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Context +@Order(Ordered.HIGHEST_PRECEDENCE) +@Internal +class GuiceModuleBinder implements Binder { + private static final Logger LOG = LoggerFactory.getLogger(GuiceModuleBinder.class); + private final ApplicationContext applicationContext; + private final List> linkedBindingBuilders = new ArrayList<>(); + private final List constantBuilders = new ArrayList<>(); + private final List errors = new ArrayList<>(); + private final List toInject = new ArrayList<>(); + private Object currentSource; + + GuiceModuleBinder( + ApplicationContext applicationContext, + List modules) { + this.applicationContext = applicationContext; + for (Module module : modules) { + withSource(module); + module.configure(this); + } + try { + if (!this.errors.isEmpty()) { + for (Message error : errors) { + Throwable cause = error.getCause(); + if (cause != null) { + LOG.error("Guice Module Error: " + error.getMessage(), cause); + } else { + LOG.error("Guice Module Error: {}", error.getMessage()); + } + } + throw new ConfigurationException("Failed to import modules due to prior errors"); + } + for (LinkedBindingBuilderImpl builder : linkedBindingBuilders) { + RuntimeBeanDefinition beanDefinition = builder.build(); + if (beanDefinition != null) { + applicationContext.registerBeanDefinition(beanDefinition); + } + } + + for (AnnotatedConstantBindingBuilderImpl constantBuilder : constantBuilders) { + RuntimeBeanDefinition beanDefinition = constantBuilder.build(); + applicationContext.registerBeanDefinition(beanDefinition); + } + } finally { + linkedBindingBuilders.clear(); + constantBuilders.clear(); + } + } + + @EventListener + void onStartup(StartupEvent startupEvent) { + // run more injections + try { + for (Object o : toInject) { + applicationContext.inject(o); + } + } finally { + toInject.clear(); + } + } + + @Override + public void bindInterceptor(Matcher> classMatcher, Matcher methodMatcher, MethodInterceptor... interceptors) { + throw new UnsupportedOperationException("Guice interceptors are not supported"); + } + + @Override + public void bindScope(Class annotationType, Scope scope) { + if (scope != Scopes.NO_SCOPE && scope != Scopes.SINGLETON) { + throw new UnsupportedOperationException("Guice custom scopes are not supported"); + } + } + + @Override + public LinkedBindingBuilder bind(Key key) { + Argument argument = (Argument) Argument.of(key.getTypeLiteral().getType()); + LinkedBindingBuilderImpl builder = new LinkedBindingBuilderImpl<>(argument); + linkedBindingBuilders.add(builder); + return builder; + } + + @Override + public AnnotatedBindingBuilder bind(TypeLiteral typeLiteral) { + Argument argument = (Argument) Argument.of(typeLiteral.getType()); + LinkedBindingBuilderImpl builder = new LinkedBindingBuilderImpl<>(argument); + linkedBindingBuilders.add(builder); + return builder; + } + + @Override + public AnnotatedBindingBuilder bind(Class type) { + LinkedBindingBuilderImpl builder = new LinkedBindingBuilderImpl<>(Argument.of(type)); + linkedBindingBuilders.add(builder); + return builder; + } + + @Override + public AnnotatedConstantBindingBuilder bindConstant() { + AnnotatedConstantBindingBuilderImpl builder = new AnnotatedConstantBindingBuilderImpl(); + constantBuilders.add(builder); + return builder; + } + + @Override + public void requestInjection(TypeLiteral type, T instance) { + requestInjection(instance); + } + + @Override + public void requestInjection(Object instance) { + if (!toInject.contains(instance)) { + toInject.add(instance); + } + + } + + @Override + public void requestStaticInjection(Class... types) { + throw new UnsupportedOperationException("Static injection is not supported"); + } + + @Override + public void install(Module module) { + module.configure(this); + } + + @Override + public Stage currentStage() { + Set activeNames = applicationContext.getEnvironment().getActiveNames(); + if (activeNames.contains(Environment.DEVELOPMENT) || activeNames.contains(Environment.TEST)) { + return Stage.DEVELOPMENT; + } + return Stage.PRODUCTION; + } + + @Override + public void addError(String message, Object... arguments) { + Objects.requireNonNull(message, "Message cannot be null"); + String msg = String.format(message, arguments); + addError(new Message(msg)); + } + + @Override + public void addError(Throwable t) { + Objects.requireNonNull(t, "Throwable cannot be null"); + addError(new Message(t.getMessage(), t)); + } + + @Override + public void addError(Message message) { + Objects.requireNonNull(message, "Message cannot be null"); + errors.add(message); + } + + @Override + public Provider getProvider(Key key) { + Objects.requireNonNull(key, "Key cannot be null"); + @SuppressWarnings("unchecked") + Argument argument = (Argument) Argument.of(key.getTypeLiteral().getType()); + @SuppressWarnings("unchecked") + BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument)); + return provider::get; + } + + @Override + public Provider getProvider(Dependency dependency) { + Objects.requireNonNull(dependency, "Dependency cannot be null"); + return getProvider(dependency.getKey()); + } + + @Override + public Provider getProvider(Class type) { + Objects.requireNonNull(type, "Type cannot be null"); + @SuppressWarnings("unchecked") + BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, type)); + return provider::get; + } + + @Override + public MembersInjector getMembersInjector(TypeLiteral typeLiteral) { + return instance -> { + if (!applicationContext.isRunning()) { + throw new IllegalStateException("Injector not started"); + } + applicationContext.inject(instance); + }; + } + + @Override + public MembersInjector getMembersInjector(Class type) { + return instance -> { + if (!applicationContext.isRunning()) { + throw new IllegalStateException("Injector not started"); + } + applicationContext.inject(instance); + }; + } + + @Override + public void convertToTypes(Matcher> typeMatcher, TypeConverter converter) { + throw new UnsupportedOperationException("Method convertToTypes is not supported"); + } + + @Override + public void bindListener(Matcher> typeMatcher, TypeListener listener) { + throw new UnsupportedOperationException("Method bindListener is not supported"); + } + + @Override + public void bindListener(Matcher> bindingMatcher, ProvisionListener... listeners) { + throw new UnsupportedOperationException("Method bindListener is not supported"); + } + + @Override + public Binder withSource(Object source) { + this.currentSource = source; + return this; + } + + @Override + public Binder skipSources(Class... classesToSkip) { + throw new UnsupportedOperationException("Method skipSources is not supported"); + } + + @Override + public PrivateBinder newPrivateBinder() { + throw new UnsupportedOperationException("Private bindings are not supported"); + } + + @Override + public void requireExplicitBindings() { + // no-op + } + + @Override + public void disableCircularProxies() { + // no-op + } + + @Override + public void requireAtInjectOnConstructors() { + // no-op + } + + @Override + public void requireExactBindingAnnotations() { + // no-op + } + + @Override + public void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner) { + // no-op + } + + private static void bindQualifier(RuntimeBeanDefinition.Builder builder, String beanName, Class beanQualifier, boolean primary) { + if (StringUtils.isNotEmpty(beanName)) { + builder.named(beanName); + } else { + if (beanQualifier != null) { + MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata(); + annotationMetadata.addAnnotation(beanQualifier.getName(), Map.of()); + builder.annotationMetadata(annotationMetadata); + builder.qualifier(Qualifiers.byAnnotation(annotationMetadata, beanQualifier)); + } else if (primary) { + builder.qualifier(PrimaryQualifier.INSTANCE); + } + } + } + + private static void validateBindingAnnotation(Class annotationType) { + Objects.requireNonNull(annotationType, "Annotation type cannot be null"); + if (annotationType.getAnnotation(BindingAnnotation.class) == null && annotationType.getAnnotation(Qualifier.class) == null) { + throw new IllegalArgumentException("Annotation type must be annotated itself with either @BindingAnnotation or jakarta.inject.Qualifier"); + } + } + + private static class AnnotatedConstantBindingBuilderImpl implements AnnotatedConstantBindingBuilder, ConstantBindingBuilder { + private Object value; + private Class annotationType; + private String name; + + @Override + public ConstantBindingBuilder annotatedWith(Class annotationType) { + Objects.requireNonNull(annotationType, "Annotation type cannot be null"); + this.annotationType = annotationType; + return this; + } + + @Override + public ConstantBindingBuilder annotatedWith(Annotation annotation) { + Objects.requireNonNull(annotation, "Annotation cannot be null"); + if (annotation instanceof Named named) { + this.name = named.value(); + } else { + this.annotationType = annotation.annotationType(); + } + return this; + } + + @Override + public void to(String value) { + this.value = value; + } + + @Override + public void to(int value) { + this.value = value; + } + + @Override + public void to(long value) { + this.value = value; + } + + @Override + public void to(boolean value) { + this.value = value; + } + + @Override + public void to(double value) { + this.value = value; + } + + @Override + public void to(float value) { + this.value = value; + } + + @Override + public void to(short value) { + this.value = value; + } + + @Override + public void to(char value) { + this.value = value; + } + + @Override + public void to(byte value) { + this.value = value; + } + + @Override + public void to(Class value) { + this.value = value; + } + + @Override + public > void to(E value) { + this.value = value; + } + + public RuntimeBeanDefinition build() { + Objects.requireNonNull(value, "Binding constant cannot be null, call one of the to(..) methods on the Guice binding"); + RuntimeBeanDefinition.Builder builder = RuntimeBeanDefinition.builder(value); + bindQualifier(builder, name, annotationType, false); + return builder.build(); + } + } + + private class LinkedBindingBuilderImpl implements LinkedBindingBuilder, AnnotatedBindingBuilder { + private final Argument beanType; + private boolean isSingleton; + private Class scope; + + private Supplier supplier; + private Class annotationType; + private String name; + private boolean primary; + + public LinkedBindingBuilderImpl(Argument argument) { + this.beanType = argument; + } + + @Override + public ScopedBindingBuilder to(Class implementation) { + BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, implementation)); + this.supplier = provider::get; + return this; + } + + @Override + public ScopedBindingBuilder to(TypeLiteral implementation) { + @SuppressWarnings("unchecked") + Argument argument = (Argument) Argument.of(implementation.getType()); + BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument)); + this.supplier = provider::get; + return this; + } + + @Override + public ScopedBindingBuilder to(Key targetKey) { + @SuppressWarnings("unchecked") + Argument argument = (Argument) Argument.of(targetKey.getTypeLiteral().getType()); + BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument)); + this.supplier = provider::get; + return this; + } + + @Override + public void toInstance(T instance) { + Objects.requireNonNull(instance, "Instance cannot be null"); + this.supplier = () -> instance; + } + + @Override + public ScopedBindingBuilder toProvider(Provider provider) { + Objects.requireNonNull(provider, "Provider cannot be null"); + this.supplier = provider::get; + return this; + } + + @Override + public ScopedBindingBuilder toProvider(jakarta.inject.Provider provider) { + Objects.requireNonNull(provider, "Provider cannot be null"); + this.supplier = provider::get; + return this; + } + + @Override + public ScopedBindingBuilder toProvider(Class> providerType) { + Objects.requireNonNull(providerType, "Provider type cannot be null"); + BeanProvider> provider = applicationContext.getBean(Argument.of(BeanProvider.class, providerType)); + this.supplier = () -> provider.get().get(); + return this; + } + + @Override + public ScopedBindingBuilder toProvider(TypeLiteral> providerType) { + Objects.requireNonNull(providerType, "Provider type cannot be null"); + @SuppressWarnings("unchecked") Argument> argument = + (Argument>) Argument.of(providerType.getType()); + BeanProvider> provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument)); + this.supplier = () -> provider.get().get(); + return this; + } + + @Override + public ScopedBindingBuilder toProvider(Key> providerKey) { + Objects.requireNonNull(providerKey, "Provider type cannot be null"); + return toProvider(providerKey.getTypeLiteral()); + } + + @Override + public ScopedBindingBuilder toConstructor(Constructor constructor) { + supplier = () -> InstantiationUtils.tryInstantiate(constructor) + .orElseThrow(() -> new BeanInstantiationException("Unable to instance bean via constructor: " + constructor)); + return this; + } + + @Override + public ScopedBindingBuilder toConstructor(Constructor constructor, TypeLiteral type) { + supplier = () -> InstantiationUtils.tryInstantiate(constructor) + .orElseThrow(() -> new BeanInstantiationException("Unable to instance bean via constructor: " + constructor)); + return this; + } + + @Override + public void in(Class scopeAnnotation) { + if (scopeAnnotation == Singleton.class || scopeAnnotation == jakarta.inject.Singleton.class) { + this.isSingleton = true; + } + this.scope = scopeAnnotation; + } + + @Override + public void in(Scope scope) { + if (scope == Scopes.SINGLETON) { + this.isSingleton = true; + } else if (scope != Scopes.NO_SCOPE) { + throw new IllegalArgumentException("Custom Guice scopes are not supported"); + } + } + + @Override + public void asEagerSingleton() { + this.isSingleton = true; + this.scope = Context.class; + } + + public RuntimeBeanDefinition build() { + Objects.requireNonNull(beanType, "Bean type cannot be null"); + if (supplier == null) { + // untargetted binding + Class javaType = beanType.getType(); + ImplementedBy implementedBy = javaType.getAnnotation(ImplementedBy.class); + ProvidedBy providedBy = javaType.getAnnotation(ProvidedBy.class); + if (implementedBy != null) { + if (!javaType.isAssignableFrom(implementedBy.value())) { + Message message = new Message(javaType, "@ImplementedBy annotation specifies a type that does not implement the declaring type"); + throw new com.google.inject.ConfigurationException( + List.of(message) + ); + } + to((Class) implementedBy.value()); + } else if (providedBy != null) { + toProvider((Class>) providedBy.value()); + } else { + if (!applicationContext.containsBean(javaType)) { + Message message = new Message(javaType, "Cannot create untargetted binding to type that is not itself declared a bean. " + + "Considering adding @Guice(classes=" + javaType.getSimpleName() + ".class) below your @Guice declaration."); + throw new com.google.inject.ConfigurationException( + List.of(message) + ); + } else { + BeanDefinition beanDefinition = applicationContext.getBeanDefinition(javaType); + toProvider(() -> applicationContext.getBean(beanDefinition)); + this.primary = true; + } + } + } + Objects.requireNonNull(supplier, "Bean Provider cannot be null, call one of the binding methods like to(instance)"); + + RuntimeBeanDefinition.Builder builder = RuntimeBeanDefinition + .builder(beanType, () -> { + try { + return supplier.get(); + } catch (NoSuchBeanException e) { + throw new CreationException(List.of( + new Message("Guice binding to bean [" + beanType.getTypeName() + "] cannot be resolved since no bean exists. " + + "Considering adding @Guice(classes=" + beanType.getSimpleName() + ".class) to the @Guice annotation definition."), + new Message(e.getMessage(), e) + )); + } + }); + + if (scope != null) { + builder.scope(scope); + } + if (isSingleton) { + builder.singleton(true); + } + builder.exposedTypes(beanType.getType()); + String beanName = name; + Class beanQualifier = annotationType; + bindQualifier(builder, beanName, beanQualifier, primary); + return builder + .build(); + } + + @Override + public LinkedBindingBuilder annotatedWith(Class annotationType) { + validateBindingAnnotation(annotationType); + this.annotationType = annotationType; + return this; + } + + @Override + public LinkedBindingBuilder annotatedWith(Annotation annotation) { + Objects.requireNonNull(annotation, "Annotation cannot be null"); + if (annotation instanceof Named named) { + this.name = named.value(); + return this; + } else { + return annotatedWith(annotation.annotationType()); + } + } + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/BindingAnnotationTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/BindingAnnotationTest.java new file mode 100644 index 0000000..90be795 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/BindingAnnotationTest.java @@ -0,0 +1,23 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import io.micronaut.context.annotation.Import; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false) +@Guice(modules = CreditCardProcessorModule.class) +@Import(classes = {PayPalCreditCardProcessor.class, CheckoutCreditCardProcessor.class}) +class BindingAnnotationTest { + @Inject @PayPal CreditCardProcessor paypalProcessor; + @Inject @GoogleCheckout CreditCardProcessor checkoutProcessor; + + @Test + void testInjectWithQualifiers() { + assertInstanceOf(CheckoutCreditCardProcessor.class, checkoutProcessor); + assertInstanceOf(PayPalCreditCardProcessor.class, paypalProcessor); + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CheckoutCreditCardProcessor.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CheckoutCreditCardProcessor.java new file mode 100644 index 0000000..7142388 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CheckoutCreditCardProcessor.java @@ -0,0 +1,4 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +public class CheckoutCreditCardProcessor implements CreditCardProcessor { +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CreditCardProcessor.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CreditCardProcessor.java new file mode 100644 index 0000000..952c978 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CreditCardProcessor.java @@ -0,0 +1,4 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +public interface CreditCardProcessor { +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CreditCardProcessorModule.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CreditCardProcessorModule.java new file mode 100644 index 0000000..19a4d42 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/CreditCardProcessorModule.java @@ -0,0 +1,22 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; + +final class CreditCardProcessorModule extends AbstractModule { + @Override + protected void configure() { + // This uses the optional `annotatedWith` clause in the `bind()` statement + bind(CreditCardProcessor.class) + .annotatedWith(PayPal.class) + .to(PayPalCreditCardProcessor.class); + } + + // This uses binding annotation with a @Provides method + @Provides + @GoogleCheckout + public CreditCardProcessor provideCheckoutProcessor( + CheckoutCreditCardProcessor processor) { + return processor; + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/GoogleCheckout.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/GoogleCheckout.java new file mode 100644 index 0000000..7b60b3b --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/GoogleCheckout.java @@ -0,0 +1,16 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +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.BindingAnnotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + + +@BindingAnnotation +@Target({ FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +public @interface GoogleCheckout {} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/PayPal.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/PayPal.java new file mode 100644 index 0000000..6e4caec --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/PayPal.java @@ -0,0 +1,15 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +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 jakarta.inject.Qualifier; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Qualifier +@Target({ FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +public @interface PayPal {} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/PayPalCreditCardProcessor.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/PayPalCreditCardProcessor.java new file mode 100644 index 0000000..22225c8 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/annotations/PayPalCreditCardProcessor.java @@ -0,0 +1,4 @@ +package io.micronaut.guice.doc.examples.bindings.annotations; + +public class PayPalCreditCardProcessor implements CreditCardProcessor { +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/instance/InstanceBindingTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/instance/InstanceBindingTest.java new file mode 100644 index 0000000..557b38f --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/instance/InstanceBindingTest.java @@ -0,0 +1,53 @@ +package io.micronaut.guice.doc.examples.bindings.instance; + +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 static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import jakarta.inject.Qualifier; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false) +@Guice(modules = InstanceBindingModule.class) +public class InstanceBindingTest { + @Inject @Named("JDBC URL") String jdbcUrl; + @Inject @Named("login timeout seconds") Integer timeout; + @Inject @HttpPort Integer port; + + @Test + void testInstanceBinding() { + assertEquals("jdbc:mysql://localhost/pizza", jdbcUrl); + assertEquals(10, timeout); + assertEquals(8080, port); + } +} + +class InstanceBindingModule extends AbstractModule { + @Override + protected void configure() { + bind(String.class) + .annotatedWith(Names.named("JDBC URL")) + .toInstance("jdbc:mysql://localhost/pizza"); + bind(Integer.class) + .annotatedWith(Names.named("login timeout seconds")) + .toInstance(10); + bindConstant() + .annotatedWith(HttpPort.class) + .to(8080); + } +} + +@Qualifier +@Target({ FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +@interface HttpPort {} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/DatabaseTransactionLog.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/DatabaseTransactionLog.java new file mode 100644 index 0000000..94d5ac3 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/DatabaseTransactionLog.java @@ -0,0 +1,9 @@ +package io.micronaut.guice.doc.examples.bindings.linked; + +import com.google.inject.Inject; + +public class DatabaseTransactionLog implements TransactionLog { + @Inject + public DatabaseTransactionLog() { + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/LinkedBindingNotActiveTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/LinkedBindingNotActiveTest.java new file mode 100644 index 0000000..63a7707 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/LinkedBindingNotActiveTest.java @@ -0,0 +1,19 @@ +package io.micronaut.guice.doc.examples.bindings.linked; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +@MicronautTest +public class LinkedBindingNotActiveTest { + @Inject + ApplicationContext applicationContext; + + @Test + void testNotActive() { + assertFalse(applicationContext.containsBean(BillingModule.class)); + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/LinkedBindingTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/LinkedBindingTest.java new file mode 100644 index 0000000..c8ff7a3 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/LinkedBindingTest.java @@ -0,0 +1,36 @@ +package io.micronaut.guice.doc.examples.bindings.linked; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false, environments = "linked") +@Guice( + modules = BillingModule.class, + environments = "linked" +) +class LinkedBindingTest { + @Test + void testLinkedBinding(TransactionLog transactionLog, DatabaseTransactionLog databaseTransactionLog) { + Assertions.assertNotNull(transactionLog); + Assertions.assertNotNull(databaseTransactionLog); + Assertions.assertInstanceOf(MySqlDatabaseTransactionLog.class, transactionLog); + Assertions.assertInstanceOf(MySqlDatabaseTransactionLog.class, databaseTransactionLog); + } +} + +class BillingModule extends AbstractModule { + @Provides + public TransactionLog provideTransactionLog(DatabaseTransactionLog databaseTransactionLog) { + return databaseTransactionLog; + } + + @Provides + public DatabaseTransactionLog provideDatabaseTransactionLog(MySqlDatabaseTransactionLog impl) { + return impl; + } +} + diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/MySqlDatabaseTransactionLog.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/MySqlDatabaseTransactionLog.java new file mode 100644 index 0000000..c5cae9e --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/MySqlDatabaseTransactionLog.java @@ -0,0 +1,9 @@ +package io.micronaut.guice.doc.examples.bindings.linked; + +import com.google.inject.Inject; + +class MySqlDatabaseTransactionLog extends DatabaseTransactionLog { + @Inject + public MySqlDatabaseTransactionLog() { + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/TransactionLog.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/TransactionLog.java new file mode 100644 index 0000000..535923d --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/linked/TransactionLog.java @@ -0,0 +1,4 @@ +package io.micronaut.guice.doc.examples.bindings.linked; + +public interface TransactionLog { +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/multi/MultiBinderTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/multi/MultiBinderTest.java new file mode 100644 index 0000000..06a9b2e --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/multi/MultiBinderTest.java @@ -0,0 +1,57 @@ +package io.micronaut.guice.doc.examples.bindings.multi; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.multibindings.Multibinder; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import java.net.URI; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false) +@Guice( + modules = FlickrPluginModule.class, + classes = FlickrPhotoSummarizer.class +) +public class MultiBinderTest { + @Inject + Set uriSummarizers; + + @Test + void testMultiBindings() { + Assertions.assertNotNull(uriSummarizers); + Assertions.assertEquals(2, uriSummarizers.size()); + Assertions.assertTrue(uriSummarizers.stream().anyMatch(s -> s instanceof FlickrPhotoSummarizer)); + Assertions.assertTrue(uriSummarizers.stream().anyMatch(s -> s instanceof GooglePhotoSummarizer)); + } +} + +class FlickrPluginModule extends AbstractModule { + public void configure() { + Multibinder uriBinder = Multibinder.newSetBinder(binder(), UriSummarizer.class); + uriBinder.addBinding().to(FlickrPhotoSummarizer.class); + uriBinder.addBinding().toInstance(new GooglePhotoSummarizer()); + } +} +interface UriSummarizer { + /** + * Returns a short summary of the URI, or null if this summarizer doesn't + * know how to summarize the URI. + */ + String summarize(URI uri); +} + +class FlickrPhotoSummarizer implements UriSummarizer { + @Override + public String summarize(URI uri) { + return "flickr"; + } +} +class GooglePhotoSummarizer implements UriSummarizer { + @Override + public String summarize(URI uri) { + return "google"; + } +} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/provider/ProviderBindingTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/provider/ProviderBindingTest.java new file mode 100644 index 0000000..640fc84 --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/provider/ProviderBindingTest.java @@ -0,0 +1,51 @@ +package io.micronaut.guice.doc.examples.bindings.provider; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Provider; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.test.annotation.MockBean; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import java.sql.Connection; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@MicronautTest(startApplication = false) +@Guice(modules = BillingModule.class) +public class ProviderBindingTest { + @MockBean Connection connection = Mockito.mock(Connection.class); + @Inject TransactionLog transactionLog; + + @Test + void testProviderInjection() { + assertInstanceOf(DatabaseTransactionLog.class, transactionLog); + DatabaseTransactionLog dtl = (DatabaseTransactionLog) transactionLog; + Assertions.assertNotNull(dtl.connection()); + } +} + +class BillingModule extends AbstractModule { + @Override + protected void configure() { + bind(TransactionLog.class) + .toProvider(DatabaseTransactionLogProvider.class); + } +} +interface TransactionLog {} +class DatabaseTransactionLogProvider implements Provider { + private final Connection connection; + + @Inject + public DatabaseTransactionLogProvider(Connection connection) { + this.connection = connection; + } + + public TransactionLog get() { + return new DatabaseTransactionLog(connection); + } +} + +record DatabaseTransactionLog(Connection connection) implements TransactionLog {} diff --git a/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/untargetted/UntargettedBindingTest.java b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/untargetted/UntargettedBindingTest.java new file mode 100644 index 0000000..be54d9b --- /dev/null +++ b/micronaut-guice/src/test/java/io/micronaut/guice/doc/examples/bindings/untargetted/UntargettedBindingTest.java @@ -0,0 +1,55 @@ +package io.micronaut.guice.doc.examples.bindings.untargetted; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.google.inject.AbstractModule; +import com.google.inject.ImplementedBy; +import com.google.inject.Singleton; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Import; +import io.micronaut.guice.annotation.Guice; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false) +@Guice(modules = MyModule.class) +@Import(classes = MyConcreteClass.class) +public class UntargettedBindingTest { + @Inject MyInterface myInterface1; + @Inject MyInterface myInterface2; + @Inject MyConcreteClass myConcreteClass; + @Inject AnotherConcreteClass first; + @Inject AnotherConcreteClass second; + @Test + void testUntargetted() { + assertInstanceOf(MyImplementation.class, myInterface1); + assertSame(myInterface1, myInterface2); + assertSame(first, second); + assertNotNull(myConcreteClass); + } +} + +class MyModule extends AbstractModule { + @Override + protected void configure() { + bind(MyConcreteClass.class); + bind(MyInterface.class); + bind(AnotherConcreteClass.class).in(Singleton.class); + } +} + +class MyConcreteClass { +} + +@ImplementedBy(MyImplementation.class) +interface MyInterface { +} + +@Singleton +class MyImplementation implements MyInterface {} + +@Bean +class AnotherConcreteClass {} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 05b6d64..0000000 --- a/settings.gradle +++ /dev/null @@ -1,21 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -plugins { - id 'io.micronaut.build.shared.settings' version '6.7.1' -} - -rootProject.name = 'project-template-parent' - -include 'project-template' -include 'project-template-bom' - -enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS' - -micronautBuild { - importMicronautCatalog() -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d4f9c3e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id("io.micronaut.build.shared.settings") version "6.6.1" +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +rootProject.name = "guice-parent" + +include("micronaut-guice-bom") +include("micronaut-guice") +include("micronaut-guice-annotation") +include("micronaut-guice-processor") + +val micronautVersion = providers.gradleProperty("micronautVersion") + +configure { + useStandardizedProjectNames.set(true) + importMicronautCatalog() +}