From e40925e762dc8ead316313cdbd8e17574c861dff Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 24 Jan 2024 21:24:21 +0100 Subject: [PATCH 1/3] [MNG-7954] New dependency injection mechanism --- api/maven-api-core/pom.xml | 4 +- .../maven/api/di/MojoExecutionScoped.java | 4 +- .../apache/maven/api/di/SessionScoped.java | 4 +- .../maven/api/plugin/annotations/Execute.java | 2 +- .../maven/api/plugin/annotations/Mojo.java | 4 +- .../api/plugin/annotations/Parameter.java | 2 +- api/maven-api-di/pom.xml | 40 ++ .../java/org/apache/maven/api/di/Inject.java | 31 ++ .../java/org/apache/maven/api/di/Named.java | 32 ++ .../org/apache/maven/api/di/Priority.java | 34 ++ .../org/apache/maven/api/di/Provides.java | 34 ++ .../org/apache/maven/api/di/Qualifier.java | 31 ++ .../java/org/apache/maven/api/di/Scope.java | 31 ++ .../org/apache/maven/api/di/Singleton.java | 29 ++ .../java/org/apache/maven/api/di/Typed.java | 33 ++ api/pom.xml | 1 + maven-core/pom.xml | 4 + .../internal/MojoExecutionScopeModule.java | 2 +- .../internal/DefaultMavenPluginManager.java | 316 +++++++----- .../scope/internal/SessionScopeModule.java | 2 +- .../session/scope/SessionScopeProxyTest.java | 2 +- maven-di/pom.xml | 39 ++ .../java/org/apache/maven/di/Injector.java | 48 ++ .../main/java/org/apache/maven/di/Key.java | 161 ++++++ .../main/java/org/apache/maven/di/Scope.java | 27 + .../org/apache/maven/di/impl/Binding.java | 173 +++++++ .../maven/di/impl/BindingInitializer.java | 58 +++ .../org/apache/maven/di/impl/DIException.java | 44 ++ .../apache/maven/di/impl/InjectorImpl.java | 305 ++++++++++++ .../apache/maven/di/impl/ReflectionUtils.java | 360 +++++++++++++ .../org/apache/maven/di/impl/TypeUtils.java | 379 ++++++++++++++ .../java/org/apache/maven/di/impl/Types.java | 471 ++++++++++++++++++ .../java/org/apache/maven/di/impl/Utils.java | 53 ++ .../processor/IndexAnnotationProcessor.java | 107 ++++ .../javax.annotation.processing.Processor | 1 + .../java/org/apache/maven/di/impl/DITest.java | 277 ++++++++++ .../java/org/apache/maven/di/impl/TypeT.java | 83 +++ .../apache/maven/di/impl/TypeUtilsTest.java | 157 ++++++ pom.xml | 11 + 39 files changed, 3261 insertions(+), 135 deletions(-) create mode 100644 api/maven-api-di/pom.xml create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Inject.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Named.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Priority.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Provides.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Qualifier.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Scope.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Singleton.java create mode 100644 api/maven-api-di/src/main/java/org/apache/maven/api/di/Typed.java create mode 100644 maven-di/pom.xml create mode 100644 maven-di/src/main/java/org/apache/maven/di/Injector.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/Key.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/Scope.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/Binding.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/BindingInitializer.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/DIException.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/TypeUtils.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/Types.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/impl/Utils.java create mode 100644 maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java create mode 100644 maven-di/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 maven-di/src/test/java/org/apache/maven/di/impl/DITest.java create mode 100644 maven-di/src/test/java/org/apache/maven/di/impl/TypeT.java create mode 100644 maven-di/src/test/java/org/apache/maven/di/impl/TypeUtilsTest.java diff --git a/api/maven-api-core/pom.xml b/api/maven-api-core/pom.xml index 33463ae51d9c..49d3429c7ad4 100644 --- a/api/maven-api-core/pom.xml +++ b/api/maven-api-core/pom.xml @@ -52,8 +52,8 @@ maven-api-plugin - jakarta.inject - jakarta.inject-api + org.apache.maven + maven-api-di diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/di/MojoExecutionScoped.java b/api/maven-api-core/src/main/java/org/apache/maven/api/di/MojoExecutionScoped.java index 2fcbe07991ce..b12f9a4159bb 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/di/MojoExecutionScoped.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/di/MojoExecutionScoped.java @@ -22,8 +22,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import jakarta.inject.Scope; - import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -32,6 +30,8 @@ * Indicates that the annotated bean has a lifespan limited to a given mojo execution, * which means each mojo execution will result in a different instance being injected. * + * TODO: this is currently not implemented + * * @since 4.0.0 */ @Scope diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/di/SessionScoped.java b/api/maven-api-core/src/main/java/org/apache/maven/api/di/SessionScoped.java index d5e9a0ddbe53..ec4d5a8ea7d4 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/di/SessionScoped.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/di/SessionScoped.java @@ -22,8 +22,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import jakarta.inject.Scope; - import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -32,6 +30,8 @@ * Indicates that annotated component should be instantiated before session execution starts * and discarded after session execution completes. * + * TODO: this is currently not implemented + * * @since 4.0.0 */ @Scope diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Execute.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Execute.java index 1cfdfc6c775a..903d224f3811 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Execute.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Execute.java @@ -35,7 +35,7 @@ */ @Experimental @Documented -@Retention(RetentionPolicy.CLASS) +@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface Execute { diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java index bc4fcc479bba..12b8344da7a1 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java @@ -27,6 +27,7 @@ import org.apache.maven.api.annotations.Experimental; import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.di.Qualifier; /** * This annotation will mark your class as a Mojo (ie. goal in a Maven plugin). @@ -38,9 +39,10 @@ */ @Experimental @Documented -@Retention(RetentionPolicy.CLASS) +@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited +@Qualifier public @interface Mojo { /** * goal name (required). diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Parameter.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Parameter.java index 415f6a507d28..64b129ae4ebe 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Parameter.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Parameter.java @@ -41,7 +41,7 @@ */ @Experimental @Documented -@Retention(RetentionPolicy.CLASS) +@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD}) @Inherited public @interface Parameter { diff --git a/api/maven-api-di/pom.xml b/api/maven-api-di/pom.xml new file mode 100644 index 000000000000..c20d05e05437 --- /dev/null +++ b/api/maven-api-di/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + + org.apache.maven + maven-api + 4.0.0-alpha-13-SNAPSHOT + + + maven-api-di + Maven 4 API :: Dependency Injection + Maven 4 API - Dependency Injection + + + + org.apache.maven + maven-api-meta + + + + diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Inject.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Inject.java new file mode 100644 index 000000000000..d31a4d11fbb0 --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Inject.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, CONSTRUCTOR, METHOD}) +@Retention(RUNTIME) +@Documented +public @interface Inject {} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Named.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Named.java new file mode 100644 index 000000000000..d38dfadc2e54 --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Named.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Retention(RUNTIME) +@Documented +public @interface Named { + String value() default ""; +} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Priority.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Priority.java new file mode 100644 index 000000000000..f2eae991c5c9 --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Priority.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +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.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Documented +public @interface Priority { + int value(); +} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Provides.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Provides.java new file mode 100644 index 000000000000..2a782505ebb3 --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Provides.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +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; + +/** + * Can be used on a static method to provide a bean. + */ +@Target(METHOD) +@Retention(RUNTIME) +@Documented +public @interface Provides {} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Qualifier.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Qualifier.java new file mode 100644 index 000000000000..863618280d20 --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Qualifier.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +import java.lang.annotation.Documented; +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; + +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +@Documented +public @interface Qualifier {} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Scope.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Scope.java new file mode 100644 index 000000000000..80b50b5d210d --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Scope.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +import java.lang.annotation.Documented; +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; + +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +@Documented +public @interface Scope {} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Singleton.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Singleton.java new file mode 100644 index 000000000000..79b440ce767a --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Singleton.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Scope +@Documented +@Retention(RUNTIME) +public @interface Singleton {} diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Typed.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Typed.java new file mode 100644 index 000000000000..b2c78606c1ca --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Typed.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.di; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, METHOD, TYPE}) +@Retention(RUNTIME) +@Documented +public @interface Typed { + Class[] value() default {}; +} diff --git a/api/pom.xml b/api/pom.xml index 426e69eb7a4c..6fc32f33324b 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -33,6 +33,7 @@ maven-api-meta + maven-api-di maven-api-xml maven-api-model maven-api-plugin diff --git a/maven-core/pom.xml b/maven-core/pom.xml index 0ccd1f7df6d1..fcd9cbf90186 100644 --- a/maven-core/pom.xml +++ b/maven-core/pom.xml @@ -33,6 +33,10 @@ under the License. + + org.apache.maven + maven-di + org.apache.maven maven-model diff --git a/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java b/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java index c79182839c9d..149b94f97c5d 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java +++ b/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java @@ -37,7 +37,7 @@ public MojoExecutionScopeModule(MojoExecutionScope scope) { @Override protected void configure() { bindScope(MojoExecutionScoped.class, scope); - bindScope(org.apache.maven.api.di.MojoExecutionScoped.class, scope); + // bindScope(org.apache.maven.api.di.MojoExecutionScoped.class, scope); bind(MojoExecutionScope.class).toInstance(scope); bind(MavenProject.class) .toProvider(MojoExecutionScope.seededKeyProvider()) diff --git a/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java index 22019842f122..7292c8c94231 100644 --- a/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java +++ b/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java @@ -22,31 +22,25 @@ import javax.inject.Named; import javax.inject.Singleton; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; +import java.io.*; +import java.lang.annotation.Annotation; import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.zip.ZipEntry; -import com.google.inject.AbstractModule; -import com.google.inject.name.Names; import org.apache.maven.RepositoryUtils; +import org.apache.maven.api.Project; +import org.apache.maven.api.Session; import org.apache.maven.api.xml.XmlNode; import org.apache.maven.artifact.Artifact; import org.apache.maven.classrealm.ClassRealmManager; +import org.apache.maven.di.Injector; import org.apache.maven.execution.MavenSession; import org.apache.maven.execution.scope.internal.MojoExecutionScope; import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule; +import org.apache.maven.internal.impl.DefaultLog; import org.apache.maven.internal.impl.DefaultMojoExecution; import org.apache.maven.internal.impl.InternalSession; import org.apache.maven.internal.xml.XmlPlexusConfiguration; @@ -429,29 +423,6 @@ private void discoverPluginComponents( ((DefaultPlexusContainer) container) .discoverComponents( pluginRealm, - new AbstractModule() { - @Override - protected void configure() { - if (pluginDescriptor != null) { - for (MojoDescriptor mojo : pluginDescriptor.getMojos()) { - if (mojo.isV4Api()) { - try { - mojo.setRealm(pluginRealm); - Class cl = mojo.getImplementationClass(); - if (cl == null) { - cl = pluginRealm.loadClass(mojo.getImplementation()); - } - bind(org.apache.maven.api.plugin.Mojo.class) - .annotatedWith(Names.named(mojo.getId())) - .to((Class) cl); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unable to load mojo class", e); - } - } - } - } - } - }, new SessionScopeModule(container.lookup(SessionScope.class)), new MojoExecutionScopeModule(container.lookup(MojoExecutionScope.class)), new PluginConfigurationModule(plugin.getDelegate())); @@ -528,115 +499,189 @@ public T getConfiguredMojo(Class mojoInterface, MavenSession session, Moj Thread.currentThread().setContextClassLoader(pluginRealm); try { - T mojo; + if (mojoDescriptor.isV4Api()) { + return loadV4Mojo(mojoInterface, session, mojoExecution, mojoDescriptor, pluginDescriptor, pluginRealm); + } else { + return loadV3Mojo(mojoInterface, session, mojoExecution, mojoDescriptor, pluginDescriptor, pluginRealm); + } + } finally { + Thread.currentThread().setContextClassLoader(oldClassLoader); + container.setLookupRealm(oldLookupRealm); + } + } - try { - mojo = container.lookup(mojoInterface, mojoDescriptor.getRoleHint()); - } catch (ComponentLookupException e) { - Throwable cause = e.getCause(); - while (cause != null - && !(cause instanceof LinkageError) - && !(cause instanceof ClassNotFoundException)) { - cause = cause.getCause(); - } + private T loadV4Mojo( + Class mojoInterface, + MavenSession session, + MojoExecution mojoExecution, + MojoDescriptor mojoDescriptor, + PluginDescriptor pluginDescriptor, + ClassRealm pluginRealm) + throws PluginContainerException, PluginConfigurationException { + T mojo; + + InternalSession sessionV4 = InternalSession.from(session.getSession()); + Project project = sessionV4.getProject(session.getCurrentProject()); + org.apache.maven.api.MojoExecution execution = new DefaultMojoExecution(sessionV4, mojoExecution); + org.apache.maven.api.plugin.Log log = new DefaultLog( + LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getFullGoalName())); + try { + Set classes = new HashSet<>(); + try (InputStream is = pluginRealm.getResourceAsStream("META-INF/maven/org.apache.maven.api.di.Inject"); + BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) { + reader.lines().forEach(classes::add); + } + Injector injector = Injector.create(); + // Add known classes + // TODO: get those from the existing plexus scopes ? + injector.bindInstance(Session.class, sessionV4); + injector.bindInstance(Project.class, project); + injector.bindInstance(org.apache.maven.api.MojoExecution.class, execution); + injector.bindInstance(org.apache.maven.api.plugin.Log.class, log); + // Add plugin classes + for (String className : classes) { + Class clazz = pluginRealm.loadClass(className); + injector.bindImplicit(clazz); + } + mojo = mojoInterface.cast(injector.getInstance(mojoDescriptor.getImplementationClass())); - if ((cause instanceof NoClassDefFoundError) || (cause instanceof ClassNotFoundException)) { - ByteArrayOutputStream os = new ByteArrayOutputStream(1024); - PrintStream ps = new PrintStream(os); - ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '" - + pluginDescriptor.getId() + "'. A required class is missing: " - + cause.getMessage()); - pluginRealm.display(ps); - - throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause); - } else if (cause instanceof LinkageError) { - ByteArrayOutputStream os = new ByteArrayOutputStream(1024); - PrintStream ps = new PrintStream(os); - ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '" - + pluginDescriptor.getId() + "' due to an API incompatibility: " - + e.getClass().getName() + ": " + cause.getMessage()); - pluginRealm.display(ps); - - throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause); - } + } catch (Exception e) { + throw new PluginContainerException(mojoDescriptor, pluginRealm, "Unable to lookup Mojo", e); + } - throw new PluginContainerException( - mojoDescriptor, - pluginRealm, - "Unable to load the mojo '" + mojoDescriptor.getGoal() - + "' (or one of its required components) from the plugin '" - + pluginDescriptor.getId() + "'", - e); - } + XmlNode dom = mojoExecution.getConfiguration() != null + ? mojoExecution.getConfiguration().getDom() + : null; - if (mojo instanceof ContextEnabled) { - MavenProject project = session.getCurrentProject(); + PlexusConfiguration pomConfiguration; - Map pluginContext = session.getPluginContext(pluginDescriptor, project); + if (dom == null) { + pomConfiguration = new DefaultPlexusConfiguration("configuration"); + } else { + pomConfiguration = XmlPlexusConfiguration.toPlexusConfiguration(dom); + } - if (pluginContext != null) { - pluginContext.put("project", project); + ExpressionEvaluator expressionEvaluator = + new PluginParameterExpressionEvaluatorV4(sessionV4, project, execution); - pluginContext.put("pluginDescriptor", pluginDescriptor); + for (MavenPluginConfigurationValidator validator : configurationValidators) { + validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator); + } - ((ContextEnabled) mojo).setPluginContext(pluginContext); - } - } + populateMojoExecutionFields( + mojo, + mojoExecution.getExecutionId(), + mojoDescriptor, + pluginRealm, + pomConfiguration, + expressionEvaluator); + + return mojo; + } + + private T loadV3Mojo( + Class mojoInterface, + MavenSession session, + MojoExecution mojoExecution, + MojoDescriptor mojoDescriptor, + PluginDescriptor pluginDescriptor, + ClassRealm pluginRealm) + throws PluginContainerException, PluginConfigurationException { + T mojo; - if (mojo instanceof Mojo) { - Logger mojoLogger = LoggerFactory.getLogger(mojoDescriptor.getImplementation()); - ((Mojo) mojo).setLog(new MojoLogWrapper(mojoLogger)); + try { + mojo = container.lookup(mojoInterface, mojoDescriptor.getRoleHint()); + } catch (ComponentLookupException e) { + Throwable cause = e.getCause(); + while (cause != null && !(cause instanceof LinkageError) && !(cause instanceof ClassNotFoundException)) { + cause = cause.getCause(); } - if (mojo instanceof Contextualizable) { - pluginValidationManager.reportPluginMojoValidationIssue( - PluginValidationManager.IssueLocality.EXTERNAL, - session, - mojoDescriptor, - mojo.getClass(), - "Mojo implements `Contextualizable` interface from Plexus Container, which is EOL."); + if ((cause instanceof NoClassDefFoundError) || (cause instanceof ClassNotFoundException)) { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os); + ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '" + + pluginDescriptor.getId() + "'. A required class is missing: " + + cause.getMessage()); + pluginRealm.display(ps); + + throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause); + } else if (cause instanceof LinkageError) { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os); + ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '" + + pluginDescriptor.getId() + "' due to an API incompatibility: " + + e.getClass().getName() + ": " + cause.getMessage()); + pluginRealm.display(ps); + + throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause); } - XmlNode dom = mojoExecution.getConfiguration() != null - ? mojoExecution.getConfiguration().getDom() - : null; + throw new PluginContainerException( + mojoDescriptor, + pluginRealm, + "Unable to load the mojo '" + mojoDescriptor.getGoal() + + "' (or one of its required components) from the plugin '" + + pluginDescriptor.getId() + "'", + e); + } - PlexusConfiguration pomConfiguration; + if (mojo instanceof ContextEnabled) { + MavenProject project = session.getCurrentProject(); - if (dom == null) { - pomConfiguration = new DefaultPlexusConfiguration("configuration"); - } else { - pomConfiguration = XmlPlexusConfiguration.toPlexusConfiguration(dom); - } + Map pluginContext = session.getPluginContext(pluginDescriptor, project); - ExpressionEvaluator expressionEvaluator; - InternalSession sessionV4 = InternalSession.from(session.getSession()); - if (mojoDescriptor.isV4Api()) { - expressionEvaluator = new PluginParameterExpressionEvaluatorV4( - sessionV4, - sessionV4.getProject(session.getCurrentProject()), - new DefaultMojoExecution(sessionV4, mojoExecution)); - } else { - expressionEvaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); - } + if (pluginContext != null) { + pluginContext.put("project", project); + + pluginContext.put("pluginDescriptor", pluginDescriptor); - for (MavenPluginConfigurationValidator validator : configurationValidators) { - validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator); + ((ContextEnabled) mojo).setPluginContext(pluginContext); } + } + + if (mojo instanceof Mojo) { + Logger mojoLogger = LoggerFactory.getLogger(mojoDescriptor.getImplementation()); + ((Mojo) mojo).setLog(new MojoLogWrapper(mojoLogger)); + } - populateMojoExecutionFields( - mojo, - mojoExecution.getExecutionId(), + if (mojo instanceof Contextualizable) { + pluginValidationManager.reportPluginMojoValidationIssue( + PluginValidationManager.IssueLocality.EXTERNAL, + session, mojoDescriptor, - pluginRealm, - pomConfiguration, - expressionEvaluator); + mojo.getClass(), + "Mojo implements `Contextualizable` interface from Plexus Container, which is EOL."); + } - return mojo; - } finally { - Thread.currentThread().setContextClassLoader(oldClassLoader); - container.setLookupRealm(oldLookupRealm); + XmlNode dom = mojoExecution.getConfiguration() != null + ? mojoExecution.getConfiguration().getDom() + : null; + + PlexusConfiguration pomConfiguration; + + if (dom == null) { + pomConfiguration = new DefaultPlexusConfiguration("configuration"); + } else { + pomConfiguration = XmlPlexusConfiguration.toPlexusConfiguration(dom); } + + InternalSession sessionV4 = InternalSession.from(session.getSession()); + ExpressionEvaluator expressionEvaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); + + for (MavenPluginConfigurationValidator validator : configurationValidators) { + validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator); + } + + populateMojoExecutionFields( + mojo, + mojoExecution.getExecutionId(), + mojoDescriptor, + pluginRealm, + pomConfiguration, + expressionEvaluator); + + return mojo; } private void populateMojoExecutionFields( @@ -892,4 +937,29 @@ private List resolveExtensionArtifacts( pluginDependenciesResolver.resolvePlugin(extensionPlugin, null, null, repositories, session); return toMavenArtifacts(root); } + + static class NamedImpl implements Named { + private final String value; + + NamedImpl(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + + @SuppressWarnings("checkstyle:MagicNumber") + public int hashCode() { + return 127 * "value".hashCode() ^ this.value.hashCode(); + } + + public boolean equals(Object o) { + return o instanceof Named && this.value.equals(((Named) o).value()); + } + + public Class annotationType() { + return Named.class; + } + } } diff --git a/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScopeModule.java b/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScopeModule.java index 5adcd0bd251d..f87334439920 100644 --- a/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScopeModule.java +++ b/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScopeModule.java @@ -46,7 +46,7 @@ public SessionScopeModule(SessionScope scope) { @Override protected void configure() { bindScope(SessionScoped.class, scope); - bindScope(org.apache.maven.api.di.SessionScoped.class, scope); + // bindScope(org.apache.maven.api.di.SessionScoped.class, scope); bind(SessionScope.class).toInstance(scope); bind(MavenSession.class) diff --git a/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java b/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java index fd3efe8aca7a..5988c64c9e32 100644 --- a/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java +++ b/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java @@ -23,8 +23,8 @@ import javax.inject.Singleton; import com.google.inject.OutOfScopeException; +import org.apache.maven.SessionScoped; import org.apache.maven.api.Session; -import org.apache.maven.api.di.SessionScoped; import org.apache.maven.session.scope.internal.SessionScope; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; diff --git a/maven-di/pom.xml b/maven-di/pom.xml new file mode 100644 index 000000000000..9efddf76d3f7 --- /dev/null +++ b/maven-di/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.apache.maven + maven + 4.0.0-alpha-13-SNAPSHOT + + + maven-di + Maven Dependency Injection + Provides the implementation for the Dependency Injection mechanism in Maven + + + + org.apache.maven + maven-api-di + + + + diff --git a/maven-di/src/main/java/org/apache/maven/di/Injector.java b/maven-di/src/main/java/org/apache/maven/di/Injector.java new file mode 100644 index 000000000000..d961b8092c28 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/Injector.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di; + +import java.lang.annotation.Annotation; + +import org.apache.maven.di.impl.InjectorImpl; + +public interface Injector { + + // + // Builder API + // + + static Injector create() { + return new InjectorImpl(); + } + + Injector bindScope(Class scopeAnnotation, Scope scope); + + Injector bindImplicit(Class cls); + + Injector bindInstance(Class cls, T instance); + + // + // Bean access + // + + T getInstance(Class key); + + T getInstance(Key key); +} diff --git a/maven-di/src/main/java/org/apache/maven/di/Key.java b/maven-di/src/main/java/org/apache/maven/di/Key.java new file mode 100644 index 000000000000..cdf9bc040c7d --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/Key.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Objects; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.di.impl.ReflectionUtils; +import org.apache.maven.di.impl.TypeUtils; +import org.apache.maven.di.impl.Types; +import org.apache.maven.di.impl.Utils; + +/** + * The key defines an identity of a binding. In any DI, a key is usually a type of the object along + * with some optional tag to distinguish between bindings which make objects of the same type. + *

+ * In ActiveJ Inject, a key is also a type token - special abstract class that can store type information + * with the shortest syntax possible in Java. + *

+ * For example, to create a key of type Map<String, List<Integer>>, you can just use + * this syntax: new Key<Map<String, List<Integer>>>(){}. + *

+ * If your types are not known at compile time, you can use {@link Types#parameterizedType} to make a + * parameterized type and give it to a {@link #ofType Key.ofType} constructor. + * + * @param binding type + */ +public abstract class Key { + private final Type type; + private final @Nullable Object qualifier; + + private int hash; + + protected Key() { + this(null); + } + + protected Key(@Nullable Object qualifier) { + this.type = TypeUtils.simplifyType(getTypeParameter()); + this.qualifier = qualifier; + } + + protected Key(Type type, @Nullable Object qualifier) { + this.type = TypeUtils.simplifyType(type); + this.qualifier = qualifier; + } + + static final class KeyImpl extends Key { + KeyImpl(Type type, Object qualifier) { + super(type, qualifier); + } + } + + public static Key of(Class type) { + return new KeyImpl<>(type, null); + } + + public static Key of(Class type, @Nullable Object qualifier) { + return new KeyImpl<>(type, qualifier); + } + + public static Key ofType(Type type) { + return new KeyImpl<>(type, null); + } + + public static Key ofType(Type type, @Nullable Object qualifier) { + return new KeyImpl<>(type, qualifier); + } + + private Type getTypeParameter() { + // this cannot possibly fail so not even a check here + Type typeArgument = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + Object outerInstance = ReflectionUtils.getOuterClassInstance(this); + // // the outer instance is null in static context + return outerInstance != null + ? Types.bind(typeArgument, Types.getAllTypeBindings(outerInstance.getClass())) + : typeArgument; + } + + public Type getType() { + return type; + } + + /** + * A shortcut for {@link Types#getRawType(Type)}(key.getType()). + * Also casts the result to a properly parameterized class. + */ + @SuppressWarnings("unchecked") + public Class getRawType() { + return (Class) Types.getRawType(type); + } + + /** + * Returns a type parameter of the underlying type wrapped as a key with no qualifier. + * + * @throws IllegalStateException when underlying type is not a parameterized one. + */ + public Key getTypeParameter(int index) { + if (type instanceof ParameterizedType) { + return new KeyImpl<>(((ParameterizedType) type).getActualTypeArguments()[index], null); + } + throw new IllegalStateException("Expected type from key " + getDisplayString() + " to be parameterized"); + } + + public @Nullable Object getQualifier() { + return qualifier; + } + + /** + * Returns an underlying type with display string formatting (package names stripped) + * and prepended qualifier display string if this key has a qualifier. + */ + public String getDisplayString() { + return (qualifier != null ? Utils.getDisplayString(qualifier) + " " : "") + + ReflectionUtils.getDisplayName(type); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Key)) { + return false; + } + Key that = (Key) o; + return type.equals(that.type) && Objects.equals(qualifier, that.qualifier); + } + + @Override + public int hashCode() { + int hashCode = hash; + if (hashCode == 0) { + hash = 31 * type.hashCode() + (qualifier == null ? 0 : qualifier.hashCode()); + } + return hash; + } + + @Override + public String toString() { + return (qualifier != null ? qualifier + " " : "") + type.getTypeName(); + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/Scope.java b/maven-di/src/main/java/org/apache/maven/di/Scope.java new file mode 100644 index 000000000000..c05334f319f2 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/Scope.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di; + +import java.lang.annotation.Annotation; +import java.util.function.Supplier; + +public interface Scope { + + Supplier scope(Key key, Annotation scope, Supplier unscoped); +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java b/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java new file mode 100644 index 000000000000..80d9bcbf5f57 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.di.Key; + +import static java.util.stream.Collectors.joining; + +public abstract class Binding { + private final Set> dependencies; + private Annotation scope; + private int priority; + private Key originalKey; + + protected Binding(Key originalKey, Set> dependencies) { + this(originalKey, dependencies, null, 0); + } + + protected Binding(Key originalKey, Set> dependencies, Annotation scope, int priority) { + this.originalKey = originalKey; + this.dependencies = dependencies; + this.scope = scope; + this.priority = priority; + } + + public static Binding toInstance(T instance) { + return new BindingToInstance<>(instance); + } + + public static Binding to(TupleConstructorN constructor, Class[] types) { + return Binding.to(constructor, Stream.of(types).map(Key::of).toArray(Key[]::new)); + } + + public static Binding to(TupleConstructorN constructor, Key[] dependencies) { + return to(constructor, dependencies, 0); + } + + public static Binding to(TupleConstructorN constructor, Key[] dependencies, int priority) { + return new BindingToConstructor<>(null, constructor, dependencies, priority); + } + + // endregion + + public Binding scope(Annotation scope) { + this.scope = scope; + return this; + } + + public Binding prioritize(int priority) { + this.priority = priority; + return this; + } + + public Binding withKey(Key key) { + this.originalKey = key; + return this; + } + + public Binding initializeWith(BindingInitializer bindingInitializer) { + return new Binding( + this.originalKey, + Stream.of(this.dependencies, bindingInitializer.getDependencies()) + .flatMap(Set::stream) + .collect(Collectors.toSet()), + this.scope, + this.priority) { + @Override + public Supplier compile(Function, Supplier> compiler) { + final Supplier compiledBinding = Binding.this.compile(compiler); + final Consumer consumer = bindingInitializer.compile(compiler); + return () -> { + T instance = compiledBinding.get(); + consumer.accept(instance); + return instance; + }; + } + }; + } + + public abstract Supplier compile(Function, Supplier> compiler); + + public Set> getDependencies() { + return dependencies; + } + + public Annotation getScope() { + return scope; + } + + public String getDisplayString() { + return dependencies.stream().map(Key::getDisplayString).collect(joining(", ", "[", "]")); + } + + public Key getOriginalKey() { + return originalKey; + } + + public int getPriority() { + return priority; + } + + @Override + public String toString() { + return "Binding" + dependencies.toString(); + } + + @FunctionalInterface + public interface TupleConstructorN { + R create(Object... args); + } + + public static class BindingToInstance extends Binding { + final T instance; + + BindingToInstance(T instance) { + super(null, Collections.emptySet()); + this.instance = instance; + } + + @Override + public Supplier compile(Function, Supplier> compiler) { + return () -> instance; + } + } + + public static class BindingToConstructor extends Binding { + final TupleConstructorN constructor; + + BindingToConstructor( + Key key, TupleConstructorN constructor, Key[] dependencies, int priority) { + super(key, new HashSet<>(Arrays.asList(dependencies)), null, priority); + this.constructor = constructor; + } + + @Override + public Supplier compile(Function, Supplier> compiler) { + return () -> { + Object[] args = getDependencies().stream() + .map(compiler) + .map(Supplier::get) + .toArray(); + return constructor.create(args); + }; + } + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/BindingInitializer.java b/maven-di/src/main/java/org/apache/maven/di/impl/BindingInitializer.java new file mode 100644 index 000000000000..8561db988399 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/BindingInitializer.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.maven.di.Key; + +import static java.util.stream.Collectors.toSet; + +public abstract class BindingInitializer { + + private final Set> dependencies; + + protected BindingInitializer(Set> dependencies) { + this.dependencies = dependencies; + } + + public Set> getDependencies() { + return dependencies; + } + + public abstract Consumer compile(Function, Supplier> compiler); + + public static BindingInitializer combine(List> bindingInitializers) { + Set> deps = bindingInitializers.stream() + .map(BindingInitializer::getDependencies) + .flatMap(Collection::stream) + .collect(toSet()); + return new BindingInitializer(deps) { + @Override + public Consumer compile(Function, Supplier> compiler) { + return instance -> bindingInitializers.stream() + .map(bindingInitializer -> bindingInitializer.compile(compiler)) + .forEach(i -> i.accept(instance)); + } + }; + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java b/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java new file mode 100644 index 000000000000..984cd94a5f42 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.di.Injector; +import org.apache.maven.di.Key; + +/** + * A runtime exception that is thrown on startup when some static conditions fail + * (missing or cyclic dependencies, incorrect annotations etc.) or in runtime when + * you ask an {@link Injector} for an instance it does not have a {@link Binding binding} for. + */ +public final class DIException extends RuntimeException { + public static DIException cannotConstruct(Key key, @Nullable Binding binding) { + return new DIException((binding != null ? "Binding refused to" : "No binding to") + + " construct an instance for key " + + key.getDisplayString()); + } + + public DIException(String message) { + super(message); + } + + public DIException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java new file mode 100644 index 000000000000..41bc4f88837a --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.maven.api.di.Provides; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.di.Typed; +import org.apache.maven.di.Injector; +import org.apache.maven.di.Key; +import org.apache.maven.di.Scope; + +public class InjectorImpl implements Injector { + + private final Map, Set>> bindings = new HashMap<>(); + private final Map, Scope> scopes = new HashMap<>(); + + public InjectorImpl() { + bindScope(Singleton.class, new SingletonScope()); + } + + public T getInstance(Class key) { + return getInstance(Key.of(key)); + } + + public T getInstance(Key key) { + return getCompiledBinding(key).get(); + } + + public Injector bindScope(Class scopeAnnotation, Scope scope) { + if (scopes.put(scopeAnnotation, scope) != null) { + throw new DIException( + "Cannot rebind scope annotation class to a different implementation: " + scopeAnnotation); + } + return this; + } + + public Injector bindInstance(Class cls, U instance) { + return bind(Key.of(cls), Binding.toInstance(instance)); + } + + public Injector bind(Key key, Binding b) { + Set> bindingSet = bindings.computeIfAbsent(key, $ -> new HashSet<>()); + bindingSet.add(b); + return this; + } + + @Override + public Injector bindImplicit(Class moduleClass) { + Class cls = moduleClass; + while (cls != Object.class && cls != null) { + doBindImplicit(cls); + cls = cls.getSuperclass(); + } + return this; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Set> getBindings(Key key) { + return (Set) bindings.get(key); + } + + public Supplier getCompiledBinding(Key key) { + Set> res = getBindings(key); + if (res != null) { + List> bindingList = new ArrayList<>(res); + Comparator> comparing = Comparator.comparing(Binding::getPriority); + bindingList.sort(comparing.reversed()); + Binding binding = bindingList.get(0); + return compile(binding); + } + if (key.getRawType() == List.class) { + Set> res2 = getBindings(key.getTypeParameter(0)); + if (res2 != null) { + List> bindingList = + res2.stream().map(this::compile).collect(Collectors.toList()); + //noinspection unchecked + return () -> (Q) new WrappingList<>(bindingList, Supplier::get); + } + } + if (key.getRawType() == Map.class) { + Key k = key.getTypeParameter(0); + Key v = key.getTypeParameter(1); + Set> res2 = getBindings(v); + if (k.getRawType() == String.class && res2 != null) { + Map> map = res2.stream() + .filter(b -> b.getOriginalKey().getQualifier() == null + || b.getOriginalKey().getQualifier() instanceof String) + .collect(Collectors.toMap( + b -> (String) b.getOriginalKey().getQualifier(), this::compile)); + //noinspection unchecked + return (() -> (Q) new WrappingMap<>(map, Supplier::get)); + } + } + throw DIException.cannotConstruct(key, null); + } + + @SuppressWarnings("unchecked") + private Supplier compile(Binding binding) { + Supplier compiled = binding.compile(this::getCompiledBinding); + if (binding.getScope() != null) { + Scope scope = scopes.entrySet().stream() + .filter(e -> e.getKey().isInstance(binding.getScope())) + .map(Map.Entry::getValue) + .findFirst() + .orElseThrow(() -> new DIException("Scope not bound for annotation " + + binding.getScope().getClass())); + compiled = scope.scope((Key) binding.getOriginalKey(), binding.getScope(), compiled); + } + return compiled; + } + + @SuppressWarnings("unchecked") + protected void doBindImplicit(Class clazz) { + Key key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz)); + Binding binding = ReflectionUtils.generateImplicitBinding(key); + if (binding != null) { + // For non-explicit bindings, also bind all their base classes and interfaces according to the @Type + Set> toBind = new HashSet<>(); + Deque> todo = new ArrayDeque<>(); + todo.add(key); + + Set> types; + Typed typed = key.getRawType().getAnnotation(Typed.class); + if (typed != null) { + Class[] typesArray = typed.value(); + if (typesArray == null || typesArray.length == 0) { + types = new HashSet<>(Arrays.asList(key.getRawType().getInterfaces())); + types.add(Object.class); + } else { + types = new HashSet<>(Arrays.asList(typesArray)); + } + } else { + types = null; + } + + Set> done = new HashSet<>(); + while (!todo.isEmpty()) { + Key type = todo.remove(); + if (done.add(type)) { + Class cls = Types.getRawType(type.getType()); + Type[] interfaces = cls.getGenericInterfaces(); + Arrays.stream(interfaces) + .map(t -> Key.ofType(t, key.getQualifier())) + .forEach(todo::add); + Type supercls = cls.getGenericSuperclass(); + if (supercls != null) { + todo.add(Key.ofType(supercls, key.getQualifier())); + } + if (types == null || types.contains(cls)) { + toBind.add(type); + } + } + } + // Also bind without the qualifier + if (key.getQualifier() != null) { + new HashSet<>(toBind).forEach(k -> toBind.add(Key.ofType(k.getType()))); + } + toBind.forEach((k -> bind((Key) k, (Binding) binding))); + } + // Bind inner classes + for (Class inner : clazz.getDeclaredClasses()) { + bindImplicit(inner); + } + // Bind inner providers + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Provides.class)) { + if (!Modifier.isStatic(method.getModifiers())) { + throw new DIException( + "Found non-static provider method while scanning for statics, method " + method); + } + + Object qualifier = ReflectionUtils.qualifierOf(method); + Annotation scope = ReflectionUtils.scopeOf(method); + + TypeVariable[] methodTypeParameters = method.getTypeParameters(); + Map, Type> mapping = new HashMap<>(); + for (TypeVariable methodTypeParameter : methodTypeParameters) { + mapping.put(methodTypeParameter, methodTypeParameter); + } + mapping.putAll(Types.getAllTypeBindings(clazz)); + + Type returnType = Types.bind(method.getGenericReturnType(), mapping); + + if (methodTypeParameters.length == 0) { + Key rkey = Key.ofType(returnType, qualifier); + bind(rkey, ReflectionUtils.bindingFromMethod(method).scope(scope)); + } else { + throw new DIException("Parameterized method are not supported " + method); + } + } + } + } + + private static class WrappingMap extends AbstractMap { + + private final Map delegate; + private final Function mapper; + + WrappingMap(Map delegate, Function mapper) { + this.delegate = delegate; + this.mapper = mapper; + } + + @SuppressWarnings("NullableProblems") + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + Iterator> it = delegate.entrySet().iterator(); + return new Iterator>() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Entry next() { + Entry n = it.next(); + return new SimpleImmutableEntry<>(n.getKey(), mapper.apply(n.getValue())); + } + }; + } + + @Override + public int size() { + return delegate.size(); + } + }; + } + } + + private static class WrappingList extends AbstractList { + + private final List delegate; + private final Function mapper; + + WrappingList(List delegate, Function mapper) { + this.delegate = delegate; + this.mapper = mapper; + } + + @Override + public Q get(int index) { + return mapper.apply(delegate.get(index)); + } + + @Override + public int size() { + return delegate.size(); + } + } + + private static class SingletonScope implements Scope { + Map, java.util.function.Supplier> cache = new HashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public java.util.function.Supplier scope( + Key key, Annotation scope, java.util.function.Supplier unscoped) { + return (java.util.function.Supplier) + cache.computeIfAbsent(key, k -> new java.util.function.Supplier() { + volatile T instance; + + @Override + public T get() { + if (instance == null) { + synchronized (this) { + if (instance == null) { + instance = unscoped.get(); + } + } + } + return instance; + } + }); + } + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java b/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java new file mode 100644 index 000000000000..b7518d00b399 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.di.*; +import org.apache.maven.di.Key; + +import static java.util.stream.Collectors.toList; + +public final class ReflectionUtils { + private static final String IDENT = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; + + private static final Pattern PACKAGE = Pattern.compile("(?:" + IDENT + "\\.)*"); + private static final Pattern PACKAGE_AND_PARENT = Pattern.compile(PACKAGE.pattern() + "(?:" + IDENT + "\\$\\d*)?"); + private static final Pattern ARRAY_SIGNATURE = Pattern.compile("\\[L(.*?);"); + + public static String getDisplayName(Type type) { + Class raw = Types.getRawType(type); + String typeName; + if (raw.isAnonymousClass()) { + Type superclass = raw.getGenericSuperclass(); + typeName = "? extends " + superclass.getTypeName(); + } else { + typeName = type.getTypeName(); + } + + return PACKAGE_AND_PARENT + .matcher(ARRAY_SIGNATURE.matcher(typeName).replaceAll("$1[]")) + .replaceAll(""); + } + + public static @Nullable Object getOuterClassInstance(Object innerClassInstance) { + if (innerClassInstance == null) { + return null; + } + Class cls = innerClassInstance.getClass(); + Class enclosingClass = cls.getEnclosingClass(); + if (enclosingClass == null) { + return null; + } + for (Field field : cls.getDeclaredFields()) { + if (!field.isSynthetic() || !field.getName().startsWith("this$") || field.getType() != enclosingClass) { + continue; + } + field.setAccessible(true); + try { + return field.get(innerClassInstance); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + return null; + } + + public static @Nullable Object qualifierOf(AnnotatedElement annotatedElement) { + Object qualifier = null; + for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(Qualifier.class)) { + if (qualifier != null) { + throw new DIException("More than one name annotation on " + annotatedElement); + } + if (annotation instanceof Named) { + qualifier = ((Named) annotation).value(); + } else { + Class annotationType = annotation.annotationType(); + qualifier = Utils.isMarker(annotationType) ? annotationType : annotation; + } + } + } + return qualifier; + } + + public static @Nullable Annotation scopeOf(AnnotatedElement annotatedElement) { + Annotation scope = null; + for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(org.apache.maven.api.di.Scope.class)) { + if (scope != null) { + throw new DIException("More than one scope annotation on " + annotatedElement); + } + scope = annotation; + } + } + return scope; + } + + public static Key keyOf(@Nullable Type container, Type type, AnnotatedElement annotatedElement) { + return Key.ofType( + container != null ? Types.bind(type, Types.getAllTypeBindings(container)) : type, + qualifierOf(annotatedElement)); + } + + public static List getAnnotatedElements( + Class cls, + Class annotationType, + Function, T[]> extractor, + boolean allowStatic) { + List result = new ArrayList<>(); + while (cls != null) { + for (T element : extractor.apply(cls)) { + if (element.isAnnotationPresent(annotationType)) { + if (!allowStatic && Modifier.isStatic(element.getModifiers())) { + throw new DIException( + "@" + annotationType.getSimpleName() + " annotation is not allowed on " + element); + } + result.add(element); + } + } + cls = cls.getSuperclass(); + } + return result; + } + + public static @Nullable Binding generateImplicitBinding(Key key) { + Binding binding = generateConstructorBinding(key); + if (binding != null) { + Annotation scope = scopeOf(key.getRawType()); + if (scope != null) { + binding = binding.scope(scope); + } + binding = binding.initializeWith(generateInjectingInitializer(key)); + } + return binding; + } + + @SuppressWarnings("unchecked") + public static @Nullable Binding generateConstructorBinding(Key key) { + Class cls = key.getRawType(); + + Annotation classInjectAnnotation = Stream.of(cls.getAnnotations()) + .filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)) + .findAny() + .orElse(null); + List> constructors = Arrays.asList(cls.getDeclaredConstructors()); + List> injectConstructors = constructors.stream() + .filter(c -> c.isAnnotationPresent(Inject.class)) + .collect(toList()); + + List factoryMethods = Arrays.stream(cls.getDeclaredMethods()) + .filter(method -> method.getReturnType() == cls && Modifier.isStatic(method.getModifiers())) + .collect(toList()); + List injectFactoryMethods = factoryMethods.stream() + .filter(method -> method.isAnnotationPresent(Inject.class)) + .collect(toList()); + + if (classInjectAnnotation != null) { + if (!injectConstructors.isEmpty()) { + throw failedImplicitBinding(key, "inject annotation on class with inject constructor"); + } + if (!factoryMethods.isEmpty()) { + throw failedImplicitBinding(key, "inject annotation on class with factory method"); + } + if (constructors.isEmpty()) { + throw failedImplicitBinding(key, "inject annotation on interface"); + } + if (constructors.size() > 1) { + throw failedImplicitBinding(key, "inject annotation on class with multiple constructors"); + } + Constructor declaredConstructor = + (Constructor) constructors.iterator().next(); + + Class enclosingClass = cls.getEnclosingClass(); + if (enclosingClass != null + && !Modifier.isStatic(cls.getModifiers()) + && declaredConstructor.getParameterCount() != 1) { + throw failedImplicitBinding( + key, + "inject annotation on local class that closes over outside variables and/or has no default constructor"); + } + return bindingFromConstructor(key, declaredConstructor); + } + if (!injectConstructors.isEmpty()) { + if (injectConstructors.size() > 1) { + throw failedImplicitBinding(key, "more than one inject constructor"); + } + if (!injectFactoryMethods.isEmpty()) { + throw failedImplicitBinding(key, "both inject constructor and inject factory method are present"); + } + return bindingFromConstructor( + key, (Constructor) injectConstructors.iterator().next()); + } + + if (!injectFactoryMethods.isEmpty()) { + if (injectFactoryMethods.size() > 1) { + throw failedImplicitBinding(key, "more than one inject factory method"); + } + return bindingFromMethod(injectFactoryMethods.iterator().next()); + } + return null; + } + + private static DIException failedImplicitBinding(Key requestedKey, String message) { + return new DIException( + "Failed to generate implicit binding for " + requestedKey.getDisplayString() + ", " + message); + } + + public static BindingInitializer generateInjectingInitializer(Key container) { + Class rawType = container.getRawType(); + List> initializers = Stream.concat( + getAnnotatedElements(rawType, Inject.class, Class::getDeclaredFields, false).stream() + .map(field -> fieldInjector(container, field)), + getAnnotatedElements(rawType, Inject.class, Class::getDeclaredMethods, true).stream() + .filter(method -> !Modifier.isStatic( + method.getModifiers())) // we allow them and just filter out to allow + // static factory methods + .map(method -> methodInjector(container, method))) + .collect(toList()); + return BindingInitializer.combine(initializers); + } + + public static BindingInitializer fieldInjector(Key container, Field field) { + field.setAccessible(true); + Key key = keyOf(container.getType(), field.getGenericType(), field); + return new BindingInitializer(Collections.singleton(key)) { + @Override + public Consumer compile(Function, Supplier> compiler) { + Supplier binding = compiler.apply(key); + return (T instance) -> { + Object arg = binding.get(); + try { + field.set(instance, arg); + } catch (IllegalAccessException e) { + throw new DIException("Not allowed to set injectable field " + field, e); + } + }; + } + }; + } + + public static BindingInitializer methodInjector(Key container, Method method) { + method.setAccessible(true); + Key[] dependencies = toDependencies(container.getType(), method); + return new BindingInitializer(new HashSet<>(Arrays.asList(dependencies))) { + @Override + public Consumer compile(Function, Supplier> compiler) { + return instance -> { + Object[] args = getDependencies().stream() + .map(compiler) + .map(Supplier::get) + .toArray(); + try { + method.invoke(instance, args); + } catch (IllegalAccessException e) { + throw new DIException("Not allowed to call injectable method " + method, e); + } catch (InvocationTargetException e) { + throw new DIException("Failed to call injectable method " + method, e.getCause()); + } + }; + } + }; + } + + public static Key[] toDependencies(@Nullable Type container, Executable executable) { + Parameter[] parameters = executable.getParameters(); + Key[] dependencies = new Key[parameters.length]; + if (parameters.length == 0) { + return dependencies; + } + + Type type = parameters[0].getParameterizedType(); + Parameter parameter = parameters[0]; + dependencies[0] = keyOf(container, type, parameter); + + Type[] genericParameterTypes = executable.getGenericParameterTypes(); + boolean hasImplicitDependency = genericParameterTypes.length != parameters.length; + for (int i = 1; i < dependencies.length; i++) { + type = genericParameterTypes[hasImplicitDependency ? i - 1 : i]; + parameter = parameters[i]; + dependencies[i] = keyOf(container, type, parameter); + } + return dependencies; + } + + @SuppressWarnings("unchecked") + public static Binding bindingFromMethod(Method method) { + method.setAccessible(true); + Binding binding = Binding.to( + args -> { + try { + T result = (T) method.invoke(null, args); + if (result == null) { + throw new NullPointerException( + "@Provides method must return non-null result, method " + method); + } + return result; + } catch (IllegalAccessException e) { + throw new DIException("Not allowed to call method " + method, e); + } catch (InvocationTargetException e) { + throw new DIException("Failed to call method " + method, e.getCause()); + } + }, + toDependencies(method.getDeclaringClass(), method)); + + Priority priority = method.getAnnotation(Priority.class); + if (priority != null) { + binding = binding.prioritize(priority.value()); + } + + return binding; + } + + public static Binding bindingFromConstructor(Key key, Constructor constructor) { + constructor.setAccessible(true); + + Key[] dependencies = toDependencies(key.getType(), constructor); + + Binding binding = Binding.to( + args -> { + try { + return constructor.newInstance(args); + } catch (InstantiationException e) { + throw new DIException( + "Cannot instantiate object from the constructor " + constructor + + " to provide requested key " + key, + e); + } catch (IllegalAccessException e) { + throw new DIException( + "Not allowed to call constructor " + constructor + " to provide requested key " + key, + e); + } catch (InvocationTargetException e) { + throw new DIException( + "Failed to call constructor " + constructor + " to provide requested key " + key, + e.getCause()); + } + }, + dependencies); + + Priority priority = constructor.getDeclaringClass().getAnnotation(Priority.class); + if (priority != null) { + binding = binding.prioritize(priority.value()); + } + + return binding.withKey(key); + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/TypeUtils.java b/maven-di/src/main/java/org/apache/maven/di/impl/TypeUtils.java new file mode 100644 index 000000000000..dbd8b72578e5 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/TypeUtils.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * This class contains reflection utilities to work with Java types. + * Its main use is for method {@link Types#parameterizedType Types.parameterized}. + * However, just like with {@link ReflectionUtils}, other type utility + * methods are pretty clean too, so they are left public. + */ +public final class TypeUtils { + + public static boolean isInheritedFrom(Type type, Type from, Map dejaVu) { + if (from == Object.class) { + return true; + } + if (matches(type, from, dejaVu) || matches(from, type, dejaVu)) { + return true; + } + if (!(type instanceof Class || type instanceof ParameterizedType || type instanceof GenericArrayType)) { + return false; + } + Class rawType = Types.getRawType(type); + + Type superclass = rawType.getGenericSuperclass(); + if (superclass != null && isInheritedFrom(superclass, from, dejaVu)) { + return true; + } + return Arrays.stream(rawType.getGenericInterfaces()).anyMatch(iface -> isInheritedFrom(iface, from, dejaVu)); + } + + public static boolean matches(Type strict, Type pattern) { + return matches(strict, pattern, new HashMap<>()); + } + + private static boolean matches(Type strict, Type pattern, Map dejaVu) { + if (strict.equals(pattern) || dejaVu.get(strict) == pattern) { + return true; + } + dejaVu.put(strict, pattern); + try { + if (pattern instanceof WildcardType) { + WildcardType wildcard = (WildcardType) pattern; + return Arrays.stream(wildcard.getUpperBounds()) + .allMatch(bound -> isInheritedFrom(strict, bound, dejaVu)) + && Arrays.stream(wildcard.getLowerBounds()) + .allMatch(bound -> isInheritedFrom(bound, strict, dejaVu)); + } + if (pattern instanceof TypeVariable) { + TypeVariable typevar = (TypeVariable) pattern; + return Arrays.stream(typevar.getBounds()).allMatch(bound -> isInheritedFrom(strict, bound, dejaVu)); + } + if (strict instanceof GenericArrayType && pattern instanceof GenericArrayType) { + return matches( + ((GenericArrayType) strict).getGenericComponentType(), + ((GenericArrayType) pattern).getGenericComponentType(), + dejaVu); + } + if (!(strict instanceof ParameterizedType) || !(pattern instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedStrict = (ParameterizedType) strict; + ParameterizedType parameterizedPattern = (ParameterizedType) pattern; + if (parameterizedPattern.getOwnerType() != null) { + if (parameterizedStrict.getOwnerType() == null) { + return false; + } + if (!matches(parameterizedPattern.getOwnerType(), parameterizedStrict.getOwnerType(), dejaVu)) { + return false; + } + } + if (!matches(parameterizedPattern.getRawType(), parameterizedStrict.getRawType(), dejaVu)) { + return false; + } + + Type[] strictParams = parameterizedStrict.getActualTypeArguments(); + Type[] patternParams = parameterizedPattern.getActualTypeArguments(); + if (strictParams.length != patternParams.length) { + return false; + } + for (int i = 0; i < strictParams.length; i++) { + if (!matches(strictParams[i], patternParams[i], dejaVu)) { + return false; + } + } + return true; + } finally { + dejaVu.remove(strict); + } + } + + public static boolean contains(Type type, Type sub) { + if (type.equals(sub)) { + return true; + } + if (type instanceof GenericArrayType) { + return contains(((GenericArrayType) type).getGenericComponentType(), sub); + } + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterized = (ParameterizedType) type; + if (contains(parameterized.getRawType(), sub)) { + return true; + } + if (parameterized.getOwnerType() != null && contains(parameterized.getOwnerType(), sub)) { + return true; + } + return Arrays.stream(parameterized.getActualTypeArguments()).anyMatch(argument -> contains(argument, sub)); + } + + // pattern = Map> + // real = Map> + // + // result = {K -> String, V -> Integer} + public static Map, Type> extractMatchingGenerics(Type pattern, Type real) { + Map, Type> result = new HashMap<>(); + extractMatchingGenerics(pattern, real, result); + return result; + } + + private static void extractMatchingGenerics(Type pattern, Type real, Map, Type> result) { + if (pattern instanceof TypeVariable) { + result.put((TypeVariable) pattern, real); + return; + } + if (pattern.equals(real)) { + return; + } + if (pattern instanceof GenericArrayType && real instanceof GenericArrayType) { + extractMatchingGenerics( + ((GenericArrayType) pattern).getGenericComponentType(), + ((GenericArrayType) real).getGenericComponentType(), + result); + return; + } + if (!(pattern instanceof ParameterizedType) || !(real instanceof ParameterizedType)) { + return; + } + ParameterizedType parameterizedPattern = (ParameterizedType) pattern; + ParameterizedType parameterizedReal = (ParameterizedType) real; + if (!parameterizedPattern.getRawType().equals(parameterizedReal.getRawType())) { + return; + } + extractMatchingGenerics(parameterizedPattern.getRawType(), parameterizedReal.getRawType(), result); + if (!Objects.equals(parameterizedPattern.getOwnerType(), parameterizedReal.getOwnerType())) { + return; + } + if (parameterizedPattern.getOwnerType() != null) { + extractMatchingGenerics(parameterizedPattern.getOwnerType(), parameterizedReal.getOwnerType(), result); + } + Type[] patternTypeArgs = parameterizedPattern.getActualTypeArguments(); + Type[] realTypeArgs = parameterizedReal.getActualTypeArguments(); + if (patternTypeArgs.length != realTypeArgs.length) { + return; + } + for (int i = 0; i < patternTypeArgs.length; i++) { + extractMatchingGenerics(patternTypeArgs[i], realTypeArgs[i], result); + } + } + + public static Type simplifyType(Type original) { + if (original instanceof Class) { + return original; + } + + if (original instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) original).getGenericComponentType(); + Type repackedComponentType = simplifyType(componentType); + if (componentType != repackedComponentType) { + return Types.genericArrayType(repackedComponentType); + } + return original; + } + + if (original instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) original; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + Type[] repackedTypeArguments = simplifyTypes(typeArguments); + + if (isAllObjects(repackedTypeArguments)) { + return parameterizedType.getRawType(); + } + + if (typeArguments != repackedTypeArguments) { + return Types.parameterizedType( + parameterizedType.getOwnerType(), parameterizedType.getRawType(), repackedTypeArguments); + } + return original; + } + + if (original instanceof TypeVariable) { + throw new IllegalArgumentException("Key should not contain a type variable: " + original); + } + + if (original instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) original; + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length == 1) { + Type upperBound = upperBounds[0]; + if (upperBound != Object.class) { + return simplifyType(upperBound); + } + } else if (upperBounds.length > 1) { + throw new IllegalArgumentException("Multiple upper bounds not supported: " + original); + } + + Type[] lowerBounds = wildcardType.getLowerBounds(); + if (lowerBounds.length == 1) { + return simplifyType(lowerBounds[0]); + } else if (lowerBounds.length > 1) { + throw new IllegalArgumentException("Multiple lower bounds not supported: " + original); + } + return Object.class; + } + + return original; + } + + private static Type[] simplifyTypes(Type[] original) { + int length = original.length; + for (int i = 0; i < length; i++) { + Type typeArgument = original[i]; + Type repackTypeArgument = simplifyType(typeArgument); + if (repackTypeArgument != typeArgument) { + Type[] repackedTypeArguments = new Type[length]; + System.arraycopy(original, 0, repackedTypeArguments, 0, i); + repackedTypeArguments[i++] = repackTypeArgument; + for (; i < length; i++) { + repackedTypeArguments[i] = simplifyType(original[i]); + } + return repackedTypeArguments; + } + } + return original; + } + + private static boolean isAllObjects(Type[] types) { + for (Type type : types) { + if (type != Object.class) { + return false; + } + } + return true; + } + + /** + * Tests whether a {@code from} type is assignable to {@code to} type + * + * @param to a 'to' type that should be checked for possible assignment + * @param from a 'from' type that should be checked for possible assignment + * @return whether an object of type {@code from} is assignable to an object of type {@code to} + */ + public static boolean isAssignable(Type to, Type from) { + // shortcut + if (to instanceof Class && from instanceof Class) { + return ((Class) to).isAssignableFrom((Class) from); + } + return isAssignable(to, from, false); + } + + private static boolean isAssignable(Type to, Type from, boolean strict) { + if (to instanceof WildcardType || from instanceof WildcardType) { + Type[] toUppers, toLowers; + if (to instanceof WildcardType) { + WildcardType wildcardTo = (WildcardType) to; + toUppers = wildcardTo.getUpperBounds(); + toLowers = wildcardTo.getLowerBounds(); + } else { + toUppers = new Type[] {to}; + toLowers = strict ? toUppers : Types.NO_TYPES; + } + + Type[] fromUppers, fromLowers; + if (from instanceof WildcardType) { + WildcardType wildcardTo = (WildcardType) to; + fromUppers = wildcardTo.getUpperBounds(); + fromLowers = wildcardTo.getLowerBounds(); + } else { + fromUppers = new Type[] {from}; + fromLowers = strict ? fromUppers : Types.NO_TYPES; + } + + for (Type toUpper : toUppers) { + for (Type fromUpper : fromUppers) { + if (!isAssignable(toUpper, fromUpper, false)) { + return false; + } + } + } + if (toLowers.length == 0) { + return true; + } + if (fromLowers.length == 0) { + return false; + } + for (Type toLower : toLowers) { + for (Type fromLower : fromLowers) { + if (!isAssignable(fromLower, toLower, false)) { + return false; + } + } + } + return true; + } + if (to instanceof GenericArrayType) { + to = Types.getRawType(to); + } + if (from instanceof GenericArrayType) { + from = Types.getRawType(from); + } + if (!strict && to instanceof Class) { + return ((Class) to).isAssignableFrom(Types.getRawType(from)); + } + Class toRawClazz = Types.getRawType(to); + Type[] toTypeArguments = Types.getActualTypeArguments(to); + return isAssignable(toRawClazz, toTypeArguments, from, strict); + } + + private static boolean isAssignable(Class toRawClazz, Type[] toTypeArguments, Type from, boolean strict) { + Class fromRawClazz = Types.getRawType(from); + if (strict && !toRawClazz.equals(fromRawClazz)) { + return false; + } + if (!strict && !toRawClazz.isAssignableFrom(fromRawClazz)) { + return false; + } + if (toRawClazz.isArray()) { + return true; + } + Type[] fromTypeArguments = Types.getActualTypeArguments(from); + if (toRawClazz == fromRawClazz) { + if (toTypeArguments.length > fromTypeArguments.length) { + return false; + } + for (int i = 0; i < toTypeArguments.length; i++) { + if (!isAssignable(toTypeArguments[i], fromTypeArguments[i], true)) { + return false; + } + } + return true; + } + Map, Type> typeBindings = Types.getTypeBindings(from); + for (Type anInterface : fromRawClazz.getGenericInterfaces()) { + if (isAssignable( + toRawClazz, + toTypeArguments, + Types.bind(anInterface, key -> typeBindings.getOrDefault(key, Types.wildcardTypeAny())), + false)) { + return true; + } + } + Type superclass = fromRawClazz.getGenericSuperclass(); + return superclass != null + && isAssignable(toRawClazz, toTypeArguments, Types.bind(superclass, typeBindings), false); + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/Types.java b/maven-di/src/main/java/org/apache/maven/di/impl/Types.java new file mode 100644 index 000000000000..4fc8a7661f24 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/Types.java @@ -0,0 +1,471 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.apache.maven.api.annotations.Nullable; + +import static java.util.stream.Collectors.joining; + +/** + * Various helper methods for type processing + */ +public class Types { + public static final Type[] NO_TYPES = new Type[0]; + public static final WildcardType WILDCARD_TYPE_ANY = new WildcardTypeImpl(new Type[] {Object.class}, new Type[0]); + private static final Map, Type>> TYPE_BINDINGS_CACHE = new ConcurrentHashMap<>(); + + /** + * Returns a raw {@link Class} for a given {@link Type}. + *

+ * A type can be any of {@link Class}, {@link ParameterizedType}, {@link WildcardType}, + * {@link GenericArrayType} or {@link TypeVariable} + */ + public static Class getRawType(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } else if (type instanceof WildcardType) { + Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + return getRawType(getUppermostType(upperBounds)); + } else if (type instanceof GenericArrayType) { + Class rawComponentType = getRawType(((GenericArrayType) type).getGenericComponentType()); + try { + return Class.forName("[L" + rawComponentType.getName() + ";"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } else if (type instanceof TypeVariable) { + return getRawType(getUppermostType(((TypeVariable) type).getBounds())); + } else { + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + /** + * Returns the most common type among given types + */ + public static Type getUppermostType(Type[] types) { + Type result = types[0]; + for (int i = 1; i < types.length; i++) { + Type type = types[i]; + if (TypeUtils.isAssignable(type, result)) { + result = type; + continue; + } else if (TypeUtils.isAssignable(result, type)) { + continue; + } + throw new IllegalArgumentException("Unrelated types: " + result + " , " + type); + } + return result; + } + + /** + * Returns an array of actual type arguments for a given {@link Type} + * + * @param type type whose actual type arguments should be retrieved + * @return an array of actual type arguments for a given {@link Type} + */ + public static Type[] getActualTypeArguments(Type type) { + if (type instanceof Class) { + return ((Class) type).isArray() ? new Type[] {((Class) type).getComponentType()} : NO_TYPES; + } else if (type instanceof ParameterizedType) { + return ((ParameterizedType) type).getActualTypeArguments(); + } else if (type instanceof GenericArrayType) { + return new Type[] {((GenericArrayType) type).getGenericComponentType()}; + } + throw new IllegalArgumentException("Unsupported type: " + type); + } + + /** + * Returns a map of type bindings for a given {@link Type} + */ + public static Map, Type> getTypeBindings(Type type) { + Type[] typeArguments = getActualTypeArguments(type); + if (typeArguments.length == 0) { + return Collections.emptyMap(); + } + TypeVariable[] typeVariables = getRawType(type).getTypeParameters(); + Map, Type> map = new HashMap<>(); + for (int i = 0; i < typeVariables.length; i++) { + map.put(typeVariables[i], typeArguments[i]); + } + return map; + } + + /** + * Returns a map of all type bindings for a given {@link Type}. + * Includes type bindings from a whole class hierarchy + */ + public static Map, Type> getAllTypeBindings(Type type) { + return TYPE_BINDINGS_CACHE.computeIfAbsent(type, t -> { + Map, Type> mapping = new HashMap<>(); + getAllTypeBindingsImpl(t, mapping); + return mapping; + }); + } + + private static void getAllTypeBindingsImpl(Type type, Map, Type> mapping) { + Class cls = getRawType(type); + + if (type instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + if (typeArguments.length != 0) { + TypeVariable>[] typeVariables = cls.getTypeParameters(); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + mapping.put( + typeVariables[i], + typeArgument instanceof TypeVariable + ? Objects.requireNonNull(mapping.get(typeArgument)) + : typeArgument); + } + } + } + + Type superclass = cls.getGenericSuperclass(); + if (superclass != null) { + getAllTypeBindingsImpl(superclass, mapping); + } + + for (Type anInterface : cls.getGenericInterfaces()) { + getAllTypeBindingsImpl(anInterface, mapping); + } + } + + /** + * Binds a given type with actual type arguments + * + * @param type a type to be bound + * @param bindings a map of actual types + */ + public static Type bind(Type type, Map, Type> bindings) { + return bind(type, bindings::get); + } + + /** + * Binds a given type with actual type arguments + * + * @param type a type to be bound + * @param bindings a lookup function for actual types + */ + public static Type bind(Type type, Function, Type> bindings) { + if (type instanceof Class) { + return type; + } + if (type instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) type; + Type actualType = bindings.apply(typeVariable); + if (actualType == null) { + throw new IllegalArgumentException("Type variable not found: " + typeVariable + " ( " + + typeVariable.getGenericDeclaration() + " ) "); + } + return actualType; + } + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + Type[] typeArguments2 = new Type[typeArguments.length]; + for (int i = 0; i < typeArguments.length; i++) { + typeArguments2[i] = bind(typeArguments[i], bindings); + } + return new ParameterizedTypeImpl( + parameterizedType.getOwnerType(), parameterizedType.getRawType(), typeArguments2); + } + if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + return new GenericArrayTypeImpl(bind(componentType, bindings)); + } + if (type instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) type; + Type[] upperBounds = wildcardType.getUpperBounds(); + Type[] upperBounds2 = new Type[upperBounds.length]; + for (int i = 0; i < upperBounds.length; i++) { + upperBounds2[i] = bind(upperBounds[i], bindings); + } + Type[] lowerBounds = wildcardType.getLowerBounds(); + Type[] lowerBounds2 = new Type[lowerBounds.length]; + for (int i = 0; i < lowerBounds.length; i++) { + lowerBounds2[i] = bind(lowerBounds[i], bindings); + } + return new WildcardTypeImpl(upperBounds2, lowerBounds2); + } + throw new IllegalArgumentException("Unsupported type: " + type); + } + + /** + * Creates an instance of {@link ParameterizedType} + * + * @param ownerType an owner type + * @param rawType a type to be parameterized + * @param parameters parameter types + * @return an instance of {@link ParameterizedType} + */ + public static ParameterizedType parameterizedType(@Nullable Type ownerType, Type rawType, Type[] parameters) { + return new ParameterizedTypeImpl(ownerType, rawType, parameters); + } + + /** + * Creates an instance of {@link ParameterizedType} + * + * @see #parameterizedType(Type, Type, Type[]) + */ + public static ParameterizedType parameterizedType(Class rawType, Type... parameters) { + return new ParameterizedTypeImpl(null, rawType, parameters); + } + + public static final class ParameterizedTypeImpl implements ParameterizedType { + private final @Nullable Type ownerType; + private final Type rawType; + private final Type[] actualTypeArguments; + + ParameterizedTypeImpl(@Nullable Type ownerType, Type rawType, Type[] actualTypeArguments) { + this.ownerType = ownerType; + this.rawType = rawType; + this.actualTypeArguments = actualTypeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type[] getActualTypeArguments() { + return actualTypeArguments; + } + + @Override + public @Nullable Type getOwnerType() { + return ownerType; + } + + @Override + public int hashCode() { + return Objects.hashCode(ownerType) ^ Arrays.hashCode(actualTypeArguments) ^ rawType.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof ParameterizedType)) { + return false; + } + ParameterizedType that = (ParameterizedType) other; + return this.getRawType().equals(that.getRawType()) + && Objects.equals(this.getOwnerType(), that.getOwnerType()) + && Arrays.equals(this.getActualTypeArguments(), that.getActualTypeArguments()); + } + + @Override + public String toString() { + return rawType.getTypeName() + + Arrays.stream(actualTypeArguments).map(Types::toString).collect(joining(", ", "<", ">")); + } + } + + /** + * Creates an instance of {@link WildcardType} bound by upper and lower bounds + * + * @param upperBounds a wildcard upper bound types + * @param lowerBounds a wildcard lower bound types + * @return an instance of {@link WildcardType} + */ + public static WildcardType wildcardType(Type[] upperBounds, Type[] lowerBounds) { + return new WildcardTypeImpl(upperBounds, lowerBounds); + } + + /** + * Returns an instance of {@link WildcardType} that matches any type + *

+ * E.g. {@code } + * + * @see #wildcardType(Type[], Type[]) + */ + public static WildcardType wildcardTypeAny() { + return WILDCARD_TYPE_ANY; + } + + /** + * Creates an instance of {@link WildcardType} bound by a single upper bound + *

+ * E.g. {@code } + * + * @param upperBound a wildcard upper bound type + * @return an instance of {@link WildcardType} + * @see #wildcardType(Type[], Type[]) + */ + public static WildcardType wildcardTypeExtends(Type upperBound) { + return new WildcardTypeImpl(new Type[] {upperBound}, NO_TYPES); + } + + /** + * Creates an instance of {@link WildcardType} bound by a single lower bound + *

+ * E.g. {@code } + * + * @param lowerBound a wildcard lower bound type + * @return an instance of {@link WildcardType} + * @see #wildcardType(Type[], Type[]) + */ + public static WildcardType wildcardTypeSuper(Type lowerBound) { + return new WildcardTypeImpl(NO_TYPES, new Type[] {lowerBound}); + } + + public static class WildcardTypeImpl implements WildcardType { + private final Type[] upperBounds; + private final Type[] lowerBounds; + + WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + this.upperBounds = upperBounds; + this.lowerBounds = lowerBounds; + } + + @Override + public Type[] getUpperBounds() { + return upperBounds; + } + + @Override + public Type[] getLowerBounds() { + return lowerBounds; + } + + @Override + public int hashCode() { + return Arrays.hashCode(upperBounds) ^ Arrays.hashCode(lowerBounds); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof WildcardType)) { + return false; + } + WildcardType that = (WildcardType) other; + return Arrays.equals(this.getUpperBounds(), that.getUpperBounds()) + && Arrays.equals(this.getLowerBounds(), that.getLowerBounds()); + } + + @Override + public String toString() { + return "?" + + (upperBounds.length == 0 + ? "" + : " extends " + + Arrays.stream(upperBounds) + .map(Types::toString) + .collect(joining(" & "))) + + (lowerBounds.length == 0 + ? "" + : " super " + + Arrays.stream(lowerBounds) + .map(Types::toString) + .collect(joining(" & "))); + } + } + + /** + * Creates an instance of {@link GenericArrayType} with a given component type + *

+ * Same as {@code T[]} + * + * @param componentType a component type of generic array + * @return an instance of {@link GenericArrayType} + * @see #wildcardType(Type[], Type[]) + */ + public static GenericArrayType genericArrayType(Type componentType) { + return new GenericArrayTypeImpl(componentType); + } + + public static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type componentType; + + GenericArrayTypeImpl(Type componentType) { + this.componentType = componentType; + } + + @Override + public Type getGenericComponentType() { + return componentType; + } + + @Override + public int hashCode() { + return componentType.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GenericArrayType)) { + return false; + } + GenericArrayType that = (GenericArrayType) other; + return this.getGenericComponentType().equals(that.getGenericComponentType()); + } + + @Override + public String toString() { + return Types.toString(componentType) + "[]"; + } + } + + private static String toString(Type type) { + return type instanceof Class ? ((Class) type).getName() : type.toString(); + } + + /** + * Returns a simple name for a given {@link Type} + * + * @see Class#getSimpleName() + */ + public static String getSimpleName(Type type) { + if (type instanceof Class) { + return ((Class) type).getSimpleName(); + } else if (type instanceof ParameterizedType) { + return Arrays.stream(((ParameterizedType) type).getActualTypeArguments()) + .map(Types::getSimpleName) + .collect(joining(",", "<", ">")); + } else if (type instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) type; + Type[] upperBounds = wildcardType.getUpperBounds(); + Type[] lowerBounds = wildcardType.getLowerBounds(); + return "?" + + (upperBounds.length == 0 + ? "" + : " extends " + + Arrays.stream(upperBounds) + .map(Types::getSimpleName) + .collect(joining(" & "))) + + (lowerBounds.length == 0 + ? "" + : " super " + + Arrays.stream(lowerBounds) + .map(Types::getSimpleName) + .collect(joining(" & "))); + } else if (type instanceof GenericArrayType) { + return Types.getSimpleName(((GenericArrayType) type).getGenericComponentType()) + "[]"; + } + + return type.getTypeName(); + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/Utils.java b/maven-di/src/main/java/org/apache/maven/di/impl/Utils.java new file mode 100644 index 000000000000..195be016e7e7 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/impl/Utils.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.annotation.Annotation; + +import org.apache.maven.api.annotations.Nullable; + +public final class Utils { + + public static String getDisplayString(Class annotationType, @Nullable Annotation annotation) { + if (annotation == null) { + return "@" + ReflectionUtils.getDisplayName(annotationType); + } + String typeName = annotationType.getName(); + String str = annotation.toString(); + return str.startsWith("@" + typeName) + ? "@" + ReflectionUtils.getDisplayName(annotationType) + str.substring(typeName.length() + 1) + : str; + } + + public static String getDisplayString(Object object) { + if (object instanceof Class && ((Class) object).isAnnotation()) { + //noinspection unchecked + return getDisplayString((Class) object, null); + } + if (object instanceof Annotation) { + Annotation annotation = (Annotation) object; + return getDisplayString(annotation.annotationType(), annotation); + } + return object.toString(); + } + + public static boolean isMarker(Class annotationType) { + return annotationType.getDeclaredMethods().length == 0; + } +} diff --git a/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java b/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java new file mode 100644 index 000000000000..f46530a9c588 --- /dev/null +++ b/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.processor; + +import javax.annotation.processing.Completion; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.StandardLocation; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.*; + +import org.apache.maven.api.di.Qualifier; + +public class IndexAnnotationProcessor implements Processor { + + private ProcessingEnvironment environment; + private Set index = new TreeSet<>(); + + @Override + public void init(ProcessingEnvironment processingEnv) { + this.environment = processingEnv; + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + if (annotation.getAnnotation(Qualifier.class) != null) { + for (Element elem : roundEnv.getElementsAnnotatedWith(annotation)) { + if (elem.getKind().isClass()) { + addClassToIndex(environment + .getElementUtils() + .getBinaryName((TypeElement) elem) + .toString()); + } + } + } + } + if (roundEnv.processingOver()) { + flushIndex(); + } + return false; + } + + protected void addClassToIndex(String className) { + index.add(className); + } + + protected void flushIndex() { + try (BufferedWriter writer = new BufferedWriter(environment + .getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/maven/org.apache.maven.api.di.Inject") + .openWriter())) { + for (String line : index) { + writer.write(line); + writer.newLine(); + } + } catch (IOException e) { + environment.getMessager().printMessage(Diagnostic.Kind.WARNING, e.toString()); + } + } + + @Override + public Iterable getCompletions( + Element element, AnnotationMirror annotation, ExecutableElement member, String userText) { + return Collections.emptySet(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton("*"); + } + + @Override + public Set getSupportedOptions() { + return Collections.emptySet(); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } +} diff --git a/maven-di/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/maven-di/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000000..e219e2ed643c --- /dev/null +++ b/maven-di/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.apache.maven.di.processor.IndexAnnotationProcessor diff --git a/maven-di/src/test/java/org/apache/maven/di/impl/DITest.java b/maven-di/src/test/java/org/apache/maven/di/impl/DITest.java new file mode 100644 index 000000000000..3e70ef5310b4 --- /dev/null +++ b/maven-di/src/test/java/org/apache/maven/di/impl/DITest.java @@ -0,0 +1,277 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.maven.api.di.*; +import org.apache.maven.di.Injector; +import org.apache.maven.di.Key; +import org.junit.jupiter.api.Test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.*; + +@SuppressWarnings("unused") +public class DITest { + + @Test + void markerQualifierTest() { + Injector injector = Injector.create().bindImplicit(QualifierTest.class); + QualifierTest.MyMojo mojo = injector.getInstance(QualifierTest.MyMojo.class); + assertNotNull(mojo); + assertInstanceOf(QualifierTest.MyQualifiedServiceImpl.class, mojo.service); + } + + static class QualifierTest { + @Qualifier + @Retention(RUNTIME) + @interface MyQualifier {} + + interface MyService {} + + @Named + @Priority(10) + static class MyNamedServiceImpl implements MyService {} + + @MyQualifier + static class MyQualifiedServiceImpl implements MyService {} + + @Named + static class MyMojo { + @Inject + @MyQualifier + MyService service; + } + } + + @Test + void priorityTest() { + Injector injector = Injector.create().bindImplicit(PriorityTest.class); + PriorityTest.MyMojo mojo = injector.getInstance(PriorityTest.MyMojo.class); + assertNotNull(mojo); + assertInstanceOf(PriorityTest.MyPriorityServiceImpl.class, mojo.service); + } + + static class PriorityTest { + + interface MyService {} + + @Named + static class MyServiceImpl implements MyService {} + + @Named + @Priority(10) + static class MyPriorityServiceImpl implements MyService {} + + @Named + static class MyMojo { + @Inject + MyService service; + } + } + + @Test + void mojoTest() { + Injector injector = Injector.create().bindImplicit(MojoTest.class); + MojoTest.MyMojo mojo = injector.getInstance(MojoTest.MyMojo.class); + assertNotNull(mojo); + } + + @SuppressWarnings("unused") + static class MojoTest { + @Qualifier + @Retention(RUNTIME) + @interface Mojo {} + + interface MyService {} + + @Named + static class MyServiceImpl implements MyService {} + + @Mojo + static class MyMojo { + @Inject + MyService service; + } + } + + @Test + void typedTest() { + Injector injector = + Injector.create().bindImplicit(TypedTest.MyServiceImpl.class).bindImplicit(TypedTest.MyMojo.class); + TypedTest.MyMojo mojo = injector.getInstance(TypedTest.MyMojo.class); + assertNotNull(mojo); + assertNotNull(mojo.service); + } + + @SuppressWarnings("unused") + static class TypedTest { + + interface MyService {} + + @Named + @Typed + static class MyServiceImpl implements MyService {} + + @Named + static class MyMojo { + @Inject + MyService service; + } + } + + @Test + public void bindInterfacesTest() { + Injector injector = Injector.create().bindImplicit(BindInterfaces.class); + BindInterfaces.TestInterface inst = + injector.getInstance(new Key>() {}); + assertNotNull(inst); + } + + static class BindInterfaces { + + interface TestInterface { + T getObj(); + } + + @Named + static class ClassImpl implements TestInterface { + @Override + public String getObj() { + return null; + } + } + + @Named + @Typed + static class TypedClassImpl implements TestInterface { + @Override + public String getObj() { + return null; + } + } + } + + @Test + void injectListTest() { + Injector injector = Injector.create().bindImplicit(InjectList.class); + List services = injector.getInstance(new Key>() {}); + assertNotNull(services); + assertEquals(2, services.size()); + + assertNotNull(services.get(0)); + assertInstanceOf(InjectList.MyService.class, services.get(0)); + assertNotNull(services.get(1)); + assertInstanceOf(InjectList.MyService.class, services.get(1)); + assertNotSame(services.get(0).getClass(), services.get(1).getClass()); + } + + static class InjectList { + + interface MyService {} + + @Named("foo") + static class MyServiceImpl implements MyService {} + + @Named("bar") + static class AnotherServiceImpl implements MyService {} + } + + @Test + void injectMapTest() { + Injector injector = Injector.create().bindImplicit(InjectMap.class); + Map services = + injector.getInstance(new Key>() {}); + assertNotNull(services); + assertEquals(2, services.size()); + + List> entries = new ArrayList<>(services.entrySet()); + assertNotNull(entries.get(0)); + assertInstanceOf(InjectMap.MyService.class, entries.get(0).getValue()); + assertInstanceOf(String.class, entries.get(0).getKey()); + assertNotNull(entries.get(1)); + assertInstanceOf(String.class, entries.get(1).getKey()); + assertInstanceOf(InjectMap.MyService.class, entries.get(1).getValue()); + assertNotEquals(entries.get(0).getKey(), entries.get(1).getKey()); + assertNotSame( + entries.get(0).getValue().getClass(), entries.get(1).getValue().getClass()); + + InjectMap.MyMojo mojo = injector.getInstance(InjectMap.MyMojo.class); + assertNotNull(mojo); + assertNotNull(mojo.services); + assertEquals(2, mojo.services.size()); + } + + static class InjectMap { + + interface MyService {} + + @Named("foo") + static class MyServiceImpl implements MyService {} + + @Named("bar") + static class AnotherServiceImpl implements MyService {} + + @Named + static class MyMojo { + @Inject + Map services; + } + } + + @Test + void testSingleton() { + Injector injector = Injector.create() + .bindImplicit(SingletonContainer.Bean1.class) + .bindImplicit(SingletonContainer.Bean2.class); + + SingletonContainer.Bean1 b1a = injector.getInstance(SingletonContainer.Bean1.class); + assertNotNull(b1a); + SingletonContainer.Bean1 b1b = injector.getInstance(SingletonContainer.Bean1.class); + assertNotNull(b1b); + assertEquals(b1a.num, b1b.num); + + SingletonContainer.Bean2 b2a = injector.getInstance(SingletonContainer.Bean2.class); + assertNotNull(b2a); + SingletonContainer.Bean2 b2b = injector.getInstance(SingletonContainer.Bean2.class); + assertNotNull(b2b); + assertNotEquals(b2a.num, b2b.num); + } + + static class SingletonContainer { + private static final AtomicInteger bean1 = new AtomicInteger(); + private static final AtomicInteger bean2 = new AtomicInteger(); + + @Named + @Singleton + static class Bean1 { + int num = bean1.incrementAndGet(); + } + + @Named + static class Bean2 { + int num = bean2.incrementAndGet(); + } + } +} diff --git a/maven-di/src/test/java/org/apache/maven/di/impl/TypeT.java b/maven-di/src/test/java/org/apache/maven/di/impl/TypeT.java new file mode 100644 index 000000000000..39db8f7b2e56 --- /dev/null +++ b/maven-di/src/test/java/org/apache/maven/di/impl/TypeT.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Type; + +/** + * A type token for defining complex types (annotated, parameterized) + *

+ * Usage example: + *

+ * {@code Type listOfStringsType = new TypeT>(){}.getType()} + * + * @param actual type + */ +public abstract class TypeT { + private final AnnotatedType annotatedType; + + /** + * Creates a new type token. A type argument {@link T} must be specified. + * A typical usage is: + *

+ * {@code TypeT> integerListTypeT = new TypeT>(){};} + * + * @throws AssertionError if a {@link TypeT} is created with a raw type + */ + protected TypeT() { + this.annotatedType = getSuperclassTypeParameter(this.getClass()); + } + + private static AnnotatedType getSuperclassTypeParameter(Class subclass) { + AnnotatedType superclass = subclass.getAnnotatedSuperclass(); + if (superclass instanceof AnnotatedParameterizedType) { + return ((AnnotatedParameterizedType) superclass).getAnnotatedActualTypeArguments()[0]; + } + throw new AssertionError(); + } + + /** + * Returns an {@link AnnotatedType} of a {@link T} + */ + public final AnnotatedType getAnnotatedType() { + return annotatedType; + } + + /** + * Returns a {@link Type} of a {@link T} + */ + public final Type getType() { + return annotatedType.getType(); + } + + /** + * Returns a raw type (e.g {@link Class}) of a {@link T} + */ + @SuppressWarnings("unchecked") + public final Class getRawType() { + return (Class) Types.getRawType(annotatedType.getType()); + } + + @Override + public final String toString() { + return annotatedType.toString(); + } +} diff --git a/maven-di/src/test/java/org/apache/maven/di/impl/TypeUtilsTest.java b/maven-di/src/test/java/org/apache/maven/di/impl/TypeUtilsTest.java new file mode 100644 index 000000000000..2bf26477eb9f --- /dev/null +++ b/maven-di/src/test/java/org/apache/maven/di/impl/TypeUtilsTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.di.impl; + +import java.lang.reflect.Type; +import java.util.*; + +import org.junit.jupiter.api.Test; + +import static org.apache.maven.di.impl.TypeUtils.simplifyType; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TypeUtilsTest { + + @Test + public void testSimplifyType() { + { + Type type = Integer.class; + assertEquals(type, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + assertEquals(type, simplifyType(type)); + } + + { + Type type = new TypeT>>>() {}.getType(); + assertEquals(type, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + Type expected = new TypeT>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>>>() {}.getType(); + Type expected = new TypeT>>>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>>>() {}.getType(); + Type expected = new TypeT>>>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + Type expected = new TypeT>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>>>() {}.getType(); + Type expected = new TypeT>>>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>>>() {}.getType(); + Type expected = new TypeT>>>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>>>() {}.getType(); + Type expected = new TypeT>>>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT[]>() {}.getType(); + Type expected = new TypeT[]>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT[]>() {}.getType(); + Type expected = new TypeT[]>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + Type expected = new TypeT>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + Type expected = new TypeT>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + Type expected = TestClass.class; + assertEquals(expected, simplifyType(type)); + } + + { + Type type = new TypeT>() {}.getType(); + Type expected = TestClass.class; + assertEquals(expected, simplifyType(type)); + } + + { + //noinspection TypeParameterExplicitlyExtendsObject + Type type = new TypeT< + TestClass< + Integer, + ? extends Integer, + ? super Integer, + Object, + ? extends Object, + ? super Object, + ?, + Set>, + Set>>>() {}.getType(); + Type expected = new TypeT< + TestClass< + Integer, + Integer, + Integer, + Object, + Object, + Object, + Object, + Set>, + Set>>>() {}.getType(); + assertEquals(expected, simplifyType(type)); + } + } + + public static final class TestClass {} + + interface TestInterface {} +} diff --git a/pom.xml b/pom.xml index feceadc5086f..00565834f7ba 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,7 @@ under the License. maven-model maven-model-builder api + maven-di maven-xml-impl maven-core maven-settings @@ -257,6 +258,16 @@ under the License. maven-api-xml ${project.version} + + org.apache.maven + maven-api-di + ${project.version} + + + org.apache.maven + maven-di + ${project.version} + org.apache.maven maven-model-builder From d26e9b0a81cca97f7458debd7ed1d46ec0409db4 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 31 Jan 2024 09:39:48 +0100 Subject: [PATCH 2/3] wip --- maven-di/src/main/java/org/apache/maven/di/Injector.java | 2 ++ .../main/java/org/apache/maven/di/impl/InjectorImpl.java | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/maven-di/src/main/java/org/apache/maven/di/Injector.java b/maven-di/src/main/java/org/apache/maven/di/Injector.java index d961b8092c28..e75324eadc99 100644 --- a/maven-di/src/main/java/org/apache/maven/di/Injector.java +++ b/maven-di/src/main/java/org/apache/maven/di/Injector.java @@ -38,6 +38,8 @@ static Injector create() { Injector bindInstance(Class cls, T instance); + Injector bindInstanceAndInject(Class cls, T instance); + // // Bean access // diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java index 41bc4f88837a..b731a66e1056 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -64,6 +64,13 @@ public Injector bindInstance(Class cls, U instance) { return bind(Key.of(cls), Binding.toInstance(instance)); } + @Override + public Injector bindInstanceAndInject(Class cls, T instance) { + Key key = Key.of(cls); + return bind( + key, Binding.toInstance(instance).initializeWith(ReflectionUtils.generateInjectingInitializer(key))); + } + public Injector bind(Key key, Binding b) { Set> bindingSet = bindings.computeIfAbsent(key, $ -> new HashSet<>()); bindingSet.add(b); From 260f95819037c974b8ce7a9bd81a7dcc294611cd Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 2 Feb 2024 23:10:21 +0100 Subject: [PATCH 3/3] Fix DI support for @Mojo and plugins --- .../maven/api/plugin/annotations/Mojo.java | 2 - .../EnhancedComponentConfigurator.java | 26 ++-- maven-di/pom.xml | 12 ++ .../java/org/apache/maven/di/Injector.java | 4 +- .../org/apache/maven/di/impl/Binding.java | 15 +++ .../org/apache/maven/di/impl/DIException.java | 8 -- .../apache/maven/di/impl/InjectorImpl.java | 115 ++++++++++++------ .../apache/maven/di/impl/ReflectionUtils.java | 25 +++- .../processor/IndexAnnotationProcessor.java | 53 +++++++- 9 files changed, 199 insertions(+), 61 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java index 12b8344da7a1..4f01f0f6d22b 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Mojo.java @@ -27,7 +27,6 @@ import org.apache.maven.api.annotations.Experimental; import org.apache.maven.api.annotations.Nonnull; -import org.apache.maven.api.di.Qualifier; /** * This annotation will mark your class as a Mojo (ie. goal in a Maven plugin). @@ -42,7 +41,6 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited -@Qualifier public @interface Mojo { /** * goal name (required). diff --git a/maven-core/src/main/java/org/apache/maven/configuration/internal/EnhancedComponentConfigurator.java b/maven-core/src/main/java/org/apache/maven/configuration/internal/EnhancedComponentConfigurator.java index f41a1d64d5f1..334eafe3af9c 100644 --- a/maven-core/src/main/java/org/apache/maven/configuration/internal/EnhancedComponentConfigurator.java +++ b/maven-core/src/main/java/org/apache/maven/configuration/internal/EnhancedComponentConfigurator.java @@ -52,16 +52,26 @@ public void configureComponent( try { ClassRealmConverter.pushContextRealm(realm); - new EnhancedConfigurationConverter() - .processConfiguration( - converterLookup, - component, - realm, // - configuration, - evaluator, - listener); + this.configureComponent(component, configuration, evaluator, (ClassLoader) realm, listener); } finally { ClassRealmConverter.popContextRealm(); } } + + public void configureComponent( + Object component, + PlexusConfiguration configuration, + ExpressionEvaluator evaluator, + ClassLoader loader, + ConfigurationListener listener) + throws ComponentConfigurationException { + new EnhancedConfigurationConverter() + .processConfiguration( + converterLookup, + component, + loader, // + configuration, + evaluator, + listener); + } } diff --git a/maven-di/pom.xml b/maven-di/pom.xml index 9efddf76d3f7..67a71f1d6573 100644 --- a/maven-di/pom.xml +++ b/maven-di/pom.xml @@ -34,6 +34,18 @@ under the License. org.apache.maven maven-api-di + + org.apache.maven + maven-api-core + + + org.apache.maven + maven-xml-impl + + + org.codehaus.plexus + plexus-xml + diff --git a/maven-di/src/main/java/org/apache/maven/di/Injector.java b/maven-di/src/main/java/org/apache/maven/di/Injector.java index e75324eadc99..9e9b65a33f2a 100644 --- a/maven-di/src/main/java/org/apache/maven/di/Injector.java +++ b/maven-di/src/main/java/org/apache/maven/di/Injector.java @@ -38,12 +38,12 @@ static Injector create() { Injector bindInstance(Class cls, T instance); - Injector bindInstanceAndInject(Class cls, T instance); - // // Bean access // + void injectInstance(T instance); + T getInstance(Class key); T getInstance(Key key); diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java b/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java index 80d9bcbf5f57..fc46f84619f0 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java @@ -101,6 +101,11 @@ public Supplier compile(Function, Supplier> compiler) { return instance; }; } + + @Override + public String toString() { + return Binding.this.toString(); + } }; } @@ -148,6 +153,11 @@ public static class BindingToInstance extends Binding { public Supplier compile(Function, Supplier> compiler) { return () -> instance; } + + @Override + public String toString() { + return "BindingToInstance[" + instance + "]" + getDependencies(); + } } public static class BindingToConstructor extends Binding { @@ -169,5 +179,10 @@ public Supplier compile(Function, Supplier> compiler) { return constructor.create(args); }; } + + @Override + public String toString() { + return "BindingToConstructor[" + constructor + "]" + getDependencies(); + } } } diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java b/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java index 984cd94a5f42..4aca38c16e97 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java @@ -18,9 +18,7 @@ */ package org.apache.maven.di.impl; -import org.apache.maven.api.annotations.Nullable; import org.apache.maven.di.Injector; -import org.apache.maven.di.Key; /** * A runtime exception that is thrown on startup when some static conditions fail @@ -28,12 +26,6 @@ * you ask an {@link Injector} for an instance it does not have a {@link Binding binding} for. */ public final class DIException extends RuntimeException { - public static DIException cannotConstruct(Key key, @Nullable Binding binding) { - return new DIException((binding != null ? "Binding refused to" : "No binding to") - + " construct an instance for key " - + key.getDisplayString()); - } - public DIException(String message) { super(message); } diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java index b731a66e1056..49df5788484d 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -20,7 +20,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.*; @@ -52,6 +51,14 @@ public T getInstance(Key key) { return getCompiledBinding(key).get(); } + @SuppressWarnings("unchecked") + @Override + public void injectInstance(T instance) { + ReflectionUtils.generateInjectingInitializer(Key.of((Class) instance.getClass())) + .compile(this::getCompiledBinding) + .accept(instance); + } + public Injector bindScope(Class scopeAnnotation, Scope scope) { if (scopes.put(scopeAnnotation, scope) != null) { throw new DIException( @@ -60,33 +67,36 @@ public Injector bindScope(Class scopeAnnotation, Scope sco return this; } - public Injector bindInstance(Class cls, U instance) { - return bind(Key.of(cls), Binding.toInstance(instance)); + public Injector bindInstance(Class clazz, U instance) { + Key key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz)); + Binding binding = Binding.toInstance(instance); + return doBind(key, binding); } @Override - public Injector bindInstanceAndInject(Class cls, T instance) { - Key key = Key.of(cls); - return bind( - key, Binding.toInstance(instance).initializeWith(ReflectionUtils.generateInjectingInitializer(key))); - } - - public Injector bind(Key key, Binding b) { - Set> bindingSet = bindings.computeIfAbsent(key, $ -> new HashSet<>()); - bindingSet.add(b); - return this; + public Injector bindImplicit(Class clazz) { + Key key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz)); + Binding binding = ReflectionUtils.generateImplicitBinding(key); + return doBind(key, binding); } - @Override - public Injector bindImplicit(Class moduleClass) { - Class cls = moduleClass; + private Injector doBind(Key key, Binding binding) { + doBindImplicit(key, binding); + Class cls = key.getRawType().getSuperclass(); while (cls != Object.class && cls != null) { - doBindImplicit(cls); + key = Key.of(cls, key.getQualifier()); + doBindImplicit(key, binding); cls = cls.getSuperclass(); } return this; } + protected Injector bind(Key key, Binding b) { + Set> bindingSet = bindings.computeIfAbsent(key, $ -> new HashSet<>()); + bindingSet.add(b); + return this; + } + @SuppressWarnings({"unchecked", "rawtypes"}) private Set> getBindings(Key key) { return (Set) bindings.get(key); @@ -124,7 +134,9 @@ public Supplier getCompiledBinding(Key key) { return (() -> (Q) new WrappingMap<>(map, Supplier::get)); } } - throw DIException.cannotConstruct(key, null); + throw new DIException("No binding to construct an instance for key " + + key.getDisplayString() + ". Existing bindings:\n" + + bindings.keySet().stream().map(Key::toString).collect(Collectors.joining("\n - ", " - ", ""))); } @SuppressWarnings("unchecked") @@ -142,10 +154,7 @@ private Supplier compile(Binding binding) { return compiled; } - @SuppressWarnings("unchecked") - protected void doBindImplicit(Class clazz) { - Key key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz)); - Binding binding = ReflectionUtils.generateImplicitBinding(key); + protected void doBindImplicit(Key key, Binding binding) { if (binding != null) { // For non-explicit bindings, also bind all their base classes and interfaces according to the @Type Set> toBind = new HashSet<>(); @@ -191,35 +200,71 @@ protected void doBindImplicit(Class clazz) { toBind.forEach((k -> bind((Key) k, (Binding) binding))); } // Bind inner classes - for (Class inner : clazz.getDeclaredClasses()) { + for (Class inner : key.getRawType().getDeclaredClasses()) { bindImplicit(inner); } // Bind inner providers - for (Method method : clazz.getDeclaredMethods()) { + for (Method method : key.getRawType().getDeclaredMethods()) { if (method.isAnnotationPresent(Provides.class)) { - if (!Modifier.isStatic(method.getModifiers())) { - throw new DIException( - "Found non-static provider method while scanning for statics, method " + method); - } - Object qualifier = ReflectionUtils.qualifierOf(method); Annotation scope = ReflectionUtils.scopeOf(method); TypeVariable[] methodTypeParameters = method.getTypeParameters(); + if (methodTypeParameters.length != 0) { + throw new DIException("Parameterized method are not supported " + method); + } Map, Type> mapping = new HashMap<>(); for (TypeVariable methodTypeParameter : methodTypeParameters) { mapping.put(methodTypeParameter, methodTypeParameter); } - mapping.putAll(Types.getAllTypeBindings(clazz)); + mapping.putAll(Types.getAllTypeBindings(key.getRawType())); Type returnType = Types.bind(method.getGenericReturnType(), mapping); - - if (methodTypeParameters.length == 0) { - Key rkey = Key.ofType(returnType, qualifier); - bind(rkey, ReflectionUtils.bindingFromMethod(method).scope(scope)); + Key rkey = Key.ofType(returnType, qualifier); + + Set> types; + Typed typed = method.getAnnotation(Typed.class); + if (typed != null) { + Class[] typesArray = typed.value(); + if (typesArray == null || typesArray.length == 0) { + types = new HashSet<>(Arrays.asList(rkey.getRawType().getInterfaces())); + types.add(Object.class); + } else { + types = new HashSet<>(Arrays.asList(typesArray)); + } } else { - throw new DIException("Parameterized method are not supported " + method); + types = null; + } + + Set> toBind = new HashSet<>(); + Deque> todo = new ArrayDeque<>(); + todo.add(rkey); + + Set> done = new HashSet<>(); + while (!todo.isEmpty()) { + Key type = todo.remove(); + if (done.add(type)) { + Class cls = Types.getRawType(type.getType()); + Type[] interfaces = cls.getGenericInterfaces(); + Arrays.stream(interfaces) + .map(t -> Key.ofType(t, qualifier)) + .forEach(todo::add); + Type supercls = cls.getGenericSuperclass(); + if (supercls != null) { + todo.add(Key.ofType(supercls, qualifier)); + } + if (types == null || types.contains(cls)) { + toBind.add(type); + } + } } + // Also bind without the qualifier + if (qualifier != null) { + new HashSet<>(toBind).forEach(k -> toBind.add(Key.ofType(k.getType()))); + } + + Binding bind = ReflectionUtils.bindingFromMethod(method).scope(scope); + toBind.forEach((k -> bind((Key) k, bind))); } } } diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java b/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java index b7518d00b399..dae002f22fda 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java @@ -83,7 +83,7 @@ public static String getDisplayName(Type type) { for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) { if (annotation.annotationType().isAnnotationPresent(Qualifier.class)) { if (qualifier != null) { - throw new DIException("More than one name annotation on " + annotatedElement); + throw new DIException("More than one qualifier annotation on " + annotatedElement); } if (annotation instanceof Named) { qualifier = ((Named) annotation).value(); @@ -276,6 +276,18 @@ public Consumer compile(Function, Supplier> compiler) { } public static Key[] toDependencies(@Nullable Type container, Executable executable) { + Key[] keys = toArgDependencies(container, executable); + if (executable instanceof Constructor || Modifier.isStatic(executable.getModifiers())) { + return keys; + } else { + Key[] nkeys = new Key[keys.length + 1]; + nkeys[0] = Key.ofType(container); + System.arraycopy(keys, 0, nkeys, 1, keys.length); + return nkeys; + } + } + + private static Key[] toArgDependencies(@Nullable Type container, Executable executable) { Parameter[] parameters = executable.getParameters(); Key[] dependencies = new Key[parameters.length]; if (parameters.length == 0) { @@ -302,7 +314,16 @@ public static Binding bindingFromMethod(Method method) { Binding binding = Binding.to( args -> { try { - T result = (T) method.invoke(null, args); + Object instance; + Object[] params; + if (Modifier.isStatic(method.getModifiers())) { + instance = null; + params = args; + } else { + instance = args[0]; + params = Arrays.copyOfRange(args, 1, args.length); + } + T result = (T) method.invoke(instance, params); if (result == null) { throw new NullPointerException( "@Provides method must return non-null result, method " + method); diff --git a/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java b/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java index f46530a9c588..ad6950671dab 100644 --- a/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java +++ b/maven-di/src/main/java/org/apache/maven/di/processor/IndexAnnotationProcessor.java @@ -23,18 +23,23 @@ import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; +import javax.lang.model.element.*; import javax.tools.Diagnostic; import javax.tools.StandardLocation; +import javax.xml.stream.XMLStreamReader; import java.io.BufferedWriter; import java.io.IOException; +import java.io.Reader; +import java.io.Writer; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.ctc.wstx.stax.WstxInputFactory; import org.apache.maven.api.di.Qualifier; +import org.apache.maven.api.xml.XmlNode; +import org.apache.maven.internal.xml.XmlNodeBuilder; public class IndexAnnotationProcessor implements Processor { @@ -60,6 +65,46 @@ public boolean process(Set annotations, RoundEnvironment } } } + for (Element elem : roundEnv.getElementsAnnotatedWith(org.apache.maven.api.plugin.annotations.Mojo.class)) { + PackageElement packageElement = environment.getElementUtils().getPackageOf(elem); + String packageName = packageElement.getQualifiedName().toString(); + String generatorClassName = elem.getSimpleName().toString() + "Factory"; + + String mojoName = elem.getAnnotation(org.apache.maven.api.plugin.annotations.Mojo.class) + .name(); + + try { + Reader reader = environment + .getFiler() + .getResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/maven/plugin.xml") + .openReader(true); + XMLStreamReader parser = WstxInputFactory.newFactory().createXMLStreamReader(reader); + XmlNode plugin = XmlNodeBuilder.build(parser, null); + String groupId = plugin.getChild("groupId").getValue(); + String artifactId = plugin.getChild("artifactId").getValue(); + String version = plugin.getChild("version").getValue(); + + Writer file = environment + .getFiler() + .createSourceFile(packageName + "." + generatorClassName) + .openWriter(); + file.write("package " + packageName + ";\n"); + file.write("public class " + generatorClassName + " {\n"); + file.write(" @org.apache.maven.api.di.Named(\"" + groupId + ":" + artifactId + ":" + version + ":" + + mojoName + "\")\n"); + file.write(" @org.apache.maven.api.di.Provides\n"); + file.write(" public static " + ((TypeElement) elem).getQualifiedName() + " create() {\n"); + file.write(" return new " + ((TypeElement) elem).getQualifiedName() + "();\n"); + file.write(" }\n"); + file.write("}\n"); + file.flush(); + file.close(); + } catch (Exception ex) { + Logger.getLogger(IndexAnnotationProcessor.class.getName()).log(Level.SEVERE, null, ex); + } + + addClassToIndex(packageName + "." + generatorClassName); + } if (roundEnv.processingOver()) { flushIndex(); }