diff --git a/all/pom.xml b/all/pom.xml index a4d6e458d54..c37fc609d7e 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -1140,6 +1140,10 @@ io.helidon.service.inject helidon-service-inject + + io.helidon.service.inject + helidon-service-inject-maven-plugin + io.helidon.metadata helidon-metadata-hson diff --git a/bom/pom.xml b/bom/pom.xml index 60d895a39ed..42775cd8560 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1506,6 +1506,11 @@ helidon-service-inject ${helidon.version} + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${helidon.version} + diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java index b5a42510ed8..3bb8a300cea 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java @@ -697,7 +697,8 @@ private static boolean isTypeInThisModule(AptContext ctx, moduleName.set(null); ModuleElement module = ctx.aptEnv().getElementUtils().getModuleOf(type); - if (!module.isUnnamed()) { + + if (module != null && !module.isUnnamed()) { String name = module.getQualifiedName().toString(); if (hasValue(name)) { moduleName.set(name); diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java index 37c17191603..243d60fa4c9 100644 --- a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java +++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.util.List; +import java.util.Optional; import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; @@ -77,6 +78,14 @@ interface CompilerOptionsBlueprint { @Option.Default("21") String target(); + /** + * The compiler release. + * If not specified, {@link #source()} and {@link #target()} would be used. + * + * @return release for compilation + */ + Optional release(); + /** * Target directory to generate class files to. * diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java index ac7f67127c8..ea116847ef9 100644 --- a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java +++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java @@ -44,6 +44,7 @@ class JavaC { private final List commandLineArgs; private final String source; private final String target; + private final String release; private final Path outputDirectory; private final CodegenLogger logger; @@ -54,6 +55,7 @@ private JavaC(CompilerOptions options) { this.commandLineArgs = options.commandLineArguments(); this.source = options.source(); this.target = options.target(); + this.release = options.release().orElse(null); this.outputDirectory = options.outputDirectory(); this.logger = options.logger(); } @@ -123,14 +125,20 @@ private void doCompile(Result result, Path[] sourceFilesToCompile) { optionList.add("--source-path"); optionList.add(toSourcepath()); } - if (source != null) { - optionList.add("--source"); - optionList.add(source); - } - if (target != null) { - optionList.add("--target"); - optionList.add(target); + if (release == null) { + if (source != null) { + optionList.add("--source"); + optionList.add(source); + } + if (target != null) { + optionList.add("--target"); + optionList.add(target); + } + } else { + optionList.add("--release"); + optionList.add(release); } + optionList.addAll(commandLineArgs); if (outputDirectory != null) { optionList.add("-d"); diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java index a167313516d..6e045d1c970 100644 --- a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -45,6 +45,39 @@ public sealed interface ElementSignature permits ElementSignatures.FieldSignatur ElementSignatures.MethodSignature, ElementSignatures.ParameterSignature, ElementSignatures.NoSignature { + /** + * A field signature. + * + * @param type type of the field + * @param name name of the field + * @return a new field signature + */ + static ElementSignature createField(TypeName type, String name) { + return ElementSignatures.createField(type, name); + } + + /** + * A constructor signature. + * + * @param parameters list of types of parameters + * @return a new constructor signature + */ + static ElementSignature createConstructor(List parameters) { + return ElementSignatures.createConstructor(parameters); + } + + /** + * A method signature. + * + * @param returnType return type of the method + * @param name name of the method + * @param parameters parameter types of the method + * @return a new method signature + */ + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + return ElementSignatures.createMethod(returnType, name, parameters); + } + /** * Type of the element. Resolves as follows: *
    diff --git a/pom.xml b/pom.xml index 0b57c348ff2..d1b7a6bb06e 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,13 @@ 2.26.3 3.10 4.8.165 + 2.7.0 + 3.3.0 + 3.9.0 + 3.9.3 + 2.2.1 + 1.1.2 + 2.4.16 @@ -120,6 +127,7 @@ 4.0.14 ${version.lib.hibernate} 3.1.2 + 3.2.2 0.8.5 3.1.2 3.3.0 @@ -127,6 +135,7 @@ 2.5.0 1.16 1.5.0.Final + 3.9.0 3.0.0 0.6.1 3.3.1 @@ -853,6 +862,39 @@ maven-antrun-plugin ${version.plugin.ant} + + org.apache.maven.plugins + maven-invoker-plugin + ${version.plugin.invoker} + + true + src/it/settings.xml + true + ${project.build.directory}/it-repo + + ${maven.test.failure.ignore} + true + + ${skipTests} + + ${skipTests} + + projects/*/pom.xml + + ${project.build.directory}/it + + clean + install + + + + + org.codehaus.groovy + groovy + ${version.lib.groovy} + + + @@ -944,6 +986,15 @@ + + org.apache.maven.plugins + maven-plugin-plugin + ${version.plugin.plugin} + + [3.6.1,) + [${version.java}.0,) + + @@ -1314,6 +1365,43 @@ jakarta.inject-tck ${version.lib.jakarta.inject} + + + org.apache.maven + maven-artifact + ${version.lib.maven.plugin.api} + provided + + + org.apache.maven + maven-model + ${version.lib.maven.plugin.api} + provided + + + org.apache.maven + maven-plugin-api + ${version.lib.maven.plugin.api} + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${version.lib.maven.plugin.annotations} + provided + + + org.apache.maven + maven-project + ${version.lib.maven.plugin.project} + provided + + + + org.apiguardian + apiguardian-api + ${version.lib.apiguardian.api} + diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java b/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java index 8c340ce7f04..c9a464af0cc 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java @@ -25,6 +25,28 @@ * New service descriptor metadata with its class code. */ public interface DescriptorClassCode { + /** + * Create a new instance. + * + * @param classCode class code that contains necessary information for the generated class. + * @param registryType type of registry that generates the descriptor (core, inject) + * @param weight weight of the service this descriptor describes + * @param contracts contracts of the service (i.e. {@code MyContract}) + * @param factoryContracts factory contracts of this service (i.e. {@code Supplier}) + * @return a new class code of service descriptor + */ + static DescriptorClassCode create(ClassCode classCode, + String registryType, + double weight, + Set contracts, + Set factoryContracts) { + return new DescriptorClassCodeImpl(classCode, + registryType, + weight, + contracts, + factoryContracts); + } + /** * New source code information. * diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java b/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java index 651ef707ffd..3fcc867a56d 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java @@ -47,7 +47,7 @@ /** * Generates a service descriptor. */ -class GenerateServiceDescriptor { +public class GenerateServiceDescriptor { static final TypeName SET_OF_RESOLVED_TYPES = TypeName.builder(TypeNames.SET) .addTypeArgument(TypeNames.RESOLVED_TYPE_NAME) .build(); @@ -89,11 +89,11 @@ private GenerateServiceDescriptor(TypeName generator, * @param service service to create a descriptor for * @return class model builder of the service descriptor */ - static ClassModel.Builder generate(TypeName generator, - RegistryCodegenContext ctx, - RegistryRoundContext roundContext, - Collection allServices, - TypeInfo service) { + public static ClassModel.Builder generate(TypeName generator, + RegistryCodegenContext ctx, + RegistryRoundContext roundContext, + Collection allServices, + TypeInfo service) { return new GenerateServiceDescriptor(generator, ctx, roundContext, diff --git a/service/inject/README.md b/service/inject/README.md index 7a8f87f72a3..304f35e63a2 100644 --- a/service/inject/README.md +++ b/service/inject/README.md @@ -13,6 +13,7 @@ Helidon Inject includes: - [Aspect Oriented Programming (interceptors)](#interceptors) - [Events](events) - [Programmatic Lookup](#programmatic-lookup) +- [Startup](#startup) - [Other](#other) - [Glossary](#glossary) @@ -361,6 +362,29 @@ Lookup parameter options: - `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments) - `Lookup` - a full search criteria for a registry lookup +# Startup + +The following options are available to start a service registry (and the application): + +1. Use API to create an `io.helidon.service.inject.InjectRegistryManager` +2. Use the Helidon startup class `io.helidon.Main`, which will use the injection main class through service loader +3. Use a generated main class, by default named `ApplicationMain` in the main package of the application (supports customization) + +## Generated Main Class + +To generate a main class, the Helidon Service Inject Maven plugin must be configured. +This is expected to be configured only for an application (i.e. not for library modules) - this is the reason we do not generate it automatically. + +The generated main class will contain full, reflection less configuration of the service registry. It registers all services directly through API, and disables service discovery from classpath. + +The Main class can also be customized; to do this: +1. Create a custom class (let's call it `CustomMain` as an example) +2. The class must extend the injection main class (`public abstract class CustomMain extends InjectionMain`) +3. The class must be annotated with `@Injection.Main`, so it is discovered by annotation processor +4. Implement any desired methods; the generated class will only implement `serviceDescriptors(InjectConfig.Builder configBuilder)` (always), and `discoverServices()` (if created from the Maven plugin) + +For details on how to configure your build, see [Maven Plugin](../maven-plugin/README.md). + # Other ## API types quick reference diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java index d26291c3f97..5a643e45993 100644 --- a/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java @@ -311,6 +311,26 @@ private Injection() { Class value() default Singleton.class; } + /** + * Marks a custom main class (should be declared as {@code abstract}). + * This is a required annotation to make sure the generated main class extends the user's main class. + * Note that the custom main class MUST extend the {@code io.helidon.service.inject.InjectionMain} to customize + * startup sequence. + * The name {@code ApplicationMain} is reserved by service registry, and cannot be used for your custom main class; + * the generated name can be modified through {@code Maven} plugin configuration and annotation processor configuration. + *

    + * The generated main class adds registration of all available services to injection config, + * and disables service discovery to prevent any and all reflection done by service registry itself. + *

    + * To have correct startup of your application, use the {@code ApplicationMain} as your application entry point + * (such as a {@code mainClass} configured in {@code pom.xml} when using Maven and Helidon application parents). + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface Main { + } + /** * Provides an ability to create more than one service instance from a single service definition. * This is useful when the cardinality can only be determined at runtime. diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java index 6f49a8ade71..6dad1fc703e 100644 --- a/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java @@ -110,6 +110,18 @@ static void addContract(Lookup.BuilderBase builder, Class contract) { builder.addContract(ResolvedType.create(contract)); } + /** + * The managed services advertised types (i.e., typically its interfaces). + * + * @param builder builder instance + * @param contract contract the service implements + * @see Lookup#contracts() + */ + @Prototype.BuilderMethod + static void addContract(Lookup.BuilderBase builder, TypeName contract) { + builder.addContract(ResolvedType.create(contract)); + } + /** * The managed service implementation type. * diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/ApplicationMainGenerator.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/ApplicationMainGenerator.java new file mode 100644 index 00000000000..4ba8d39126e --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/ApplicationMainGenerator.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Method; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.DOUBLE_ARRAY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_CONFIG; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_CONFIG_BUILDER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_MAIN; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_REGISTRY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.STRING_ARRAY; +import static java.util.function.Predicate.not; + +/** + * Utility for {@value #CLASS_NAME} class generation. + */ +public final class ApplicationMainGenerator { + /** + * Default class name of the generated main class. + */ + public static final String CLASS_NAME = "ApplicationMain"; + /** + * Signature of the {@code serviceDescriptors} method (this method is abstract in InjectionMain). + */ + private static final ElementSignature METHOD_SERVICE_DESCRIPTORS = ElementSignature.createMethod( + TypeNames.PRIMITIVE_VOID, + "serviceDescriptors", + List.of(INJECT_CONFIG_BUILDER)); + /** + * Signature of the {@code discoverServices} method. + */ + private static final ElementSignature METHOD_DISCOVER_SERVICES = ElementSignature.createMethod( + TypeNames.PRIMITIVE_BOOLEAN, + "discoverServices", + List.of()); + /** + * Signature of the {@code runLevels} method. + */ + private static final ElementSignature METHOD_RUN_LEVELS = ElementSignature.createMethod( + DOUBLE_ARRAY, + "runLevels", + List.of(INJECT_CONFIG, INJECT_REGISTRY)); + + private ApplicationMainGenerator() { + } + + /** + * Generate the common parts of the type. + * + *

      + *
    • Class declaration with javadoc, @Generated, and copyright, with name {@value #CLASS_NAME}
    • + *
    • Protected constructor with javadoc
    • + *
    • {@code public static void main} method with javadoc
    • + *
    + * + * @param generator generator type name + * @param declaredSignatures signatures declared on custom main class (if any) + * @param superType super type of the generated class + * @param generatedType the type to generate + * @param discoverServices whether to discover services (false when all services are manually registered to the + * builder) + * @param addRunLevels whether to add run levels from discovered services + * @param serviceDescriptorsHandler handler of the service descriptor method + * @param runLevelHandler handler of the run level method + * @return class model builder + */ + @SuppressWarnings("checkstyle:ParameterNumber") // all parameters are mandatory + public static ClassModel.Builder generate(TypeName generator, + Set declaredSignatures, + TypeName superType, + TypeName generatedType, + boolean discoverServices, + boolean addRunLevels, + CodeGeneratorHandler serviceDescriptorsHandler, + CodeGeneratorHandler runLevelHandler) { + ClassModel.Builder classModel = ClassModel.builder() + .type(generatedType) + .accessModifier(AccessModifier.PUBLIC) + .copyright(CodegenUtil.copyright(generator, + generator, + generatedType)) + .addAnnotation(CodegenUtil.generatedAnnotation(generator, + generatedType, + generatedType, + "1", + "")) + .superType(superType) + .addDescriptionLine("Main class generated for Helidon Inject Application.") + .isFinal(true); + + classModel.addConstructor(ctr -> ctr + .accessModifier(AccessModifier.PACKAGE_PRIVATE)); + + classModel.addMethod(main -> main + .accessModifier(AccessModifier.PUBLIC) + .isStatic(true) + .returnType(TypeNames.PRIMITIVE_VOID) + .description("Start the application.") + .name("main") + .addParameter(args -> args + .type(STRING_ARRAY) + .description("Command line arguments.") + .name("args")) + .update(it -> mainMethodBody(generatedType, it))); + + if (!declaredSignatures.contains(METHOD_SERVICE_DESCRIPTORS)) { + // only create this method if it is not created by the user + classModel.addMethod(methodModel -> methodModel + .name("serviceDescriptors") + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(AccessModifier.PROTECTED) + .returnType(TypeNames.PRIMITIVE_VOID) + .addParameter(config -> config + .type(INJECT_CONFIG_BUILDER) + .name("config")) + .update(it -> serviceDescriptorsHandler.handle(classModel, it, "config"))); + } + + if (discoverServices && !declaredSignatures.contains(METHOD_DISCOVER_SERVICES)) { + classModel.addMethod(discoverServicesMethod -> discoverServicesMethod + .name("discoverServices") + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(AccessModifier.PROTECTED) + .returnType(TypeNames.PRIMITIVE_BOOLEAN) + .addContentLine("return true;")); + } + + if (addRunLevels && !declaredSignatures.contains(METHOD_RUN_LEVELS)) { + classModel.addMethod(runLevels -> runLevels + .name("runLevels") + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(AccessModifier.PUBLIC) + .returnType(DOUBLE_ARRAY) + .addParameter(config -> config + .type(INJECT_CONFIG) + .name("config")) + .addParameter(registry -> registry + .type(INJECT_REGISTRY) + .name("registry")) + .addContentLine("return new double[] {") + .update(it -> runLevelHandler.handle(classModel, it, "config")) + .addContentLine("};")); + } + + return classModel; + } + + /** + * Provides all relevant signatures that may override methods from {@code InjectionMain}. + * + * @param customMain type to analyze + * @return set of method signatures that are non-private, non-static + */ + public static Set declaredSignatures(TypeInfo customMain) { + return customMain.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isStatic)) + .filter(not(ElementInfoPredicates::isPrivate)) + .map(TypedElementInfo::signature) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Validate a type, to make sure it is a valid custom main class. + * + * @param customMain type to validate + */ + public static void validate(TypeInfo customMain) { + Optional superType = customMain.superTypeInfo(); + if (superType.isEmpty()) { + throw new CodegenException("Custom main class must directly extend " + INJECT_MAIN.fqName() + ", but " + + customMain.typeName().fqName() + " does not extend any class", + customMain.originatingElementValue()); + } + if (!superType.get().typeName().equals(INJECT_MAIN)) { + throw new CodegenException("Custom main class must directly extend " + INJECT_MAIN.fqName() + ", but " + + customMain.typeName().fqName() + " extends " + superType.get().typeName(), + customMain.originatingElementValue()); + } + if (customMain.accessModifier() == AccessModifier.PRIVATE) { + throw new CodegenException("Custom main class must be accessible (non-private) class, but " + + customMain.typeName().fqName() + " is private.", + customMain.originatingElementValue()); + } + if (customMain.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(ElementInfoPredicates::isStatic) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(ElementInfoPredicates.elementName("main")) + .anyMatch(ElementInfoPredicates.hasParams(TypeName.create(String[].class)))) { + throw new CodegenException("Custom main class must not declare a static main(String[]) method, as it is code " + + "generated into the ApplicationMain class, but " + + customMain.typeName().fqName() + " declares it.", + customMain.originatingElementValue()); + } + if (customMain.elementInfo() + .stream() + .filter(ElementInfoPredicates::isConstructor) + .filter(not(ElementInfoPredicates::isPrivate)) + .noneMatch(ElementInfoPredicates.hasParams())) { + throw new CodegenException("Custom main class must have an accessible no-argument constructor, but " + + customMain.typeName().fqName() + " does not.", + customMain.originatingElementValue()); + } + } + + private static void mainMethodBody(TypeName type, Method.Builder method) { + method.addContent("new ") + .addContent(type) + .addContentLine("().start(args);"); + } + + /** + * Handler to generated method serviceDescriptors in {@value #CLASS_NAME} class. + */ + @FunctionalInterface + public interface CodeGeneratorHandler { + /** + * Handle the class model (to allow adding constants and helper methods), and method model (to add the body). + * + * @param classModel class model of the generated main + * @param methodModel method model of the serviceDescriptors method + * @param configParamName name of the parameter of {@code InjectConfig.Builder} + */ + void handle(ClassModel.Builder classModel, Method.Builder methodModel, String configParamName); + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java index 50ebda400ac..e4c01742ad6 100644 --- a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java @@ -184,7 +184,7 @@ private void addMergeQualifiers(ClassModel.Builder classModel) { .addContentLine("}") .addContent("var qualifierSet = new ") .addContent(HashSet.class) - .addContentLine("(QUALIFIERS);") + .addContentLine("<>(QUALIFIERS);") .addContent("qualifierSet.addAll(") .addContent(Set.class) .addContentLine(".of(qualifiers));") diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java index 765b6e9521b..2737c061821 100644 --- a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java @@ -17,6 +17,7 @@ package io.helidon.service.inject.codegen; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; /** * Types for code generation from Helidon Service Inject API and Helidon Service Inject. @@ -63,6 +64,10 @@ public class InjectCodegenTypes { * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.RunLevel}. */ public static final TypeName INJECTION_RUN_LEVEL = TypeName.create("io.helidon.service.inject.api.Injection.RunLevel"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Main}. + */ + public static final TypeName INJECTION_MAIN = TypeName.create("io.helidon.service.inject.api.Injection.Main"); /** * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InjectionPointFactory}. */ @@ -120,6 +125,42 @@ public class InjectCodegenTypes { */ public static final TypeName INJECT_SERVICE_DESCRIPTOR = TypeName.create("io.helidon.service.inject.api.InjectServiceDescriptor"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectConfig.Builder}. + */ + public static final TypeName INJECT_CONFIG_BUILDER = + TypeName.create("io.helidon.service.inject.InjectConfig.Builder"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectConfig}. + */ + public static final TypeName INJECT_CONFIG = + TypeName.create("io.helidon.service.inject.InjectConfig"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InjectRegistry}. + */ + public static final TypeName INJECT_REGISTRY = + TypeName.create("io.helidon.service.inject.api.InjectRegistry"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectRegistryManager}. + */ + public static final TypeName INJECT_REGISTRY_MANAGER = + TypeName.create("io.helidon.service.inject.InjectRegistryManager"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectionMain}. + */ + public static final TypeName INJECT_MAIN = + TypeName.create("io.helidon.service.inject.InjectionMain"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.Binding}. + */ + public static final TypeName INJECT_BINDING = + TypeName.create("io.helidon.service.inject.Binding"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectionPlanBinder}. + */ + public static final TypeName INJECT_PLAN_BINDER = + TypeName.create("io.helidon.service.inject.InjectionPlanBinder"); + /** * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InvocationException}. */ @@ -210,6 +251,22 @@ public class InjectCodegenTypes { public static final TypeName INJECT_G_EVENT_OBSERVER_REGISTRATION = TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.EventObserverRegistration"); + /** + * {@link io.helidon.common.types.TypeName} for String array. + */ + public static final TypeName STRING_ARRAY = TypeName.builder() + .from(TypeNames.STRING) + .array(true) + .build(); + /** + * {@link io.helidon.common.types.TypeName} for primitive double array. + */ + public static final TypeName DOUBLE_ARRAY = TypeName.builder() + .from(TypeNames.PRIMITIVE_DOUBLE) + .array(true) + .build(); + + private InjectCodegenTypes() { } } diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java index 66e8e72db63..41cf8b1ddfc 100644 --- a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java @@ -17,6 +17,7 @@ package io.helidon.service.inject.codegen; import java.util.Set; +import java.util.function.Function; import io.helidon.codegen.Option; import io.helidon.common.GenericType; @@ -48,6 +49,33 @@ public final class InjectOptions { TypeName::create, new GenericType>() { }); + /** + * Name of the generated Main class for Injection. Defaults to + * {@value ApplicationMainGenerator#CLASS_NAME}. + * The same property must be provided to the maven plugin, to correctly update the generated class. + * To configure package name, use {@link io.helidon.codegen.CodegenOptions#CODEGEN_PACKAGE} option. + */ + public static final Option APPLICATION_MAIN_CLASS_NAME = + Option.create("helidon.inject.application.main.class.name", + "Name of the generated Main class for Helidon Injection.", + ApplicationMainGenerator.CLASS_NAME, + Function.identity(), + GenericType.STRING); + + /** + * Whether to generate main class for Helidon Injection. + * Defaults to false. In case a custom main class is present (annotated with Injection.Main), this option is ignored. + *

    + * As main class only makes sense for the end application, this is set to {@code false} by default, so we do not generate + * main classes for library modules. + */ + public static final Option APPLICATION_MAIN_GENERATE = + Option.create("helidon.inject.application.main.generate", + "Whether to generate Main class for Helidon Injection.", + false, + Boolean::parseBoolean, + GenericType.create(Boolean.class)); + private InjectOptions() { } } diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtension.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtension.java index 2cef3307b2a..9ae22e16a18 100644 --- a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtension.java +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtension.java @@ -34,6 +34,7 @@ import io.helidon.codegen.CodegenOptions; import io.helidon.codegen.CodegenUtil; import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.ModuleInfo; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.codegen.classmodel.Field; @@ -70,6 +71,7 @@ import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_DESCRIBE; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_INJECT; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_MAIN; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_NAMED; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_PER_INSTANCE; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_PER_LOOKUP; @@ -79,6 +81,7 @@ import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_G_IP_SUPPORT; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_G_QUALIFIED_FACTORY_DESCRIPTOR; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_G_SCOPE_HANDLER_DESCRIPTOR; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_MAIN; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_SERVICE_INSTANCE; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPTION_DELEGATE; import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPTION_EXTERNAL_DELEGATE; @@ -117,6 +120,9 @@ class InjectionExtension implements RegistryCodegenExtension { private final Set scopeMetaAnnotations; private final List observers; + private volatile boolean mainClassGenerated; + private volatile String packageName; + InjectionExtension(RegistryCodegenContext codegenContext) { this.ctx = codegenContext; @@ -131,6 +137,9 @@ class InjectionExtension implements RegistryCodegenExtension { .stream() .map(it -> it.create(codegenContext)) .toList(); + this.packageName = CodegenOptions.CODEGEN_PACKAGE.findValue(options) + .orElse(null); + this.mainClassGenerated = !options.enabled(InjectOptions.APPLICATION_MAIN_GENERATE); } static void annotationsField(ClassModel.Builder classModel, TypeInfo service) { @@ -180,7 +189,29 @@ static List declareCtrParamsAndGetThem(Method.Builder method, L @Override public void process(RegistryRoundContext roundContext) { + if (this.packageName == null) { + // first try from module + this.packageName = ctx.module() + .flatMap(ModuleInfo::firstUnqualifiedExport) + .orElse(null); + // then use the first from source code + if (packageName == null) { + packageName = roundContext.types() + .stream() + .map(TypeInfo::typeName) + .map(TypeName::packageName) + .findFirst() + .orElse(null); + } + } + + Collection mainClasses = roundContext.annotatedTypes(INJECTION_MAIN); + if (!mainClasses.isEmpty()) { + generateMain(roundContext, mainClasses); + } + List descriptorsRequired = new ArrayList<>(roundContext.types()); + mainClasses.forEach(descriptorsRequired::remove); for (TypeInfo typeInfo : descriptorsRequired) { if (typeInfo.hasAnnotation(INTERCEPTION_EXTERNAL_DELEGATE)) { @@ -196,6 +227,13 @@ public void process(RegistryRoundContext roundContext) { notifyObservers(roundContext, descriptorsRequired); } + @Override + public void processingOver() { + if (!mainClassGenerated) { + generateMain(); + } + } + private static void addAnnotationValue(ContentBuilder contentBuilder, Object objectValue) { switch (objectValue) { case String value -> contentBuilder.addContent("\"" + value + "\""); @@ -389,6 +427,71 @@ private void generateScopeDescriptor(RegistryRoundContext roundContext, TypeInfo serviceDescriptor.typeInfo().originatingElementValue()); } + private void generateMain() { + if (packageName == null) { + throw new CodegenException("Cannot determine package name for the generated main class. " + + "Please use option " + CodegenOptions.CODEGEN_PACKAGE.name() + + " to specify it"); + } + // generate main class if it doe not exist + String className = InjectOptions.APPLICATION_MAIN_CLASS_NAME.value(ctx.options()); + TypeName generatedType = TypeName.builder() + .packageName(packageName) + .className(className) + .build(); + + ClassModel.Builder applicationMain = ApplicationMainGenerator.generate(GENERATOR, + Set.of(), + INJECT_MAIN, + generatedType, + false, + false, + (a, b, c) -> { + }, + (a, b, c) -> { + }); + ctx.filer() + .writeSourceFile(applicationMain.build(), GENERATOR); + } + + private void generateMain(RegistryRoundContext roundCtx, Collection customMainClasses) { + TypeInfo customMain = customMainClasses.iterator().next(); + + if (customMainClasses.size() != 1) { + String names = customMainClasses.stream() + .map(TypeInfo::typeName) + .map(TypeName::fqName) + .collect(Collectors.joining(", ")); + throw new CodegenException("There can only be one class annotated with " + INJECTION_MAIN.fqName() + ", " + + "but discovered more than one: " + names, + customMain.originatingElementValue()); + } + + // we always generate the main class, even when there is no Maven plugin + mainClassGenerated = true; + String className = InjectOptions.APPLICATION_MAIN_CLASS_NAME.value(ctx.options()); + TypeName generatedType = TypeName.builder() + .packageName(customMain.typeName().packageName()) + .className(className) + .build(); + ApplicationMainGenerator.validate(customMain); + var declaredSignatures = ApplicationMainGenerator.declaredSignatures(customMain); + + ClassModel.Builder applicationMain = ApplicationMainGenerator.generate(GENERATOR, + declaredSignatures, + customMain.typeName(), + generatedType, + true, + false, + (a, b, c) -> { + }, + (a, b, c) -> { + }); + roundCtx.addGeneratedType(generatedType, + applicationMain, + GENERATOR); + } + // we are generating source code, that requires multiple lines // I would rather keep this method readable, then to put multiple statements in a single line @SuppressWarnings("checkstyle:MethodLength") @@ -963,6 +1066,14 @@ private List fieldInjectElements(TypeInfo typeInfo) { .filter(ElementInfoPredicates::isPrivate) .findFirst(); if (firstFound.isPresent()) { + if (typeInfo.kind() == ElementKind.RECORD) { + throw new CodegenException("Discovered " + InjectCodegenTypes.INJECTION_INJECT.fqName() + + " annotation on record field(s). This is not supported. " + + "If this is the only constructor, you can remove the Inject annotation; " + + "if you need to inject the default constructor, kindly create an explicit" + + " default constructor and annotate it with Inject.", + firstFound.get().originatingElementValue()); + } throw new CodegenException("Discovered " + InjectCodegenTypes.INJECTION_INJECT.fqName() + " annotation on private field(s). We cannot support private field injection.", firstFound.get().originatingElementValue()); diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtensionProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtensionProvider.java index e34e787efaf..fa8b4979485 100644 --- a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtensionProvider.java +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtensionProvider.java @@ -71,7 +71,8 @@ public Set> supportedOptions() { public Set supportedAnnotations() { return Set.of(InjectCodegenTypes.INJECTION_INJECT, InjectCodegenTypes.INJECTION_DESCRIBE, - InjectCodegenTypes.INJECTION_PER_INSTANCE); + InjectCodegenTypes.INJECTION_PER_INSTANCE, + InjectCodegenTypes.INJECTION_MAIN); } @Override diff --git a/service/inject/inject/pom.xml b/service/inject/inject/pom.xml index 59f65966e6b..fab2a878377 100644 --- a/service/inject/inject/pom.xml +++ b/service/inject/inject/pom.xml @@ -39,6 +39,10 @@ + + io.helidon + helidon + io.helidon.common helidon-common @@ -55,6 +59,10 @@ io.helidon.common helidon-common-types + + io.helidon.logging + helidon-logging-common + io.helidon.builder helidon-builder-api diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectConfigBlueprint.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectConfigBlueprint.java index e021bb27c4d..3a0287053e0 100644 --- a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectConfigBlueprint.java +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectConfigBlueprint.java @@ -85,4 +85,15 @@ interface InjectConfigBlueprint extends ServiceRegistryConfig { @Option.Configured @Option.DefaultBoolean(true) boolean useBinding(); + + /** + * Maximal run level to handle when starting from the generated main class + * ({@link InjectionMain}). This setting is ignored when starting registry using + * other means, as run levels are not handled by default. + * + * @return maximal run level to lookup during application startup when using generated main class + */ + @Option.Configured + @Option.DefaultDouble(Double.MAX_VALUE) + double maxRunLevel(); } diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectStartupProvider.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectStartupProvider.java new file mode 100644 index 00000000000..58b13960320 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectStartupProvider.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject; + +import java.lang.System.Logger.Level; + +import io.helidon.Main; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.registry.ServiceRegistryManager; +import io.helidon.spi.HelidonShutdownHandler; +import io.helidon.spi.HelidonStartupProvider; + +/** + * {@link java.util.ServiceLoader} implementation of a Helidon startup provider for Helidon Service Inject based + * applications. + */ +@Weight(Weighted.DEFAULT_WEIGHT) // explicit default weight, this should be the "default" startup class +public class InjectStartupProvider extends InjectionMain implements HelidonStartupProvider { + /** + * Default constructor required by {@link java.util.ServiceLoader}. + * + * @deprecated please do not use directly + */ + @Deprecated + public InjectStartupProvider() { + } + + /** + * Register a shutdown handler. + * The handler is registered, and never de-registered. This method should only be used by main classes of applications. + * If a custom shutdown is desired, please use + * {@link io.helidon.Main#addShutdownHandler(io.helidon.spi.HelidonShutdownHandler)} directly. + * + * @param registryManager registry manager + */ + public static void registerShutdownHandler(ServiceRegistryManager registryManager) { + System.Logger logger = System.getLogger(InjectStartupProvider.class.getName()); + Main.addShutdownHandler(new InjectShutdownHandler(logger, registryManager)); + } + + @Override + public void start(String[] arguments) { + super.start(arguments); + } + + @Override + protected void serviceDescriptors(InjectConfig.Builder configBuilder) { + // service descriptors are discovered when startup provider is used - see below discoverServices() method + } + + @Override + protected boolean discoverServices() { + return true; + } + + // higher than default, so we stop server as a service, not through shutdown + @Weight(Weighted.DEFAULT_WEIGHT + 10) + private static final class InjectShutdownHandler implements HelidonShutdownHandler { + private final System.Logger logger; + private final ServiceRegistryManager registryManager; + + private InjectShutdownHandler(System.Logger logger, ServiceRegistryManager registryManager) { + this.logger = logger; + this.registryManager = registryManager; + } + + @Override + public void shutdown() { + try { + registryManager.shutdown(); + } catch (Exception e) { + logger.log(Level.ERROR, + "Failed to shutdown Helidon Inject registry", + e); + } + } + + @Override + public String toString() { + return "Helidon Inject shutdown handler"; + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionMain.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionMain.java new file mode 100644 index 00000000000..ac153fe9516 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionMain.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject; + +import java.lang.System.Logger.Level; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; +import io.helidon.common.types.TypeName; +import io.helidon.logging.common.LogConfig; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.registry.GlobalServiceRegistry; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceLoader__ServiceDescriptor; + +/** + * Common ancestor for generated main classes. + *

    + * The following methods are code generated to the {@code ApplicationMain} class by the Maven plugin. + * If you do not want the generated code to be used, simply implement those methods in your custom Main class, + * and they will NOT be generated. + * When not using Maven plugin, the generated class will have no-op implementations nad service discovery will be enabled. + *

      + *
    • {@link #serviceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} - registers all service descriptors + * in the current application; to customize the config builder, you can use + * {@link #beforeServiceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} and + * {@link #afterServiceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} methods
    • + *
    • {@link #discoverServices()} - set to {@code false} when using Maven plugin, to avoid all reflection
    • + *
    + */ +public abstract class InjectionMain { + /* + A change in this class requires change in ApplicationMainGenerator! + */ + + private static final System.Logger LOGGER = System.getLogger(InjectionMain.class.getName()); + + static { + LogConfig.initClass(); + } + + /** + * Default constructor with no side effects. + */ + protected InjectionMain() { + LogConfig.configureRuntime(); + } + + /** + * Create a service descriptor for a Java {@link java.util.ServiceLoader} based service. + * + * @param providerInterface provider interface of the service + * @param implType provider implementation + * @param instanceSupplier supplier of a new instance of the service implementation (to allow avoiding reflection) + * @param weight weight assigned to the service + * @param type of the service implementation + * @return a new service descriptor of the service + */ + protected static ServiceDescriptor serviceLoader(TypeName providerInterface, + Class implType, + Supplier instanceSupplier, + double weight) { + return ServiceLoader__ServiceDescriptor.create(providerInterface, implType, instanceSupplier, weight); + } + + /** + * Method that handles the startup sequence of an application. + * This method is expected to be code generated. + *

    + * The following sequence is implemented: + *

      + *
    1. {@link #configBuilder(String[])} to prepare the configuration builder from + * {@link io.helidon.common.config.Config}
    2. + *
    3. {@link #beforeServiceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} to update the builder
    4. + *
    5. {@link #serviceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} for code generated setup
    6. + *
    7. {@link #afterServiceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} to update the builder
    8. + *
    9. {@link #init(InjectConfig)} to initialize the service registry
    10. + *
    + * + * @param arguments command line arguments + */ + protected void start(String[] arguments) { + var config = configBuilder(arguments); + beforeServiceDescriptors(config); + serviceDescriptors(config); + afterServiceDescriptors(config); + var registry = init(config.build()); + afterInit(registry); + } + + /** + * Method that registers all service descriptors. + * This method is expected to be code generated. + * + * @param configBuilder in progress config builder + */ + protected abstract void serviceDescriptors(InjectConfig.Builder configBuilder); + + /** + * Create a config builder from command line arguments. + * This method will create default configuration (but not register it as a global config, so we still use the registry + * to create a config instance), configure the {@link io.helidon.service.inject.InjectConfig.Builder} from it, + * then disable discovery of services. + * + * @param arguments command line arguments + * @return a new config builder + */ + protected InjectConfig.Builder configBuilder(String[] arguments) { + Config config; + if (GlobalConfig.configured()) { + config = GlobalConfig.config(); + } else { + config = Config.create(); + } + + return InjectConfig.builder() + .config(config.get("registry")) + .discoverServices(discoverServices()) + .discoverServicesFromServiceLoader(discoverServices()); + } + + /** + * Whether to discover services from classpath and from service loader. + * Defaults to {@code false}, should be overridden when + * {@link #serviceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} + * does not configure all services. + * + * @return whether to discover services + */ + protected boolean discoverServices() { + return false; + } + + /** + * Called before service descriptors are configured in the generated main class. + * This method is invoked from generated {@link #start(String[])}. + * + * @param configBuilder in-progress config builder + */ + protected void beforeServiceDescriptors(InjectConfig.Builder configBuilder) { + } + + /** + * Called after service descriptors are configured in the generated main class, before the registry is initialized. + * This method is invoked from generated {@link #start(String[])} + * + * @param configBuilder in-progress config builder + */ + protected void afterServiceDescriptors(InjectConfig.Builder configBuilder) { + } + + /** + * Initialize the service registry, register it for shutdown during process shutdown, and lookup all + * startup services. + * + * @param config configuration of the Inject service registry + * @return the inject registry created within this method + */ + protected InjectRegistry init(InjectConfig config) { + InjectRegistryManager manager = InjectRegistryManager.create(config); + InjectRegistry registry = manager.registry(); + InjectStartupProvider.registerShutdownHandler(manager); + + GlobalServiceRegistry.registry(registry); + + double maxRunLevel = maxRunLevel(config, registry); + for (double runLevel : runLevels(config, registry)) { + if (runLevel <= maxRunLevel) { + List all = registry.all(Lookup.builder() + .addScope(Injection.Singleton.TYPE) + .runLevel(runLevel) + .build()); + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.DEBUG, "Starting services in run level: " + runLevel + ": "); + for (Object o : all) { + LOGGER.log(Level.DEBUG, "\t" + o); + } + } else if (LOGGER.isLoggable(Level.DEBUG)) { + LOGGER.log(Level.TRACE, "Starting services in run level: " + runLevel); + } + } + } + + return registry; + } + + /** + * Maximal run level to initialize. + * The default startup sequence will go through each + * {@link #runLevels(InjectConfig, io.helidon.service.inject.api.InjectRegistry) run level} up to (and including) the value + * returned by this method. + * + * @param config injection config + * @param registry registry instance + * @return maximal run level to initialize + */ + protected double maxRunLevel(InjectConfig config, + InjectRegistry registry) { + return config.maxRunLevel(); + } + + /** + * Run levels that should be initialized at startup. + * Default implementation initializes all declared run levels (services with explicit + * {@link io.helidon.service.inject.api.Injection.RunLevel} annotation). + * + * @param config injection config + * @param registry registry instance + * @return array of doubles that represent run levels to initialize, will be used in the order provided here + */ + protected double[] runLevels(InjectConfig config, + InjectRegistry registry) { + // child classes will have this method code generated at build time + List runLevelList = registry.lookupServices(Lookup.EMPTY) + .stream() + .map(InjectServiceInfo::runLevel) + .flatMap(Optional::stream) + .distinct() + .sorted() + .toList(); + double[] result = new double[runLevelList.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = runLevelList.get(i); + } + return result; + } + + /** + * Allows to query the registry after the service is started. + * + * @param registry inject service registry + */ + protected void afterInit(InjectRegistry registry) { + } +} diff --git a/service/inject/inject/src/main/java/module-info.java b/service/inject/inject/src/main/java/module-info.java index f3b6ee0cf16..26bc2a3d01a 100644 --- a/service/inject/inject/src/main/java/module-info.java +++ b/service/inject/inject/src/main/java/module-info.java @@ -28,6 +28,8 @@ module io.helidon.service.inject { requires static io.helidon.common.features.api; + requires io.helidon; + requires io.helidon.logging.common; requires io.helidon.metrics.api; requires io.helidon.service.metadata; @@ -42,4 +44,7 @@ provides io.helidon.service.registry.spi.ServiceRegistryManagerProvider with io.helidon.service.inject.InjectRegistryManagerProvider; + + provides io.helidon.spi.HelidonStartupProvider + with io.helidon.service.inject.InjectStartupProvider; } \ No newline at end of file diff --git a/service/inject/maven-plugin/README.md b/service/inject/maven-plugin/README.md new file mode 100644 index 00000000000..ca14faba7bc --- /dev/null +++ b/service/inject/maven-plugin/README.md @@ -0,0 +1,61 @@ +Maven Plugin +--- + +The Helidon Service Maven Plugin provides the following goals: + +1. Create application artifacts (`create-application`, `create-test-application`) + +# Create application artifacts Maven goals + +This goal creates artifacts that are only valid for the service (assembled from libraries and its own sources). +This goal generates: + +1. Application Binding - a mapping of services to injection points (to bypass runtime lookups) - generates class `Injection__Binding` +2. Application Main - a generated main class that registers all services (to bypass service discovery) - generates class `ApplicationMain` + +Usage of this plugin goal is not required, yet it is recommended for final application module, it will add +- binding for injection points, to avoid runtime lookups +- explicit registration of all services into `InjectConfig`, to avoid resource lookup and reflection at runtime +- overall speedup of bootstrapping, as all the required tasks to start a service registry are code generated + +This goal should not be used for library modules (i.e. modules that do not have a Main class that bootstraps registry). + +## Usage + +Goal names: + +- `create-application` - for production sources +- `create-test-application` - for test sources (only creates binding, main class not relevant) + +Configuration options: + +| Name | Property | Default | Description | +|--------------------|-------------------------------------------------|------------------------|---------------------------------------------------------------------------------| +| `packageName` | `helidon.codegen.package-name` | Inferred from module | Package to put the generated classes in | +| `moduleName` | `helidon.codegen.module-name` | Inferred from module | Name of the JPMS module | +| `validate` | `helidon.inject.application.validate` | `true` | Whether to validate application | +| `createMain` | `helidon.inject.application.main.generate` | `true` | Whether to create application Main class | +| `mainClassName` | `helidon.inject.application.main.class.name` | `ApplicationMain` | Name of the generated Main class | +| `createBinding` | `helidon.inject.application.binding.generate` | `true` | Whether to create application binding | +| `bindingClassName` | `helidon.inject.application.binding.class.name` | `Application__Binding` | Name of the generated binding class, for test, it is `TestApplication__Binding` | +| `failOnError` | `helidon.inject.fail-on-error` | `true` | Whether to fail when the plugin encounters an error | +| `failOnWarning` | `helidon.inject.fail-on-warning` | `false` | Whether to fail when the plugin encounters a warning | +| `compilerArgs` | | | Arguments of the Java compiler (both classes are compiled by the plugin) | + +Configuration example in `pom.xml`: + +```xml + + + io.helidon.service.inject + helidon-service-inject-maven-plugin + + + create-application + + create-application + + + + +``` diff --git a/service/inject/maven-plugin/etc/spotbugs/exclude.xml b/service/inject/maven-plugin/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..e672fbdf494 --- /dev/null +++ b/service/inject/maven-plugin/etc/spotbugs/exclude.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/service/inject/maven-plugin/pom.xml b/service/inject/maven-plugin/pom.xml new file mode 100644 index 00000000000..7e320213b06 --- /dev/null +++ b/service/inject/maven-plugin/pom.xml @@ -0,0 +1,160 @@ + + + + + io.helidon.service.inject + helidon-service-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-inject-maven-plugin + Helidon Service Maven Plugin + maven-plugin + + + etc/spotbugs/exclude.xml + + + + + + org.apache.maven.plugins + maven-plugin-plugin + + + + report + + + + + + + + + + org.apache.maven + maven-artifact + provided + + + org.apache.maven + maven-model + provided + + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-types + + + io.helidon.codegen + helidon-codegen-class-model + + + io.helidon.service + helidon-service-registry + + + io.helidon.service + helidon-service-codegen + + + io.helidon.service.inject + helidon-service-inject-codegen + + + io.helidon.service + helidon-service-metadata + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-scan + + + io.helidon.codegen + helidon-codegen-compiler + + + io.github.classgraph + classgraph + + + org.apache.maven + maven-plugin-api + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + provided + + + org.apache.maven + maven-project + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + -proc:none + + + + org.apache.maven.plugins + maven-plugin-plugin + + helidon-inject + false + + + + help-goal + + helpmojo + + + + + + + + diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/ApplicationValidator.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/ApplicationValidator.java new file mode 100644 index 00000000000..e5545f21777 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/ApplicationValidator.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.lang.System.Logger.Level; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenLogger; +import io.helidon.common.Errors; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Ip; +import io.helidon.service.inject.api.Lookup; + +class ApplicationValidator { + private final MavenCodegenContext scanContext; + private final boolean failOnWarning; + + ApplicationValidator(MavenCodegenContext scanContext, boolean failOnWarning) { + this.scanContext = scanContext; + this.failOnWarning = failOnWarning; + } + + void validate(WrappedServices services) { + Errors.Collector collector = Errors.collector(); + + validate(services, collector); + + Errors errors = collector.collect(); + CodegenLogger logger = scanContext.logger(); + for (Errors.ErrorMessage error : errors) { + Level level = switch (error.getSeverity()) { + case FATAL -> Level.ERROR; + case WARN -> Level.WARNING; + case HINT -> Level.TRACE; + }; + logger.log(level, error.getSeverity() + " " + error.getSource() + ": " + error.getMessage()); + } + + if (errors.hasFatal() || failOnWarning && errors.hasWarning()) { + throw new CodegenException("Application validation failed, see log output for details."); + } + } + + private void validate(WrappedServices services, Errors.Collector collector) { + // check all singletons, that they only contain injection points that are singletons, or have a supplier + List requestScopedServices = services.all(Lookup.builder() + .addScope(Injection.PerRequest.TYPE) + .build()); + Set requestScopedContracts = new HashSet<>(); + Map> requestScopedByContracts = new HashMap<>(); + + for (InjectServiceInfo requestScoped : requestScopedServices) { + TypeName serviceType = requestScoped.serviceType(); + Set contracts = requestScoped.contracts(); + + ResolvedType resolvedServiceType = ResolvedType.create(serviceType); + requestScopedContracts.add(resolvedServiceType); + requestScopedByContracts.computeIfAbsent(resolvedServiceType, k -> new HashSet<>()) + .add(serviceType); + + requestScopedContracts.addAll(contracts); + for (ResolvedType contract : contracts) { + requestScopedByContracts.computeIfAbsent(contract, k -> new HashSet<>()) + .add(serviceType); + } + } + + List singletons = services.all(Lookup.builder() + .addScope(Injection.Singleton.TYPE) + .build()); + + boolean requestScopeHinted = false; + + for (InjectServiceInfo singleton : singletons) { + for (Ip dependency : singleton.dependencies()) { + ResolvedType contract = ResolvedType.create(dependency.contract()); + if (requestScopedContracts.contains(contract)) { + // this is an injection of request scope service into a singleton + if (dependency.typeName().isSupplier()) { + // this is correct + if (!requestScopeHinted) { + collector.hint( + "Injection of request scoped service into a singleton (as a supplier). This is correct, " + + "please " + + "make sure you have appropriate request scope library on your module path."); + requestScopeHinted = true; + } + } else { + // this does not have to be an error, if the user decides the whole application has a request + // scope active and they implement their own request scope initialization + collector.warn("Injection of request scoped service into a singleton without a supplier. " + + " Singleton: " + singleton.serviceType() + ", request scoped service(s): " + + requestScopedByContracts.get(contract)); + } + } + } + } + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/BindingGenerator.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/BindingGenerator.java new file mode 100644 index 00000000000..93f3ea286a6 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/BindingGenerator.java @@ -0,0 +1,589 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.compiler.Compiler; +import io.helidon.codegen.compiler.CompilerOptions; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.service.codegen.DescriptorClassCode; +import io.helidon.service.codegen.GenerateServiceDescriptor; +import io.helidon.service.codegen.HelidonMetaInfServices; +import io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.Ip; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.metadata.DescriptorMetadata; +import io.helidon.service.registry.ServiceLoader__ServiceDescriptor; + +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_CONTRACT; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_POINT_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_BINDING; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_PLAN_BINDER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_SERVICE_INSTANCE; + +/** + * The default implementation for {@link BindingGenerator}. + */ +class BindingGenerator { + private static final TypeName GENERATOR = TypeName.create(BindingGenerator.class); + + private final MavenCodegenContext ctx; + private final boolean failOnError; + + BindingGenerator(MavenCodegenContext scanContext, boolean failOnError) { + this.ctx = scanContext; + this.failOnError = failOnError; + } + + /** + * Generates the source and class file for {@code io.helidon.inject.Binding} using the current classpath. + * + * @param injectionServices injection services to use + * @param serviceTypes types to process + * @param typeName generated binding type name + * @param moduleName name of the module of this maven module + * @param compilerOptions compilation options + */ + void createBinding(WrappedServices injectionServices, + Set serviceTypes, + TypeName typeName, + String moduleName, + CompilerOptions compilerOptions) { + Objects.requireNonNull(injectionServices); + Objects.requireNonNull(serviceTypes); + + try { + codegen(injectionServices, serviceTypes, typeName, moduleName, compilerOptions); + } catch (CodegenException ce) { + handleError(ce); + } catch (Throwable te) { + handleError(new CodegenException("Failed to code generate binding class", te)); + } + } + + void codegen(WrappedServices injectionServices, + Set serviceTypes, + TypeName typeName, + String moduleName, + CompilerOptions compilerOptions) { + ClassModel.Builder classModel = ClassModel.builder() + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .copyright(CodegenUtil.copyright(GENERATOR, + GENERATOR, + typeName)) + .description("Generated Binding to provide explicit bindings for known services.") + .type(typeName) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + GENERATOR, + typeName, + "1", + "")) + .addInterface(INJECT_BINDING); + + // deprecated default constructor - binding should always be service loaded + classModel.addConstructor(ctr -> ctr + .accessModifier(AccessModifier.PACKAGE_PRIVATE)); + + // public String name() + classModel.addMethod(nameMethod -> nameMethod + .addAnnotation(Annotations.OVERRIDE) + .returnType(io.helidon.common.types.TypeNames.STRING) + .name("name") + .addContentLine("return \"" + moduleName + "\";")); + + // public void configure(ServiceInjectionPlanBinder binder) + classModel.addMethod(configureMethod -> configureMethod + .addAnnotation(Annotations.OVERRIDE) + .name("configure") + // constructors of services for service loader are usually deprecated in Helidon + .addAnnotation(Annotation.create(SuppressWarnings.class, "deprecation")) + .addParameter(binderParam -> binderParam + .name("binder") + .type(INJECT_PLAN_BINDER)) + .update(it -> createConfigureMethodBody(injectionServices, + serviceTypes, + it))); + + Path generated = ctx.filer() + .writeSourceFile(classModel.build()); + + TypeInfo appTypeInfo = createAppTypeInfo(typeName); + RegistryCodegenContext registryCodegenContext = RegistryCodegenContext.create(ctx); + MavenRoundContext roundContext = new MavenRoundContext(ctx); + GenerateServiceDescriptor.generate(GENERATOR, + registryCodegenContext, + roundContext, + List.of(appTypeInfo), + appTypeInfo); + + List toCompile = new ArrayList<>(); + toCompile.add(generated); + + HelidonMetaInfServices services = HelidonMetaInfServices.create(ctx.filer(), moduleName); + + for (DescriptorClassCode descriptor : roundContext.descriptors()) { + Path path = ctx.filer().writeSourceFile(descriptor.classCode().classModel().build(), + descriptor.classCode().originatingElements()); + toCompile.add(path); + + services.add(DescriptorMetadata.create(descriptor.registryType(), + descriptor.classCode().newType(), + descriptor.weight(), + descriptor.contracts(), + Set.of())); + } + + services.write(); + Compiler.compile(compilerOptions, toCompile.toArray(new Path[0])); + } + + BindingPlan bindingPlan(WrappedServices services, + TypeName serviceTypeName) { + + Lookup lookup = toLookup(serviceTypeName); + InjectServiceInfo sp = services.get(lookup); + TypeName serviceDescriptorType = sp.descriptorType(); + + if (!isQualifiedInjectionTarget(sp)) { + return new BindingPlan(serviceDescriptorType, Set.of()); + } + + List dependencies = sp.dependencies(); + if (dependencies.isEmpty()) { + return new BindingPlan(serviceDescriptorType, Set.of()); + } + + Set bindings = new LinkedHashSet<>(); + for (Ip dependency : dependencies) { + InjectionPlan iPlan = injectionPlan(services, sp, dependency); + List qualified = iPlan.qualifiedProviders(); + List unqualified = iPlan.unqualifiedProviders(); + List usedList; + + if (qualified.isEmpty() && !unqualified.isEmpty()) { + usedList = unqualified; + } else { + usedList = qualified; + } + + bindings.add(new Binding(dependency, + usedList)); + } + + return new BindingPlan(serviceDescriptorType, bindings); + } + + private static Consumer> toContentBuilder(InjectServiceInfo serviceInfo) { + if (serviceInfo.coreInfo() instanceof ServiceLoader__ServiceDescriptor sl) { + // we need to create a specific descriptor for interface and implementation + TypeName providerInterface = sl.providerInterface(); + TypeName providerImpl = sl.serviceType(); + return it -> it.addContent(sl.descriptorType()) + .addContent(".create(") + .addContentCreate(providerInterface) + .addContent(", ") + .addContent(providerImpl) + .addContent(".class, ") + .addContent(providerImpl) + .addContent("::new, ") + .addContent(String.valueOf(sl.weight())) + .addContent(")"); + } else { + // the usual singleton instance + return it -> it.addContent(serviceInfo.descriptorType().fqName()) + .addContent(".INSTANCE"); + } + } + + private static Lookup toLookup(TypeName typeName) { + return Lookup.builder() + .serviceType(typeName) + .build(); + } + + /** + * Determines if the service is valid to receive injections. + * + * @param sp the service provider + * @return true if the service provider can receive injection + */ + private static boolean isQualifiedInjectionTarget(InjectServiceInfo sp) { + Set contractsImplemented = sp.contracts(); + List dependencies = sp.dependencies(); + + if (contractsImplemented.contains(ResolvedType.create(INJECT_BINDING))) { + return false; + } + boolean hasDependencies = !dependencies.isEmpty(); + boolean hasContract = !contractsImplemented.isEmpty(); + + return hasContract || hasDependencies; + } + + private TypeInfo createAppTypeInfo(TypeName typeName) { + return TypeInfo.builder() + .kind(ElementKind.CLASS) + .typeName(typeName) + // to trigger generation of descriptor + .addAnnotation(Annotation.create(SERVICE_ANNOTATION_PROVIDER)) + .addInterfaceTypeInfo(TypeInfo.builder() + .kind(ElementKind.INTERFACE) + .typeName(INJECT_BINDING) + .addAnnotation(Annotation.create(SERVICE_ANNOTATION_CONTRACT)) + .build()) + .build(); + } + + private void handleError(CodegenException ce) { + if (failOnError) { + throw ce; + } else { + ctx.logger().log(ce.toEvent(System.Logger.Level.WARNING)); + } + } + + private InjectionPlan injectionPlan(WrappedServices services, + InjectServiceInfo self, + Ip dependency) { + /* + very similar code is used in ServiceManager.planForIp + make sure this is kept in sync! + */ + Lookup dependencyTo = Lookup.create(dependency); + Set qualifiers = dependencyTo.qualifiers(); + if (self.contracts().containsAll(dependencyTo.contracts()) && self.qualifiers().equals(qualifiers)) { + /* + lookup must have a single contract for each injection point + if this service implements the contracts actually required, we must look for services with lower weight + but only if we also have the same qualifiers; + this is to ensure that if an injection point in a service injects a contract the service implements itself, + we do not end up in an infinite loop, but look for a service with a lower weight to satisfy that injection point + this allows us to "chain" a single contract through multiple services + */ + dependencyTo = Lookup.builder(dependencyTo) + .weight(self.weight()) + .build(); + } + + /* + An injection point can be satisfied by: + 1. a service that matches the type or contract, and qualifiers match + 2. a Supplier service, where T matches service type or contract, and qualifiers match + 3. an InjectionPointProvider, where T matches service type or contract, regardless of qualifiers + 4. an InjectionResolver, where the method resolve returns an information if this type can be resolved (config driven) + */ + + List qualifiedProviders = services.all(dependencyTo); + List unqualifiedProviders; + + if (qualifiedProviders.isEmpty()) { + unqualifiedProviders = injectionPointProvidersFor(services, dependency) + .stream() + .filter(it -> !it.serviceType().equals(self.serviceType())) + .toList(); + } else { + unqualifiedProviders = List.of(); + } + + // remove current service provider from matches + qualifiedProviders = qualifiedProviders.stream() + .filter(it -> !it.serviceType().equals(self.serviceType())) + .toList(); + + // the list now contains all providers that match the processed injection points + return new InjectionPlan(unqualifiedProviders, qualifiedProviders); + } + + private List injectionPointProvidersFor(WrappedServices services, Ip injectionPoint) { + if (injectionPoint.qualifiers().isEmpty()) { + return List.of(); + } + Lookup criteria = Lookup.builder(Lookup.create(injectionPoint)) + .qualifiers(Set.of()) // remove qualifier from lookup + .addContract(INJECTION_POINT_FACTORY) // only search for injection point providers + .build(); + return services.all(criteria); + } + + private void createConfigureMethodBody(WrappedServices services, + Set serviceTypes, + Method.Builder method) { + // find all interceptors and bind them + List interceptors = + services.all(Lookup.builder() + .addContract(Interception.Interceptor.class) + .addQualifier(Qualifier.WILDCARD_NAMED) + .build()); + method.addContent("binder.interceptors("); + boolean multiline = interceptors.size() > 2; + if (multiline) { + method.addContentLine("") + .increaseContentPadding(); + } + + Iterator interceptorIterator = interceptors.iterator(); + while (interceptorIterator.hasNext()) { + method.addContent(interceptorIterator.next().descriptorType()) + .addContent(".INSTANCE"); + if (interceptorIterator.hasNext()) { + method.addContent(","); + if (multiline) { + method.addContentLine(""); + } else { + method.addContent(" "); + } + } + } + + if (multiline) { + method.addContentLine("") + .decreaseContentPadding(); + } + method.addContentLine(");") + .addContentLine(""); + + // first collect required dependencies by descriptor + Map> injectionPlan = new LinkedHashMap<>(); + for (TypeName serviceType : serviceTypes) { + BindingPlan plan = bindingPlan(services, serviceType); + if (!plan.bindings.isEmpty()) { + injectionPlan.put(plan.descriptorType(), plan.bindings()); + } + } + + boolean supportNulls = false; + // we group all bindings by descriptor they belong to + injectionPlan.forEach((descriptorType, bindings) -> { + method.addContent("binder.bindTo(") + .addContent(descriptorType.genericTypeName()) + .addContentLine(".INSTANCE)") + .increaseContentPadding(); + + for (Binding binding : bindings) { + Consumer> ipId = content -> content + .addContent(binding.injectionPoint().descriptor().genericTypeName()) + .addContent(".") + .addContent(binding.injectionPoint.descriptorConstant()); + + buildTimeBinding(method, binding, ipId, supportNulls); + } + + /* + Commit the dependencies + */ + method.addContentLine(".commit();") + .decreaseContentPadding() + .addContentLine(""); + }); + } + + /* + Very similar code is used for runtime discovery in ServiceProvider.planForIp + make sure this is doing the same thing! + Here we code generate the calls to the binding class + */ + private void buildTimeBinding(Method.Builder method, + Binding binding, + Consumer> ipId, + boolean supportNulls) { + + Ip injectionPoint = binding.injectionPoint(); + List discovered = binding.descriptors(); + Iterator>> descriptors = discovered.stream() + .map(BindingGenerator::toContentBuilder) + .iterator(); + + TypeName ipType = injectionPoint.typeName(); + + // now there are a few options - optional, list, and single instance + if (ipType.isList()) { + TypeName typeOfList = ipType.typeArguments().getFirst(); + if (typeOfList.isSupplier()) { + // inject List> + method.addContent(".bindListOfSuppliers("); + } else if (typeOfList.equals(INJECT_SERVICE_INSTANCE)) { + method.addContent(".bindServiceInstanceList("); + } else { + // inject List + method.addContent(".bindList("); + } + method.update(ipId::accept); + + if (discovered.isEmpty()) { + method.addContentLine(")"); + } else { + method.addContent(", ") + .update(it -> { + while (descriptors.hasNext()) { + descriptors.next().accept(it); + if (descriptors.hasNext()) { + it.addContent(", "); + } + } + }) + .addContentLine(")"); + } + } else if (ipType.isOptional()) { + TypeName typeOfOptional = ipType.typeArguments().getFirst(); + if (typeOfOptional.isSupplier()) { + // inject Optional> + method.addContent(".bindOptionalOfSupplier("); + } else if (typeOfOptional.equals(INJECT_SERVICE_INSTANCE)) { + // inject Optional> + method.addContent(".bindOptionalOfServiceInstance("); + } else { + // inject Optional + method.addContent(".bindOptional("); + } + method.update(ipId::accept); + + if (discovered.isEmpty()) { + method.addContentLine(")"); + } else { + method.addContent(", "); + descriptors.next().accept(method); + method.addContentLine(")"); + } + } else if (ipType.isSupplier()) { + // one of the supplier options + + TypeName typeOfSupplier = ipType.typeArguments().getFirst(); + if (typeOfSupplier.isOptional()) { + // inject Supplier> + method.addContent(".bindSupplierOfOptional(") + .update(ipId::accept); + if (discovered.isEmpty()) { + method.addContentLine(")"); + } else { + method.addContent(", "); + descriptors.next().accept(method); + method.addContentLine(")"); + } + } else if (typeOfSupplier.isList()) { + // inject Supplier> + method.addContent(".bindSupplierOfList(") + .update(ipId::accept); + if (discovered.isEmpty()) { + method.addContentLine(")"); + } else { + method.addContent(", ") + .update(it -> { + while (descriptors.hasNext()) { + descriptors.next().accept(it); + if (descriptors.hasNext()) { + it.addContent(", "); + } + } + }) + .addContentLine(")"); + } + } else { + // inject Supplier + method.addContent(".bindSupplier(") + .update(ipId::accept); + + if (discovered.isEmpty()) { + // null binding is not supported at runtime + throw new CodegenException("Injection point requires a value, but no provider discovered: " + + injectionPoint); + } + method.addContent(", "); + descriptors.next().accept(method); + method.addContentLine(")"); + } + } else if (ipType.equals(INJECT_SERVICE_INSTANCE)) { + // inject Contract + if (discovered.isEmpty()) { + if (supportNulls) { + method.addContent(".bindNull(") + .update(ipId::accept) + .addContentLine(")"); + } else { + // null binding is not supported at runtime + throw new CodegenException("Injection point requires a value, but no provider discovered: " + + injectionPoint); + } + } else { + method.addContent(".bindServiceInstance(") + .update(ipId::accept) + .addContent(", ") + .update(descriptors.next()::accept) + .addContentLine(")"); + } + } else { + // inject Contract + if (discovered.isEmpty()) { + if (supportNulls) { + method.addContent(".bindNull(") + .update(ipId::accept) + .addContentLine(")"); + } else { + // null binding is not supported at runtime + throw new CodegenException("Injection point requires a value, but no provider discovered: " + + injectionPoint); + } + } else { + method.addContent(".bind(") + .update(ipId::accept) + .addContent(", ") + .update(descriptors.next()::accept) + .addContentLine(")"); + } + } + } + + record InjectionPlan(List unqualifiedProviders, + List qualifiedProviders) { + } + + record BindingPlan(TypeName descriptorType, + Set bindings) { + } + + /** + * @param injectionPoint to bind to + * @param descriptors matching descriptors + */ + record Binding(Ip injectionPoint, + List descriptors) { + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CodegenAbstractMojo.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CodegenAbstractMojo.java new file mode 100644 index 00000000000..2a1ac7f0f59 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CodegenAbstractMojo.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.util.List; +import java.util.Optional; + +import io.helidon.codegen.CodegenOptions; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +/** + * Abstract base for all codegen goals. + */ +abstract class CodegenAbstractMojo extends AbstractMojo { + /** + * Tag controlling whether we fail on error. + */ + static final String TAG_FAIL_ON_ERROR = "helidon.inject.fail-on-error"; + + /** + * Tag controlling whether we fail on warnings. + */ + static final String TAG_FAIL_ON_WARNING = "helidon.inject.fail-on-warning"; + + // ---------------------------------------------------------------------- + // Configurables + // ---------------------------------------------------------------------- + /** + * The module name to apply. If not found the module name will be inferred + * from {@code module-info.java} if present, or defined as {@code unnamed/package name}. + * + * @see io.helidon.codegen.CodegenOptions#TAG_CODEGEN_MODULE + */ + @Parameter(property = CodegenOptions.TAG_CODEGEN_MODULE) + private String moduleName; + /** + * The package name to apply. If not found the package name will be inferred. + * + * @see io.helidon.codegen.CodegenOptions#TAG_CODEGEN_PACKAGE + */ + @Parameter(property = CodegenOptions.TAG_CODEGEN_PACKAGE) + private String packageName; + /** + * Indicates whether the build will continue even if there are compilation errors. + */ + @Parameter(property = TAG_FAIL_ON_ERROR, defaultValue = "true") + private boolean failOnError; + /** + * Indicates whether the build will continue even if there are any warnings. + */ + @Parameter(property = TAG_FAIL_ON_WARNING) + private boolean failOnWarning; + /** + * Sets the arguments to be passed to the compiler. + *

    + * Example: + *

    +     * <compilerArgs>
    +     *   <arg>-Xmaxerrs</arg>
    +     *   <arg>1000</arg>
    +     *   <arg>-Xlint</arg>
    +     *   <arg>-J-Duser.language=en_us</arg>
    +     * </compilerArgs>
    +     * 
    + */ + @Parameter + private List compilerArgs; + + // ---------------------------------------------------------------------- + // Generic Configurables + // ---------------------------------------------------------------------- + + /** + * The current project instance. This is used for propagating generated-sources paths as + * compile/testCompile source roots. + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + /** + * Default constructor. + */ + CodegenAbstractMojo() { + } + + @Override + public final void execute() throws MojoExecutionException, MojoFailureException { + try { + innerExecute(); + } catch (MojoFailureException | MojoExecutionException e) { + if (failOnError) { + throw e; + } + getLog().warn("Failed to process " + getClass().getSimpleName(), e); + } catch (Throwable t) { + if (failOnError) { + throw new MojoExecutionException(t); + } + getLog().warn("Failed to process " + getClass().getSimpleName(), t); + } + } + + /** + * Handle execution of this plugin. The {@link #execute()} method handles exceptions according to + * {@code failOnError} configuration. + * + * @throws org.apache.maven.plugin.MojoExecutionException as needed + * @throws org.apache.maven.plugin.MojoFailureException as needed + */ + abstract void innerExecute() throws MojoExecutionException, MojoFailureException; + + /** + * The target package name. + * + * @return the target package name, if configured + */ + Optional packageNameFromMavenConfig() { + return Optional.ofNullable(packageName); + } + + /** + * The module name of current module. + * + * @return the module name, if configured + */ + Optional moduleNameFromMavenConfig() { + return Optional.ofNullable(moduleName); + } + + /** + * The Maven project. + * + * @return maven project + */ + MavenProject mavenProject() { + return project; + } + + /** + * Whether to fail on error. + * Handled in {@link #execute()} by default. + * + * @return if processing should fail on error + */ + boolean failOnError() { + return failOnError; + } + + /** + * Whether to fail on warning. + * + * @return if processing should fail on warning + */ + boolean failOnWarning() { + return failOnWarning; + } + + /** + * List of compiler arguments (expected to start with {@code -A}). + * + * @return compiler arguments + */ + List getCompilerArgs() { + return compilerArgs == null ? List.of() : compilerArgs; + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateApplicationAbstractMojo.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateApplicationAbstractMojo.java new file mode 100644 index 00000000000..144e26b9d24 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateApplicationAbstractMojo.java @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.CodegenScope; +import io.helidon.codegen.ModuleInfo; +import io.helidon.codegen.ModuleInfoSourceParser; +import io.helidon.codegen.compiler.CompilerOptions; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.codegen.ApplicationMainGenerator; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +/** + * Abstract base for the Injection {@code maven-plugin} responsible for creating + * {@code Binding}, Test {@code Binding}, and application Main class. + */ +abstract class CreateApplicationAbstractMojo extends CodegenAbstractMojo { + /** + * Class name of the binding class generated by Maven plugin (for end user application). + */ + protected static final String BINDING_CLASS_NAME = "Injection__Binding"; + /** + * Name of the generated main class. + */ + @Parameter(property = "helidon.inject.application.main.class.name", + defaultValue = ApplicationMainGenerator.CLASS_NAME) + private String mainClassName; + /** + * The -source argument for the Java compiler. + * Note: using the same as maven-compiler for convenience and least astonishment. + */ + @Parameter(property = "maven.compiler.source", + defaultValue = "21") + private String release; + /** + * Whether to validate the application when creating its bindings. + */ + @Parameter(property = "helidon.inject.application.validate", + defaultValue = "true") + private boolean validate; + /** + * Whether to generate binding class (provides generated injection plan for all services). + */ + @Parameter(property = "helidon.inject.application.binding.generate", + defaultValue = "true") + private boolean generateBinding; + + /** + * Default constructor. + */ + CreateApplicationAbstractMojo() { + } + + @Override + void innerExecute() { + MavenLogger mavenLogger = MavenLogger.create(getLog(), failOnWarning()); + + boolean hasModuleInfo = hasModuleInfo(); + Set modulepath = hasModuleInfo ? getModulepathElements() : Set.of(); + Set classpath = getClasspathElements(); + ClassLoader prev = Thread.currentThread().getContextClassLoader(); + URLClassLoader loader = createClassLoader(classpath, prev); + getLog().debug("Service registry classpath: " + classpath); + + Optional nonTestModuleInfo = findModuleInfo(nonTestSourceRootPaths()) + .map(ModuleInfoSourceParser::parse); + + /* + We may have module info both in sources and in tests + */ + Optional myModuleInfo = findModuleInfo(sourceRootPaths()) + .map(ModuleInfoSourceParser::parse); + CodegenOptions codegenOptions = MavenOptions.create(toOptions()); + CodegenScope scope = scope(); + codegenOptions.validate(Set.of()); + + // package name to use (should be the same as ModuleComponent package) + String packageName = packageName(codegenOptions, myModuleInfo, nonTestModuleInfo); + // module name to use to define application name (should be the same as ModuleComponent uses for this module) + String moduleName = moduleName(loader, codegenOptions, myModuleInfo, packageName, scope); + + try (ScanResult scan = new ClassGraph() + .overrideClasspath(classpath) + .enableAllInfo() + .scan()) { + MavenCodegenContext scanContext = MavenCodegenContext.create(codegenOptions, + scan, + scope, + generatedSourceDirectory(), + outputDirectory(), + mavenLogger, + myModuleInfo.orElse(null)); + + Thread.currentThread().setContextClassLoader(loader); + + CompilerOptions compilerOptions = CompilerOptions.builder() + .classpath(List.copyOf(classpath)) + .modulepath(List.copyOf(modulepath)) + .sourcepath(sourceRootPaths()) + .release(javaRelease()) + .commandLineArguments(getCompilerArgs()) + .outputDirectory(outputDirectory()) + .build(); + + applicationBinding(loader, + mavenLogger, + scanContext, + compilerOptions, + moduleName, + packageName); + + if (createMain()) { + // we need to re-create the classloader, as we have created new classes for binding + loader = createClassLoader(classpath, prev); + createMain(loader, + mavenLogger, + scanContext, + compilerOptions, + packageName); + } + } finally { + Thread.currentThread().setContextClassLoader(prev); + } + } + + void createMain(ClassLoader loader, + MavenLogger mavenLogger, + MavenCodegenContext scanContext, + CompilerOptions compilerOptions, + String packageName) { + try (WrappedServices services = WrappedServices.create(loader, mavenLogger, false)) { + createMainClass(compilerOptions, + scanContext, + services, + packageName); + } catch (CodegenException e) { + throw e; + } catch (Exception e) { + throw new CodegenException("An error occurred creating the main class in " + getClass().getName(), e); + } + } + + void applicationBinding(ClassLoader loader, + MavenLogger mavenLogger, + MavenCodegenContext scanContext, + CompilerOptions compilerOptions, + String moduleName, + String packageName) { + try (WrappedServices services = WrappedServices.create(loader, mavenLogger, false)) { + applicationBinding(scanContext, + services, + compilerOptions, + moduleName, + packageName); + } catch (CodegenException e) { + throw e; + } catch (Exception e) { + throw new CodegenException("An error occurred creating the binding in " + getClass().getName(), e); + } + } + + void createMainClass(CompilerOptions compilerOptions, + MavenCodegenContext scanContext, + WrappedServices services, + String packageName) { + TypeName generatedType = TypeName.builder() + .packageName(packageName) + .className(mainClassName) + .build(); + + getLog().info("Generating application main class: " + generatedType.fqName()); + + MainClassCreator creator = new MainClassCreator(scanContext, failOnError()); + creator.create(scanContext, compilerOptions, services, generatedType); + } + + void applicationBinding(MavenCodegenContext scanContext, + WrappedServices services, + CompilerOptions compilerOptions, + String moduleName, + String packageName) { + + // retrieves all the services in the registry + Set allServices = services.all() + .stream() + .map(InjectServiceInfo::serviceType) + .collect(Collectors.toCollection(TreeSet::new)); + + if (allServices.isEmpty()) { + warn("Binding generator found no services to process"); + return; + } + + getLog().debug("All services to be processed: " + allServices); + + String className = bindingClassName(); + + if (validate) { + // validate the application + ApplicationValidator validator = new ApplicationValidator(scanContext, failOnWarning()); + validator.validate(services); + } + + if (generateBinding) { + // get the binding generator only after services are initialized (we need to ignore any existing apps) + BindingGenerator creator = new BindingGenerator(scanContext, failOnError()); + TypeName bindingTypeName = TypeName.create(packageName + "." + className); + + getLog().info("Generating application binding: " + bindingTypeName.fqName()); + + creator.createBinding(services, + allServices, + bindingTypeName, + moduleName, + compilerOptions); + } + } + + /** + * Where to generate sources. As this directory differs between production code and test code, it must be provided + * by a subclass. + * + * @return where to generate sources + */ + abstract Path generatedSourceDirectory(); + + /** + * Binding class name to be generated. + * + * @return binding class name + */ + abstract String bindingClassName(); + + /** + * Output directory for this {@link #scope()}. + * + * @return output directory + */ + abstract Path outputDirectory(); + + abstract boolean createMain(); + + /** + * Source roots for this {@link #scope()}. + * + * @return source roots + */ + List sourceRootPaths() { + return nonTestSourceRootPaths(); + } + + /** + * Production source roots for this project. + * + * @return source roots for production code + */ + List nonTestSourceRootPaths() { + MavenProject project = mavenProject(); + List result = new ArrayList<>(project.getCompileSourceRoots().size()); + for (Object a : project.getCompileSourceRoots()) { + result.add(Path.of(a.toString())); + } + return result; + } + + /** + * Test source roots for this project. + * + * @return source roots for test code + */ + protected List testSourceRootPaths() { + MavenProject project = mavenProject(); + List result = new ArrayList<>(project.getTestCompileSourceRoots().size()); + for (Object a : project.getTestCompileSourceRoots()) { + result.add(Path.of(a.toString())); + } + return result; + } + + LinkedHashSet getModulepathElements() { + return getSourceClasspathElements(); + } + + boolean hasModuleInfo() { + return sourceRootPaths() + .stream() + .anyMatch(p -> Files.exists(p.resolve(ModuleInfo.FILE_NAME))); + } + + Optional findModuleInfo(List sourcePaths) { + return sourcePaths.stream() + .map(it -> it.resolve(ModuleInfo.FILE_NAME)) + .filter(Files::exists) + .findFirst(); + } + + void warn(String msg) { + getLog().warn(msg); + + if (failOnWarning()) { + throw new CodegenException(msg); + } + } + + /** + * The scope of the code generation (production, test etc.). + * + * @return codegen scope + */ + abstract CodegenScope scope(); + + /** + * Creates a new classloader. + * + * @param classPath the classpath to use + * @param parent the parent loader + * @return the loader + */ + URLClassLoader createClassLoader(Collection classPath, + ClassLoader parent) { + List urls = new ArrayList<>(classPath.size()); + for (Path dependency : classPath) { + try { + urls.add(dependency.toUri().toURL()); + } catch (MalformedURLException e) { + throw new CodegenException("Unable to build the classpath. Dependency cannot be converted to URL: " + + dependency, + e); + } + } + + if (parent == null) { + parent = Thread.currentThread().getContextClassLoader(); + } + return new URLClassLoader(urls.toArray(new URL[0]), parent); + } + + String javaRelease() { + return release; + } + + LinkedHashSet getSourceClasspathElements() { + MavenProject project = mavenProject(); + LinkedHashSet result = new LinkedHashSet<>(project.getCompileArtifacts().size()); + result.add(Paths.get(project.getBuild().getOutputDirectory())); + for (Object a : project.getCompileArtifacts()) { + result.add(((Artifact) a).getFile().toPath()); + } + return result; + } + + /** + * Provides a convenient way to handle test scope. Returns the classpath for source files (or test sources) only. + */ + LinkedHashSet getClasspathElements() { + return getSourceClasspathElements(); + } + + // to dot separated path + private static String toDotSeparated(Path relativePath) { + return StreamSupport.stream(relativePath.spliterator(), false) + .map(Path::toString) + .collect(Collectors.joining(".")); + } + + private String packageName(CodegenOptions codegenOptions, + Optional myModuleInfo, + Optional srcModuleInfo) { + return CodegenOptions.CODEGEN_PACKAGE + .findValue(codegenOptions) + .or(() -> myModuleInfo.flatMap(this::exportedPackage)) + .or(() -> srcModuleInfo.flatMap(this::exportedPackage)) + .or(this::firstUsedPackage) + .orElseThrow(() -> new CodegenException("Unable to determine package for binding class.")); + } + + private Optional firstUsedPackage() { + // we expect at least some source code. If none found, try test source, if none found, must be configured + return firstUsedPackage(nonTestSourceRootPaths()) + .or(() -> firstUsedPackage(testSourceRootPaths())); + } + + private Optional firstUsedPackage(List sourceRoots) { + Set found = new TreeSet<>(Comparator.comparing(String::length)); + + for (Path sourceRoot : sourceRoots) { + try { + try (Stream pathStream = Files.walk(sourceRoot)) { + pathStream + .filter(it -> it.getFileName().toString().endsWith(".java")) + .map(it -> packageName(sourceRoot, it)) + .filter(Predicate.not(String::isBlank)) + .forEach(found::add); + } + } catch (IOException e) { + getLog().debug("Failed to walk path tree for source root: " + sourceRoot.toAbsolutePath(), + e); + } + } + return found.stream() + .findFirst(); + } + + private Optional exportedPackage(ModuleInfo moduleInfo) { + Set unqualifiedExports = new TreeSet<>(Comparator.comparing(String::length)); + moduleInfo.exports() + .forEach((export, to) -> { + if (to.isEmpty()) { + unqualifiedExports.add(export); + } + }); + return unqualifiedExports.stream().findFirst(); + } + + private String moduleName(ClassLoader loader, + CodegenOptions codegenOptions, + Optional myModuleInfo, + String packageName, + CodegenScope scope) { + return CodegenOptions.CODEGEN_MODULE + .findValue(codegenOptions) + .or(() -> myModuleInfo.map(ModuleInfo::name)) + .orElseGet(() -> "unnamed/" + + packageName + + (scope.isProduction() ? "" : "/" + scope.name())); + } + + private String packageName(Path rootPath, Path filePath) { + Path parent = filePath.getParent(); + if (parent == null) { + return ""; + } + return toDotSeparated(rootPath.relativize(parent)); + } + + private Set toOptions() { + Set options = new HashSet<>(getCompilerArgs()); + + moduleNameFromMavenConfig().ifPresent(it -> options.add("-A" + CodegenOptions.TAG_CODEGEN_MODULE + "=" + it)); + packageNameFromMavenConfig().ifPresent(it -> options.add("-A" + CodegenOptions.TAG_CODEGEN_PACKAGE + "=" + it)); + + return options; + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateApplicationMojo.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateApplicationMojo.java new file mode 100644 index 00000000000..a6bedc507cc --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateApplicationMojo.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.io.File; +import java.nio.file.Path; + +import io.helidon.codegen.CodegenScope; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * Maven goal to create application bindings (a mapping of services that satisfy injection points), + * and to create application main class (reflection-free registration of services). + */ +@Mojo(name = "create-application", + defaultPhase = LifecyclePhase.COMPILE, + threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE) +public class CreateApplicationMojo extends CreateApplicationAbstractMojo { + + /** + * Specify where to place generated source files created by annotation processing. + */ + @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations") + private File generatedSourcesDirectory; + + /** + * The directory for compiled classes. + */ + @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) + private File outputDirectory; + + /** + * Whether to generate main class. Default name is {@code ApplicationMain} in the same package as + * the generated application. + */ + @Parameter(property = "helidon.inject.application.main.generate", + defaultValue = "true") + private boolean generateMain; + + /** + * Name of the generated binding class. + */ + @Parameter(property = "helidon.inject.application.binding.class.name", + defaultValue = BINDING_CLASS_NAME) + private String bindingClassName; + + /** + * Default constructor. + */ + public CreateApplicationMojo() { + } + + @Override + protected Path generatedSourceDirectory() { + return generatedSourcesDirectory.toPath(); + } + + @Override + protected Path outputDirectory() { + return outputDirectory.toPath(); + } + + @Override + protected CodegenScope scope() { + return CodegenScope.PRODUCTION; + } + + @Override + String bindingClassName() { + return bindingClassName; + } + + @Override + boolean createMain() { + return generateMain; + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateTestApplicationMojo.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateTestApplicationMojo.java new file mode 100644 index 00000000000..637ef1f1ea3 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/CreateTestApplicationMojo.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.io.File; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; + +import io.helidon.codegen.CodegenScope; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; + +/** + * A mojo wrapper to {@link BindingGenerator} for test specific types. + * For test scope, we only generate binding, as main class would not be useful. + */ +@Mojo(name = "test-application-create", defaultPhase = LifecyclePhase.TEST_COMPILE, threadSafe = true, + requiresDependencyResolution = ResolutionScope.TEST) +@SuppressWarnings("unused") +public class CreateTestApplicationMojo extends CreateApplicationAbstractMojo { + + /** + * Name of the generated binding class. + */ + @Parameter(property = "helidon.inject.application.binding.classname", + defaultValue = "Test" + BINDING_CLASS_NAME) + private String bindingClassName; + + /** + * Specify where to place generated source files created by annotation processing. + * Only applies to JDK 1.6+ + */ + @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations") + private File generatedTestSourcesDirectory; + + /** + * The directory where compiled test classes go. + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true) + private File testOutputDirectory; + + /** + * Default constructor. + */ + public CreateTestApplicationMojo() { + } + + @Override + protected Path generatedSourceDirectory() { + return generatedTestSourcesDirectory.toPath(); + } + + @Override + protected Path outputDirectory() { + return testOutputDirectory.toPath(); + } + + @Override + protected List sourceRootPaths() { + return testSourceRootPaths(); + } + + @Override + protected LinkedHashSet getClasspathElements() { + MavenProject project = mavenProject(); + LinkedHashSet result = new LinkedHashSet<>(project.getTestArtifacts().size()); + result.add(new File(project.getBuild().getTestOutputDirectory()).toPath()); + for (Object a : project.getTestArtifacts()) { + result.add(((Artifact) a).getFile().toPath()); + } + result.addAll(super.getClasspathElements()); + return result; + } + + @Override + LinkedHashSet getModulepathElements() { + return getClasspathElements(); + } + + @Override + protected String bindingClassName() { + return bindingClassName; + } + + @Override + protected CodegenScope scope() { + return new CodegenScope("test"); + } + + @Override + boolean createMain() { + return false; + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MainClassCreator.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MainClassCreator.java new file mode 100644 index 00000000000..027b00f67a3 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MainClassCreator.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.compiler.Compiler; +import io.helidon.codegen.compiler.CompilerOptions; +import io.helidon.codegen.scan.ScanContext; +import io.helidon.codegen.scan.ScanTypeInfoFactory; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection.RunLevel; +import io.helidon.service.inject.codegen.ApplicationMainGenerator; +import io.helidon.service.metadata.DescriptorMetadata; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceDiscovery; +import io.helidon.service.registry.ServiceLoader__ServiceDescriptor; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_MAIN; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_RUN_LEVEL; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_BINDING; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_MAIN; + +class MainClassCreator { + private static final TypeName GENERATOR = TypeName.create(MainClassCreator.class); + private static final String INJECTION_MAIN_ANNOTATION = INJECTION_MAIN.packageName() + + "." + + INJECTION_MAIN.classNameWithEnclosingNames().replace('.', '$'); + + private final MavenCodegenContext ctx; + private final boolean failOnError; + + MainClassCreator(MavenCodegenContext scanContext, boolean failOnError) { + this.ctx = scanContext; + this.failOnError = failOnError; + } + + void create(MavenCodegenContext scanContext, + CompilerOptions compilerOptions, + WrappedServices services, + TypeName generatedType) { + try { + codegen(scanContext, compilerOptions, services, generatedType); + } catch (CodegenException ce) { + handleError(ce); + } catch (Throwable te) { + handleError(new CodegenException("Failed to code generate application main class", te)); + } + } + + private void codegen(ScanContext scanContext, + CompilerOptions compilerOptions, + WrappedServices services, + TypeName generatedType) { + // if there is a custom main class present, we must honor it + Optional foundCustomMain = findCustomMain(scanContext, compilerOptions.outputDirectory()); + Set declaredSignatures; + TypeName superType; + + if (foundCustomMain.isPresent()) { + TypeInfo customMain = foundCustomMain.get(); + ApplicationMainGenerator.validate(customMain); + declaredSignatures = ApplicationMainGenerator.declaredSignatures(customMain); + superType = customMain.typeName(); + } else { + declaredSignatures = Set.of(); + superType = INJECT_MAIN; + } + + ClassModel classModel = + ApplicationMainGenerator.generate(GENERATOR, + declaredSignatures, + superType, + generatedType, + false, + true, + (classBuilder, methodModel, paramName) -> serviceDescriptors(classBuilder, + methodModel, + paramName, + services), + (classBuilder, methodModel, paramName) -> runLevels( + methodModel, + services)) + .build(); + + Path generated = ctx.filer() + .writeSourceFile(classModel); + + Compiler.compile(compilerOptions, generated); + } + + private Optional findCustomMain(ScanContext scanContext, Path targetPath) { + // we do not want to search the whole application, just the current module. Main class MUST be in the module that + // uses the maven plugin + try (ScanResult scan = new ClassGraph() + .overrideClasspath(Set.of(targetPath)) + .enableAllInfo() + .scan()) { + ClassInfoList customMainClasses = scan.getClassesWithAnnotation(INJECTION_MAIN_ANNOTATION); + if (customMainClasses.isEmpty()) { + return Optional.empty(); + } + if (customMainClasses.size() > 1) { + String names = customMainClasses.stream() + .map(ClassInfo::getName) + .collect(Collectors.joining(", ")); + throw new CodegenException("There can only be one class annotated with " + INJECTION_MAIN.fqName() + ", " + + "but discovered more than one: " + names); + } + return ScanTypeInfoFactory.create(scanContext, customMainClasses.getFirst()); + } + } + + private void runLevels(Method.Builder method, + WrappedServices services) { + Set runLevels = new TreeSet<>(); + + for (InjectServiceInfo serviceInfo : services.all()) { + if (serviceInfo.runLevel().isPresent()) { + runLevels.add(serviceInfo.runLevel().get()); + } + } + + List runLevelList = List.copyOf(runLevels); + + for (int i = 0; i < runLevelList.size(); i++) { + double current = runLevelList.get(i); + if (Double.compare(RunLevel.STARTUP, current) == 0) { + method.addContent(INJECTION_RUN_LEVEL) + .addContent(".STARTUP"); + } else if (Double.compare(RunLevel.SERVER, current) == 0) { + method.addContent(INJECTION_RUN_LEVEL) + .addContent(".SERVER"); + } else if (Double.compare(RunLevel.NORMAL, current) == 0) { + method.addContent(INJECTION_RUN_LEVEL) + .addContent(".NORMAL"); + } else { + method.addContent(String.valueOf(current)) + .addContent("D"); + } + if (i == runLevelList.size() - 1) { + method.addContentLine(""); + } else { + method.addContentLine(","); + } + } + } + + private void serviceDescriptors(ClassModel.Builder classModel, + Method.Builder method, + String paramName, + WrappedServices services) { + List serviceLoaded = new ArrayList<>(); + List serviceDescriptors = new ArrayList<>(); + + // bindings must be added directly from service discovery, as they are not part of the registry + addBindings(serviceDescriptors); + + // for each discovered service, add it to the configuration + for (InjectServiceInfo serviceInfo : services.all()) { + if (serviceInfo.coreInfo() instanceof ServiceLoader__ServiceDescriptor sl) { + serviceLoaded.add(sl); + } else { + serviceDescriptors.add(serviceInfo.descriptorType()); + } + } + + Map providerConstants = new HashMap<>(); + AtomicInteger constantCounter = new AtomicInteger(); + + serviceLoaded.stream() + .sorted(serviceLoaderComparator()) + .forEach(it -> addServiceLoader(classModel, method, providerConstants, constantCounter, it)); + + if (!serviceLoaded.isEmpty() && !serviceDescriptors.isEmpty()) { + // visually separate service loaded services from service descriptors + method.addContentLine(""); + } + + // config.addServiceDescriptor(ImperativeFeature__ServiceDescriptor.INSTANCE); + serviceDescriptors.stream().sorted() + .forEach(it -> method.addContent(paramName) + .addContent(".addServiceDescriptor(") + .addContent(it) + .addContentLine(".INSTANCE);")); + } + + private void addBindings(List serviceDescriptors) { + ResolvedType binding = ResolvedType.create(INJECT_BINDING); + + ServiceDiscovery.create() + .allMetadata() + .stream() + .filter(it -> it.contracts().contains(binding)) + .map(DescriptorMetadata::descriptorType) + .forEach(serviceDescriptors::add); + } + + private void addServiceLoader(ClassModel.Builder classModel, + Method.Builder main, + Map providerConstants, + AtomicInteger constantCounter, + ServiceLoader__ServiceDescriptor sl) { + // Generated code: + // config.addServiceDescriptor(serviceLoader(PROVIDER_1, + // YamlConfigParser.class, + // () -> new io.helidon.config.yaml.YamlConfigParser(), + // 90.0)); + TypeName providerInterface = sl.providerInterface(); + String constantName = providerConstants.computeIfAbsent(providerInterface, it -> { + int i = constantCounter.getAndIncrement(); + String constant = "PROVIDER_" + i; + classModel.addField(field -> field + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(TypeNames.TYPE_NAME) + .name(constant) + .addContentCreate(providerInterface)); + return constant; + }); + + main.addContent("config") + .addContent(".addServiceDescriptor(serviceLoader(") + .addContent(constantName) + .addContentLine(",") + .increaseContentPadding() + .increaseContentPadding() + .increaseContentPadding() + .addContent(sl.serviceType()).addContentLine(".class,") + .addContent("() -> new ").addContent(sl.serviceType()).addContentLine("(),") + .addContent(String.valueOf(sl.weight())) + .addContentLine("));") + .decreaseContentPadding() + .decreaseContentPadding() + .decreaseContentPadding(); + } + + private Comparator serviceLoaderComparator() { + return Comparator.comparing(ServiceLoader__ServiceDescriptor::providerInterface) + .thenComparing(ServiceDescriptor::serviceType); + } + + private void handleError(CodegenException ce) { + if (failOnError) { + throw ce; + } else { + ctx.logger().log(ce.toEvent(System.Logger.Level.WARNING)); + } + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenCodegenContext.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenCodegenContext.java new file mode 100644 index 00000000000..f5bffbf0b37 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenCodegenContext.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.CodegenContextBase; +import io.helidon.codegen.CodegenLogger; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.CodegenScope; +import io.helidon.codegen.ModuleInfo; +import io.helidon.codegen.scan.ScanContext; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import io.github.classgraph.ScanResult; + +class MavenCodegenContext extends CodegenContextBase implements CodegenContext, ScanContext { + private final ModuleInfo module; + private final MavenFiler filer; + private final ScanResult scanResult; + + protected MavenCodegenContext(CodegenOptions options, + MavenFiler filer, + CodegenLogger logger, + CodegenScope scope, + ScanResult scanResult, + ModuleInfo module) { + super(options, Set.of(), filer, logger, scope); + + this.module = module; + this.filer = filer; + this.scanResult = scanResult; + } + + static MavenCodegenContext create(CodegenOptions options, + ScanResult scanResult, + CodegenScope scope, + Path generatedSourceDir, + Path outputDirectory, + MavenLogger logger, + ModuleInfo module /* may be null*/) { + return new MavenCodegenContext(options, + MavenFiler.create(generatedSourceDir, outputDirectory), + logger, + scope, + scanResult, + module); + } + + @Override + public Optional module() { + return Optional.ofNullable(module); + } + + @Override + public MavenFiler filer() { + return filer; + } + + @Override + public Optional typeInfo(TypeName typeName) { + return Optional.empty(); + } + + @Override + public Optional typeInfo(TypeName typeName, Predicate elementPredicate) { + return Optional.empty(); + } + + @Override + public ScanResult scanResult() { + return scanResult; + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFiler.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFiler.java new file mode 100644 index 00000000000..1de4e40a56b --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFiler.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenFiler; +import io.helidon.codegen.FilerResource; +import io.helidon.codegen.FilerTextResource; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeName; + +class MavenFiler implements CodegenFiler { + private final Path generatedSourceDir; + private final Path outputDirectory; + private boolean generatedSources; + + MavenFiler(Path generatedSourceDir, Path outputDirectory) { + this.generatedSourceDir = generatedSourceDir; + this.outputDirectory = outputDirectory; + } + + static MavenFiler create(Path generatedSourceDir, Path outputDirectory) { + return new MavenFiler(generatedSourceDir, outputDirectory); + } + + @Override + public Path writeSourceFile(TypeName typeName, String content, Object... originatingElements) { + String pathToSourceFile = typeName.packageName().replace('.', '/'); + String fileName = typeName.className() + ".java"; + Path path = generatedSourceDir.resolve(pathToSourceFile) + .resolve(fileName); + Path parentDir = path.getParent(); + if (parentDir != null) { + mkdirs(parentDir); + } + + try (Writer writer = Files.newBufferedWriter(path, + StandardCharsets.UTF_8, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) { + writer.write(content); + generatedSources = true; + } catch (IOException e) { + throw new CodegenException("Failed to write new source file: " + path.toAbsolutePath(), e, typeName); + } + return path; + } + + @Override + public Path writeSourceFile(ClassModel classModel, Object... originatingElements) { + try { + StringWriter sw = new StringWriter(); + classModel.write(sw); + return writeSourceFile(classModel.typeName(), sw.toString(), originatingElements); + } catch (IOException e) { + throw new CodegenException("Failed to write new source file: " + classModel.typeName(), e, classModel.typeName()); + } + } + + @Override + public Path writeResource(byte[] resource, String location, Object... originatingElements) { + Path path = outputDirectory.resolve(location); + Path parentDir = path.getParent(); + if (parentDir != null) { + mkdirs(parentDir); + } + try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) { + out.write(resource); + } catch (IOException e) { + throw new CodegenException("Failed to write new resource file: " + path.toAbsolutePath(), location); + } + return path; + } + + @Override + public FilerTextResource textResource(String location, Object... originatingElements) { + Path resourcePath = outputDirectory.resolve(location); + Path parentDir = resourcePath.getParent(); + if (parentDir != null) { + mkdirs(parentDir); + } + if (Files.exists(resourcePath)) { + try { + return new MavenFilerTextResource(resourcePath, Files.readAllLines(resourcePath)); + } catch (IOException e) { + throw new CodegenException("Failed to read existing text resource: " + resourcePath.toAbsolutePath(), e); + } + } else { + return new MavenFilerTextResource(resourcePath); + } + } + + @Override + public FilerResource resource(String location, Object... originatingElements) { + Path resourcePath = outputDirectory.resolve(location); + Path parentDir = resourcePath.getParent(); + if (parentDir != null) { + mkdirs(parentDir); + } + if (Files.exists(resourcePath)) { + try { + return new MavenFilerResource(resourcePath, Files.readAllBytes(resourcePath)); + } catch (IOException e) { + throw new CodegenException("Failed to read existing resource: " + resourcePath.toAbsolutePath(), e); + } + } else { + return new MavenFilerResource(resourcePath); + } + } + + boolean generatedSources() { + return generatedSources; + } + + private void mkdirs(Path path) { + try { + Files.createDirectories(path); + } catch (IOException e) { + throw new CodegenException("Failed to create directories for: " + path.toAbsolutePath()); + } + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFilerResource.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFilerResource.java new file mode 100644 index 00000000000..284ea4ab585 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFilerResource.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.FilerResource; + +class MavenFilerResource implements FilerResource { + private final Path resourcePath; + + private byte[] currentBytes; + private boolean modified; + + MavenFilerResource(Path resourcePath) { + this(resourcePath, new byte[0]); + } + + MavenFilerResource(Path resourcePath, byte[] bytes) { + this.resourcePath = resourcePath; + this.currentBytes = bytes; + } + + @Override + public byte[] bytes() { + return Arrays.copyOf(currentBytes, currentBytes.length); + } + + @Override + public void bytes(byte[] newBytes) { + currentBytes = Arrays.copyOf(newBytes, newBytes.length); + modified = true; + } + + @Override + public void write() { + if (modified) { + try { + Files.write(resourcePath, currentBytes, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + throw new CodegenException("Failed to write resource " + resourcePath.toAbsolutePath(), e); + } + } + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFilerTextResource.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFilerTextResource.java new file mode 100644 index 00000000000..14a0cbcf7ca --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenFilerTextResource.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.FilerTextResource; + +class MavenFilerTextResource implements FilerTextResource { + private final Path resourcePath; + private final ArrayList currentLines; + + private boolean modified; + + MavenFilerTextResource(Path resourcePath) { + this.resourcePath = resourcePath; + this.currentLines = new ArrayList<>(); + } + + MavenFilerTextResource(Path resourcePath, List lines) { + this.resourcePath = resourcePath; + this.currentLines = new ArrayList<>(lines); + } + + @Override + public List lines() { + return List.copyOf(currentLines); + } + + @Override + public void lines(List newLines) { + currentLines.clear(); + currentLines.addAll(newLines); + modified = true; + } + + @Override + public void write() { + if (modified) { + try { + Files.write(resourcePath, currentLines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + throw new CodegenException("Failed to write resource " + resourcePath.toAbsolutePath(), e); + } + } + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenLogger.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenLogger.java new file mode 100644 index 00000000000..334cbb958ad --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenLogger.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.CodegenLogger; +import io.helidon.common.types.TypeName; + +import io.github.classgraph.ClassInfo; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodInfo; +import org.apache.maven.plugin.logging.Log; + +class MavenLogger implements CodegenLogger { + private final Log log; + private final List warnings = new CopyOnWriteArrayList<>(); + private final List errors = new CopyOnWriteArrayList<>(); + private final Consumer warningConsumer; + + private MavenLogger(Log log, boolean failOnWarning) { + this.log = log; + if (failOnWarning) { + // keep them + warningConsumer = warnings::add; + } else { + // throw away + warningConsumer = it -> { + }; + } + } + + public static MavenLogger create(Log log, boolean failOnWarning) { + return new MavenLogger(log, failOnWarning); + } + + @Override + public void log(CodegenEvent event) { + String message = toMessage(event); + + switch (event.level()) { + case TRACE, DEBUG -> log(log::debug, log::debug, event, message); + case INFO -> log(log::info, log::info, event, message); + case WARNING -> { + warningConsumer.accept(message); + log(log::warn, log::warn, event, message); + } + case ERROR -> { + errors.add(message); + log(log::error, log::error, event, message); + } + default -> { + } + } + } + + boolean hasErrors() { + return !errors.isEmpty() && !warnings.isEmpty(); + } + + List messages() { + return Stream.concat( + errors.stream() + .map(it -> "error: " + it), + warnings.stream() + .map(it -> "warning: " + it) + ) + .toList(); + } + + private void log(Consumer messageLog, + BiConsumer throwableLog, + CodegenEvent event, + String message) { + Optional throwable = event.throwable(); + if (throwable.isPresent()) { + throwableLog.accept(message, throwable.get()); + } else { + messageLog.accept(message); + } + } + + private String toMessage(CodegenEvent event) { + List objects = event.objects(); + if (objects.isEmpty()) { + return event.message(); + } + return event.message() + ", originating in: " + objects.stream() + .map(this::toString) + .collect(Collectors.joining(", ")); + } + + private String toString(Object object) { + if (object instanceof TypeName type) { + return type.fqName(); + } + if (object instanceof ClassInfo ci) { + return ci.getName(); + } + if (object instanceof MethodInfo mi) { + return mi.getClassInfo().getName() + "#" + mi.toStringWithSimpleNames(); + } + if (object instanceof FieldInfo fi) { + return fi.getClassInfo().getName() + "." + fi.getName(); + } + return String.valueOf(object); + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenOptions.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenOptions.java new file mode 100644 index 00000000000..eb393e30fa0 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenOptions.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.Option; + +class MavenOptions implements CodegenOptions { + private final Map options; + + private MavenOptions(Map options) { + this.options = options; + } + + static CodegenOptions create(Set compilerArgs) { + Map options = new HashMap<>(); + + compilerArgs.forEach(it -> addInjectOption(options, it)); + + return new MavenOptions(Map.copyOf(options)); + } + + @Override + public Optional option(String option) { + return Optional.ofNullable(options.get(option)).map(String::trim); + } + + @Override + public void validate(Set> permittedOptions) { + Set helidonOptions = options + .keySet() + .stream() + .filter(it -> it.startsWith("helidon.")) + .collect(Collectors.toSet()); + + // now remove all expected + permittedOptions.stream() + .map(Option::name) + .forEach(helidonOptions::remove); + + helidonOptions.remove(CODEGEN_SCOPE.name()); + helidonOptions.remove(CODEGEN_MODULE.name()); + helidonOptions.remove(CODEGEN_PACKAGE.name()); + helidonOptions.remove(INDENT_TYPE.name()); + helidonOptions.remove(INDENT_COUNT.name()); + helidonOptions.remove(CREATE_META_INF_SERVICES.name()); + + if (!helidonOptions.isEmpty()) { + throw new CodegenException("Unrecognized/unsupported Helidon option configured: " + helidonOptions); + } + } + + private static void addInjectOption(Map options, String option) { + String toProcess = option; + if (toProcess.startsWith("-A")) { + toProcess = toProcess.substring(2); + } + int eq = toProcess.indexOf('='); + if (eq < 0) { + options.put(toProcess, "true"); + return; + } + options.put(toProcess.substring(0, eq), toProcess.substring(eq + 1)); + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenRoundContext.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenRoundContext.java new file mode 100644 index 00000000000..23cfa7802ee --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/MavenRoundContext.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.codegen.ClassCode; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.service.codegen.DescriptorClassCode; +import io.helidon.service.codegen.RegistryRoundContext; +import io.helidon.service.codegen.ServiceContracts; + +class MavenRoundContext implements RegistryRoundContext { + private final List descriptors = new ArrayList<>(); + private final MavenCodegenContext ctx; + + MavenRoundContext(MavenCodegenContext ctx) { + this.ctx = ctx; + } + + @Override + public void addDescriptor(String registryType, + TypeName serviceType, + TypeName descriptorType, + ClassModel.Builder descriptor, + double weight, + Set contracts, + Set factoryContracts, + Object... originatingElements) { + ClassCode cc = new ClassCode(descriptorType, + descriptor, + serviceType, + originatingElements); + descriptors.add(DescriptorClassCode.create(cc, registryType, weight, contracts, factoryContracts)); + } + + @Override + public ServiceContracts serviceContracts(TypeInfo serviceInfo) { + return ServiceContracts.create(ctx.options(), + this::typeInfo, + serviceInfo); + } + + @Override + public Collection availableAnnotations() { + return List.of(); + } + + @Override + public Collection types() { + return List.of(); + } + + @Override + public Collection annotatedTypes(TypeName annotationType) { + return List.of(); + } + + @Override + public Collection annotatedElements(TypeName annotationType) { + return List.of(); + } + + @Override + public void addGeneratedType(TypeName type, + ClassModel.Builder newClass, + TypeName mainTrigger, + Object... originatingElements) { + + } + + @Override + public Optional generatedType(TypeName type) { + return Optional.empty(); + } + + @Override + public Optional typeInfo(TypeName typeName) { + return ctx.typeInfo(typeName); + } + + public List descriptors() { + return descriptors; + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/WrappedServices.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/WrappedServices.java new file mode 100644 index 00000000000..20bebf70b01 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/WrappedServices.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.inject.maven.plugin; + +import java.lang.System.Logger.Level; +import java.util.List; + +import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenLogger; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_CONFIG; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_REGISTRY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_REGISTRY_MANAGER; + +class WrappedServices implements AutoCloseable { + private final ClassLoader classLoader; + private final CodegenLogger logger; + private final Class injectionServicesType; + private final Class servicesType; + private final Object injectionServices; + private final Object services; + + private WrappedServices(ClassLoader classLoader, + CodegenLogger logger, + Class injectionServicesType, + Class servicesType, + Object injectionServices, + Object services) { + this.classLoader = classLoader; + this.logger = logger; + this.injectionServicesType = injectionServicesType; + this.servicesType = servicesType; + this.injectionServices = injectionServices; + this.services = services; + } + + static WrappedServices create(ClassLoader classLoader, CodegenLogger logger, boolean useBindings) { + try { + /* + Phase.GATHERING_DEPENDENCIES + */ + + Activator.Phase limitPhase = Activator.Phase.ACTIVATION_STARTING; + + /* + InjectionConfig.builder()....build(); + */ + Class injectConfigType = classLoader.loadClass(INJECT_CONFIG.fqName()); + Object injectConfigBuilder = injectConfigType.getMethod("builder") + .invoke(null); + Class injectConfigBuilderType = injectConfigBuilder.getClass(); + + injectConfigBuilderType.getMethod("useBinding", boolean.class) + .invoke(injectConfigBuilder, useBindings); + injectConfigBuilderType.getMethod("limitRuntimePhase", Activator.Phase.class) + .invoke(injectConfigBuilder, limitPhase); + Object injectionConfig = injectConfigBuilderType.getMethod("build") + .invoke(injectConfigBuilder); + + /* + InjectRegistryManager registryManager = InjectRegistryManager.create(injectionConfig) + InjectRegistry services = injectionServices.registry(); + */ + Class injectionServicesType = classLoader.loadClass(INJECT_REGISTRY_MANAGER.fqName()); + Object injectionServices = injectionServicesType.getMethod("create", injectConfigType) + .invoke(null, injectionConfig); + Class servicesType = classLoader.loadClass(INJECT_REGISTRY.fqName()); + Object services = injectionServicesType.getMethod("registry") + .invoke(injectionServices); + + return new WrappedServices(classLoader, + logger, + injectionServicesType, + servicesType, + injectionServices, + services); + } catch (ReflectiveOperationException e) { + throw new CodegenException( + "Failed to invoke Service registry related methods in user's application class loader using reflection", + e); + } + } + + @Override + public void close() { + try { + injectionServicesType.getMethod("shutdown") + .invoke(injectionServices); + } catch (ReflectiveOperationException e) { + logger.log(CodegenEvent.builder() + .level(Level.WARNING) + .message("Failed to shutdown services used from Maven plugin") + .throwable(e) + .build()); + } + } + + List all() { + return all(Lookup.EMPTY); + } + + @SuppressWarnings("unchecked") + List all(Lookup lookup) { + try { + // retrieves all the services in the registry + return (List) servicesType.getMethod("lookupServices", Lookup.class) + .invoke(services, lookup); + } catch (ReflectiveOperationException e) { + throw new CodegenException("Failed to get providers from service registry using reflection", e); + } + } + + InjectServiceInfo get(Lookup lookup) { + List services = all(lookup); + if (services.size() == 1) { + return services.getFirst(); + } + if (services.isEmpty()) { + throw new CodegenException("Expected that service registry contains service: " + lookup + ", yet none was found"); + } + + throw new CodegenException("Expected that service registry contains service: " + lookup + + ", yet more than one was found: " + services); + } +} diff --git a/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/package-info.java b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/package-info.java new file mode 100644 index 00000000000..1f616eb568e --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/io/helidon/service/inject/maven/plugin/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Maven plugin for Helidon Service Inject. + *

    + * This plugin should be used by the application - i.e. the actual microservice that is going to be deployed and started. + * This plugin will not help when used on a library. + *

    + * The plugin generates application binding (mapping of services to injection points they satisfy), and application main class + * to avoid lookups (binding), and reflection and resources discovery from classpath (main class). + */ +package io.helidon.service.inject.maven.plugin; diff --git a/service/inject/maven-plugin/src/main/java/module-info.java b/service/inject/maven-plugin/src/main/java/module-info.java new file mode 100644 index 00000000000..2922be8f334 --- /dev/null +++ b/service/inject/maven-plugin/src/main/java/module-info.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Injection maven-plugin module. + */ +module io.helidon.service.inject.maven.plugin { + + requires io.helidon.builder.api; + requires io.helidon.common; + requires io.helidon.common.config; + requires io.helidon.codegen; + requires io.helidon.codegen.scan; + requires io.helidon.codegen.compiler; + requires io.helidon.service.codegen; + requires io.helidon.service.inject.codegen; + requires io.helidon.service.inject.api; + + requires maven.artifact; + requires maven.model; + requires maven.plugin.annotations; + requires maven.plugin.api; + requires maven.project; + requires java.xml; + requires io.github.classgraph; + requires io.helidon.service.metadata; + + exports io.helidon.service.inject.maven.plugin; +} diff --git a/service/inject/pom.xml b/service/inject/pom.xml index a246ccb2276..1d863c4a322 100644 --- a/service/inject/pom.xml +++ b/service/inject/pom.xml @@ -40,5 +40,6 @@ codegen api inject + maven-plugin diff --git a/service/pom.xml b/service/pom.xml index 72de3cd13e2..dc67f7c9624 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -25,6 +25,7 @@ io.helidon helidon-project 4.2.0-SNAPSHOT + ../pom.xml io.helidon.service diff --git a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceDiscovery.java b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceDiscovery.java index e7635d0681b..ab6e4ae7461 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceDiscovery.java +++ b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceDiscovery.java @@ -100,9 +100,16 @@ private static Class toClass(TypeName className) { return (Class) cl.loadClass(className.fqName()); } catch (ClassNotFoundException e) { - throw new ServiceRegistryException("Resolution of type \"" + className.fqName() - + "\" to class failed.", - e); + try { + // fall back to classloader of our class + return (Class) CoreServiceDiscovery.class.getClassLoader().loadClass(className.fqName()); + } catch (ClassNotFoundException ex) { + var toThrow = new ServiceRegistryException("Resolution of type \"" + className.fqName() + + "\" to class failed.", + ex); + toThrow.addSuppressed(e); + throw toThrow; + } } } diff --git a/service/tests/inject/codegen/src/test/java/io/helidon/service/tests/inject/codegen/InjectCodegenTypesTest.java b/service/tests/inject/codegen/src/test/java/io/helidon/service/tests/inject/codegen/InjectCodegenTypesTest.java index c221515bca2..5856b589df5 100644 --- a/service/tests/inject/codegen/src/test/java/io/helidon/service/tests/inject/codegen/InjectCodegenTypesTest.java +++ b/service/tests/inject/codegen/src/test/java/io/helidon/service/tests/inject/codegen/InjectCodegenTypesTest.java @@ -24,10 +24,16 @@ import java.util.Set; import io.helidon.common.types.TypeName; +import io.helidon.service.inject.Binding; +import io.helidon.service.inject.InjectConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.InjectionMain; +import io.helidon.service.inject.InjectionPlanBinder; import io.helidon.service.inject.api.Event; import io.helidon.service.inject.api.EventManager; import io.helidon.service.inject.api.FactoryType; import io.helidon.service.inject.api.GeneratedInjectService; +import io.helidon.service.inject.api.InjectRegistry; import io.helidon.service.inject.api.InjectServiceDescriptor; import io.helidon.service.inject.api.Injection; import io.helidon.service.inject.api.Interception; @@ -85,6 +91,7 @@ void testTypes() { checkField(toCheck, checked, fields, "INJECTION_SCOPE_HANDLER", Injection.ScopeHandler.class); checkField(toCheck, checked, fields, "INJECTION_SERVICES_FACTORY", Injection.ServicesFactory.class); checkField(toCheck, checked, fields, "INJECTION_QUALIFIED_FACTORY", Injection.QualifiedFactory.class); + checkField(toCheck, checked, fields, "INJECTION_MAIN", Injection.Main.class); // api.Interception.* checkField(toCheck, checked, fields, "INTERCEPTION_INTERCEPTED", Interception.Intercepted.class); @@ -97,6 +104,13 @@ void testTypes() { checkField(toCheck, checked, fields, "INJECT_INJECTION_POINT", Ip.class); checkField(toCheck, checked, fields, "INJECT_SERVICE_INSTANCE", ServiceInstance.class); checkField(toCheck, checked, fields, "INJECT_SERVICE_DESCRIPTOR", InjectServiceDescriptor.class); + checkField(toCheck, checked, fields, "INJECT_CONFIG", InjectConfig.class); + checkField(toCheck, checked, fields, "INJECT_CONFIG_BUILDER", InjectConfig.Builder.class); + checkField(toCheck, checked, fields, "INJECT_MAIN", InjectionMain.class); + checkField(toCheck, checked, fields, "INJECT_BINDING", Binding.class); + checkField(toCheck, checked, fields, "INJECT_REGISTRY", InjectRegistry.class); + checkField(toCheck, checked, fields, "INJECT_REGISTRY_MANAGER", InjectRegistryManager.class); + checkField(toCheck, checked, fields, "INJECT_PLAN_BINDER", InjectionPlanBinder.class); // api.* interception types checkField(toCheck, checked, fields, "INTERCEPT_EXCEPTION", InterceptionException.class); @@ -132,6 +146,9 @@ void testTypes() { checkField(toCheck, checked, fields, "INTERCEPT_G_WRAPPER_QUALIFIED_FACTORY", GeneratedInjectService.QualifiedFactoryInterceptionWrapper.class); + checkField(toCheck, checked, fields, "STRING_ARRAY", String[].class); + checkField(toCheck, checked, fields, "DOUBLE_ARRAY", double[].class); + assertThat("If the collection is not empty, please add appropriate checkField line to this test", toCheck, IsEmptyCollection.empty()); diff --git a/service/tests/inject/inject/pom.xml b/service/tests/inject/inject/pom.xml index 3d483dca7b4..ca0630caaa7 100644 --- a/service/tests/inject/inject/pom.xml +++ b/service/tests/inject/inject/pom.xml @@ -137,6 +137,29 @@ + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${helidon.version} + + + create-application + + create-application + + + + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + true + + diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/CustomMain.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/CustomMain.java new file mode 100644 index 00000000000..41fff4325b6 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/CustomMain.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.tests.inject; + +import io.helidon.service.inject.InjectConfig; +import io.helidon.service.inject.InjectionMain; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.Injection; + +/** + * Example of a custom main class. + * This class is here to make sure this does not get broken. + */ +@Injection.Main +public abstract class CustomMain extends InjectionMain { + /* + Important note: + DO NOT change the signature of methods in this class, as that would cause a backward incompatible change + for our users. + The subtype is code generated. Any changes in the + Helidon APIs would cause older generated code to stop working. + The only exception is major version updates, but it would still be better if this stays compatible. + */ + + @Override + protected void beforeServiceDescriptors(InjectConfig.Builder configBuilder) { + System.out.println("Before service descriptors"); + } + + @Override + protected void afterServiceDescriptors(InjectConfig.Builder configBuilder) { + System.out.println("After service descriptors"); + } + + @Override + protected InjectRegistry init(InjectConfig config) { + System.out.println("Before init method"); + try { + return super.init(config); + } finally { + System.out.println("After init method"); + } + } + + @Override + protected void start(String[] arguments) { + super.start(arguments); + } + + @Override + protected InjectConfig.Builder configBuilder(String[] arguments) { + return super.configBuilder(arguments); + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/MainClassTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/MainClassTest.java new file mode 100644 index 00000000000..82cc105a13c --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/MainClassTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.tests.inject; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import io.helidon.service.inject.InjectConfig; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; + +// test that the generated main class exists and works as expected +public class MainClassTest { + @Test + public void testMain() throws NoSuchMethodException { + // this class is expected to be code generated based on CustomMain @Injection.Main annotation + ApplicationMain appMain = new ApplicationMain(); + + assertThat(appMain, instanceOf(CustomMain.class)); + + Class theClass = ApplicationMain.class; + + assertThat("The class must be public, to be a candidate for Main class", + Modifier.isPublic(theClass.getModifiers())); + + // the class must have the following two methods (when not using the maven plugin): + // public static void main(String[] args) {} + // protected void serviceDescriptors(InjectConfig.Builder config) {} + Method mainMethod = theClass.getMethod("main", String[].class); + assertThat("The main method must be public", Modifier.isPublic(mainMethod.getModifiers())); + assertThat("The main method must be static", Modifier.isStatic(mainMethod.getModifiers())); + assertThat("The main method must return void", mainMethod.getReturnType(), equalTo(void.class)); + + Method serviceDescriptorMethod = theClass.getDeclaredMethod("serviceDescriptors", InjectConfig.Builder.class); + assertThat("The service descriptors method must be protected", + Modifier.isProtected(serviceDescriptorMethod.getModifiers())); + assertThat("The service descriptors method must not be static", + !Modifier.isStatic(serviceDescriptorMethod.getModifiers())); + assertThat("The service descriptors method must return void", + serviceDescriptorMethod.getReturnType(), + equalTo(void.class)); + } +} diff --git a/service/tests/inject/lookup/pom.xml b/service/tests/inject/lookup/pom.xml index 4ee6d85a335..1d1be68d387 100644 --- a/service/tests/inject/lookup/pom.xml +++ b/service/tests/inject/lookup/pom.xml @@ -137,6 +137,22 @@ + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${helidon.version} + + + create-application + + create-application + + + + + true + + diff --git a/service/tests/inject/maven-plugin/pom.xml b/service/tests/inject/maven-plugin/pom.xml new file mode 100644 index 00000000000..e4acf371362 --- /dev/null +++ b/service/tests/inject/maven-plugin/pom.xml @@ -0,0 +1,86 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-maven-plugin + Helidon Service Tests Inject Maven Plugin + Tests for Maven Plugin + + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.build-tools.common + helidon-build-common-test-utils + ${version.plugin.helidon-build-tools} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT.java + + + + + org.apache.maven.plugins + maven-invoker-plugin + + + + install + integration-test + verify + + + + + + + + diff --git a/service/tests/inject/maven-plugin/src/it/projects/test1/pom.xml b/service/tests/inject/maven-plugin/src/it/projects/test1/pom.xml new file mode 100644 index 00000000000..0692c5a56bf --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test1/pom.xml @@ -0,0 +1,98 @@ + + + + + 4.0.0 + + io.helidon.service.tests.inject.maven.plugin.test + test-1 + @project.version@ + Test Inject Maven Plugin 1 + + + + io.helidon.service + helidon-service-registry + @project.version@ + + + io.helidon.service.inject + helidon-service-inject-api + @project.version@ + + + io.helidon.service.inject + helidon-service-inject + @project.version@ + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + @project.version@ + + + io.helidon.service.inject + helidon-service-inject-codegen + @project.version@ + + + io.helidon.codegen + helidon-codegen-helidon-copyright + @project.version@ + + + + + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${project.version} + + + create-application + + create-application + + + + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${project.version} + + + + true + + + + + diff --git a/service/tests/inject/maven-plugin/src/it/projects/test1/postbuild.groovy b/service/tests/inject/maven-plugin/src/it/projects/test1/postbuild.groovy new file mode 100644 index 00000000000..aaeb9dcaca5 --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test1/postbuild.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.build.common.test.utils.JUnitLauncher +import io.helidon.service.tests.inject.maven.plugin.ProjectsTestIT + +//noinspection GroovyAssignabilityCheck,GrUnresolvedAccess +JUnitLauncher.builder() + .select(ProjectsTestIT.class, "test1", String.class) + .parameter("basedir", basedir.getAbsolutePath()) + .reportsDir(basedir) + .outputFile(new File(basedir, "test.log")) + .suiteId("helidon-inject-maven-plugin-it-test1") + .suiteDisplayName("Helidon Inject Maven Plugin Integration Test 1") + .build() + .launch() diff --git a/service/tests/inject/maven-plugin/src/it/projects/test1/src/main/java/my/module/ServiceType.java b/service/tests/inject/maven-plugin/src/it/projects/test1/src/main/java/my/module/ServiceType.java new file mode 100644 index 00000000000..4244fb878cb --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test1/src/main/java/my/module/ServiceType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package my.module; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class ServiceType { + +} \ No newline at end of file diff --git a/service/tests/inject/maven-plugin/src/it/projects/test2/pom.xml b/service/tests/inject/maven-plugin/src/it/projects/test2/pom.xml new file mode 100644 index 00000000000..3279da678b6 --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test2/pom.xml @@ -0,0 +1,101 @@ + + + + + 4.0.0 + + io.helidon.service.tests.inject.maven.plugin.test + test-2 + @project.version@ + Test Inject Maven Plugin 2 + + + + io.helidon.service + helidon-service-registry + @project.version@ + + + io.helidon.service.inject + helidon-service-inject-api + @project.version@ + + + io.helidon.service.inject + helidon-service-inject + @project.version@ + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + @project.version@ + + + io.helidon.service.inject + helidon-service-inject-codegen + @project.version@ + + + io.helidon.codegen + helidon-codegen-helidon-copyright + @project.version@ + + + + + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${project.version} + + + create-application + + create-application + + + + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${project.version} + + + + my.updated + UpdatedMain + UpdatedBinding + true + + + + + diff --git a/service/tests/inject/maven-plugin/src/it/projects/test2/postbuild.groovy b/service/tests/inject/maven-plugin/src/it/projects/test2/postbuild.groovy new file mode 100644 index 00000000000..39aa4086ed9 --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test2/postbuild.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.build.common.test.utils.JUnitLauncher +import io.helidon.service.tests.inject.maven.plugin.ProjectsTestIT + +//noinspection GroovyAssignabilityCheck,GrUnresolvedAccess +JUnitLauncher.builder() + .select(ProjectsTestIT.class, "test2", String.class) + .parameter("basedir", basedir.getAbsolutePath()) + .reportsDir(basedir) + .outputFile(new File(basedir, "test.log")) + .suiteId("helidon-inject-maven-plugin-it-test2") + .suiteDisplayName("Helidon Inject Maven Plugin Integration Test 2") + .build() + .launch() diff --git a/service/tests/inject/maven-plugin/src/it/projects/test2/src/main/java/my/module/ServiceType.java b/service/tests/inject/maven-plugin/src/it/projects/test2/src/main/java/my/module/ServiceType.java new file mode 100644 index 00000000000..4244fb878cb --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test2/src/main/java/my/module/ServiceType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package my.module; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class ServiceType { + +} \ No newline at end of file diff --git a/service/tests/inject/maven-plugin/src/it/projects/test3/pom.xml b/service/tests/inject/maven-plugin/src/it/projects/test3/pom.xml new file mode 100644 index 00000000000..b81874fb86d --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test3/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + io.helidon.service.tests.inject.maven.plugin.test + test-3 + @project.version@ + Test Inject Maven Plugin 3 + + + + io.helidon.service + helidon-service-registry + @project.version@ + + + io.helidon.service.inject + helidon-service-inject-api + @project.version@ + + + io.helidon.service.inject + helidon-service-inject + @project.version@ + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + @project.version@ + + + io.helidon.service.inject + helidon-service-inject-codegen + @project.version@ + + + io.helidon.codegen + helidon-codegen-helidon-copyright + @project.version@ + + + + + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${project.version} + + + create-application + + create-application + + + + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${project.version} + + + + false + false + true + + + + + diff --git a/service/tests/inject/maven-plugin/src/it/projects/test3/postbuild.groovy b/service/tests/inject/maven-plugin/src/it/projects/test3/postbuild.groovy new file mode 100644 index 00000000000..fcf3d319c86 --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test3/postbuild.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.build.common.test.utils.JUnitLauncher +import io.helidon.service.tests.inject.maven.plugin.ProjectsTestIT + +//noinspection GroovyAssignabilityCheck,GrUnresolvedAccess +JUnitLauncher.builder() + .select(ProjectsTestIT.class, "test3", String.class) + .parameter("basedir", basedir.getAbsolutePath()) + .reportsDir(basedir) + .outputFile(new File(basedir, "test.log")) + .suiteId("helidon-inject-maven-plugin-it-test3") + .suiteDisplayName("Helidon Inject Maven Plugin Integration Test 3") + .build() + .launch() diff --git a/service/tests/inject/maven-plugin/src/it/projects/test3/src/main/java/my/module/ServiceType.java b/service/tests/inject/maven-plugin/src/it/projects/test3/src/main/java/my/module/ServiceType.java new file mode 100644 index 00000000000..4244fb878cb --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/projects/test3/src/main/java/my/module/ServiceType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package my.module; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class ServiceType { + +} \ No newline at end of file diff --git a/service/tests/inject/maven-plugin/src/it/settings.xml b/service/tests/inject/maven-plugin/src/it/settings.xml new file mode 100644 index 00000000000..251955ad56b --- /dev/null +++ b/service/tests/inject/maven-plugin/src/it/settings.xml @@ -0,0 +1,54 @@ + + + + + + it-repo + + + local.central + @localRepositoryUrl@ + + true + + + ignore + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + ignore + true + + + + + + + it-repo + + \ No newline at end of file diff --git a/service/tests/inject/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java b/service/tests/inject/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java new file mode 100644 index 00000000000..fb46edd1e93 --- /dev/null +++ b/service/tests/inject/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.service.tests.inject.maven.plugin; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.helidon.build.common.test.utils.ConfigurationParameterSource; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; + +import static io.helidon.build.common.test.utils.FileMatchers.fileExists; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Integration test that verifies the projects under {@code src/it/projects}. + */ +public class ProjectsTestIT { + @ParameterizedTest + @ConfigurationParameterSource("basedir") + @DisplayName("Test default binding and main class names") + void test1(String basedir) { + // test the first project under src/it/projects/test1 (referenced from postbuild.groovy) + // make sure all the required types are created + Path projectPath = Paths.get(basedir); + Path compiledClasses = projectPath.resolve("target/classes/my/module"); + Path generatedSources = projectPath.resolve("target/generated-sources/annotations/my/module"); + + assertThat("Generated by the Maven plugin", generatedSources.resolve("Injection__Binding.java"), fileExists()); + assertThat("Generated by the Maven plugin", + generatedSources.resolve("Injection__Binding__ServiceDescriptor.java"), + fileExists()); + assertThat("Generated by the service inject codegen during compilation", + generatedSources.resolve("ServiceType__ServiceDescriptor.java"), + fileExists()); + assertThat("Generated by the Maven plugin", generatedSources.resolve("ApplicationMain.java"), fileExists()); + assertThat("Not a generated type, exists in project sources only", + generatedSources.resolve("ServiceType.java"), + not(fileExists())); + + assertThat("Generated by the Maven plugin", compiledClasses.resolve("Injection__Binding.class"), fileExists()); + assertThat("Generated by the Maven plugin", + compiledClasses.resolve("Injection__Binding__ServiceDescriptor.class"), + fileExists()); + assertThat("Generated by the service inject codegen during compilation", + compiledClasses.resolve("ServiceType__ServiceDescriptor.class"), + fileExists()); + assertThat("Generated by the Maven plugin", compiledClasses.resolve("ApplicationMain.class"), fileExists()); + assertThat("Compiled service", compiledClasses.resolve("ServiceType.class"), fileExists()); + + } + + @ParameterizedTest + @ConfigurationParameterSource("basedir") + @DisplayName("Test custom binding and main class names and custom package") + void test2(String basedir) { + // test the first project under src/it/projects/test1 (referenced from postbuild.groovy) + // make sure all the required types are created + Path projectPath = Paths.get(basedir); + Path compiledClasses = projectPath.resolve("target/classes/my/module"); + Path compiledCustomClasses = projectPath.resolve("target/classes/my/updated"); + Path generatedSources = projectPath.resolve("target/generated-sources/annotations/my/module"); + Path generatedCustomSources = projectPath.resolve("target/generated-sources/annotations/my/updated"); + + assertThat("Generated by the Maven plugin", + generatedCustomSources.resolve("UpdatedBinding.java"), + fileExists()); + assertThat("Generated by the Maven plugin", + generatedCustomSources.resolve("UpdatedBinding__ServiceDescriptor.java"), + fileExists()); + assertThat("Generated by the Maven plugin", + generatedCustomSources.resolve("UpdatedMain.java"), + fileExists()); + assertThat("Generated by the service inject codegen during compilation", + generatedSources.resolve("ServiceType__ServiceDescriptor.java"), + fileExists()); + assertThat("Not a generated type, exists in project sources only", + generatedSources.resolve("ServiceType.java"), + not(fileExists())); + + assertThat("Generated by the Maven plugin", + compiledCustomClasses.resolve("UpdatedBinding.class"), + fileExists()); + assertThat("Generated by the Maven plugin", + compiledCustomClasses.resolve("UpdatedBinding__ServiceDescriptor.class"), + fileExists()); + assertThat("Generated by the Maven plugin", + compiledCustomClasses.resolve("UpdatedMain.class"), + fileExists()); + assertThat("Generated by the service inject codegen during compilation", + compiledClasses.resolve("ServiceType__ServiceDescriptor.class"), + fileExists()); + assertThat("Compiled service", compiledClasses.resolve("ServiceType.class"), fileExists()); + + } + + @ParameterizedTest + @ConfigurationParameterSource("basedir") + @DisplayName("Test binding and main class generation disabled") + void test3(String basedir) { + // test the first project under src/it/projects/test1 (referenced from postbuild.groovy) + // make sure all the required types are created + Path projectPath = Paths.get(basedir); + Path compiledClasses = projectPath.resolve("target/classes/my/module"); + Path generatedSources = projectPath.resolve("target/generated-sources/annotations/my/module"); + + assertThat("Should not be generated by the Maven plugin", + generatedSources.resolve("Injection__Binding.java"), + not(fileExists())); + assertThat("Should nto be generated by the Maven plugin", + generatedSources.resolve("Injection__Binding__ServiceDescriptor.java"), + not(fileExists())); + assertThat("Generated by the service inject codegen during compilation", + generatedSources.resolve("ServiceType__ServiceDescriptor.java"), + fileExists()); + assertThat("Should not be generated by the Maven plugin", + generatedSources.resolve("ApplicationMain.java"), + not(fileExists())); + assertThat("Not a generated type, exists in project sources only", + generatedSources.resolve("ServiceType.java"), + not(fileExists())); + + assertThat("Should not be generated by the Maven plugin", + compiledClasses.resolve("Injection__Binding.class"), + not(fileExists())); + assertThat("Should nto be generated by the Maven plugin", + compiledClasses.resolve("Injection__Binding__ServiceDescriptor.class"), + not(fileExists())); + assertThat("Generated by the service inject codegen during compilation", + compiledClasses.resolve("ServiceType__ServiceDescriptor.class"), + fileExists()); + assertThat("Should not be generated by the Maven plugin", + compiledClasses.resolve("ApplicationMain.class"), + not(fileExists())); + assertThat("Compiled service", compiledClasses.resolve("ServiceType.class"), fileExists()); + + } +} diff --git a/service/tests/inject/pom.xml b/service/tests/inject/pom.xml index 71b7de940a5..65a3bf6683b 100644 --- a/service/tests/inject/pom.xml +++ b/service/tests/inject/pom.xml @@ -46,5 +46,6 @@ stacking toolbox events + maven-plugin diff --git a/service/tests/inject/qualified-providers/pom.xml b/service/tests/inject/qualified-providers/pom.xml index 0772cf44a96..e00b45ef4c1 100644 --- a/service/tests/inject/qualified-providers/pom.xml +++ b/service/tests/inject/qualified-providers/pom.xml @@ -127,6 +127,22 @@ + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${helidon.version} + + + create-application + + create-application + + + + + true + + diff --git a/service/tests/inject/toolbox/pom.xml b/service/tests/inject/toolbox/pom.xml index 5cc2a7b0322..84c7f654273 100644 --- a/service/tests/inject/toolbox/pom.xml +++ b/service/tests/inject/toolbox/pom.xml @@ -141,6 +141,22 @@ + + io.helidon.service.inject + helidon-service-inject-maven-plugin + ${helidon.version} + + + create-application + + create-application + + + + + true + + diff --git a/service/tests/inject/toolbox/src/test/java/io/helidon/service/tests/inject/toolbox/ToolBoxTest.java b/service/tests/inject/toolbox/src/test/java/io/helidon/service/tests/inject/toolbox/ToolBoxTest.java index 5e9b9a6a023..974c976e4c0 100644 --- a/service/tests/inject/toolbox/src/test/java/io/helidon/service/tests/inject/toolbox/ToolBoxTest.java +++ b/service/tests/inject/toolbox/src/test/java/io/helidon/service/tests/inject/toolbox/ToolBoxTest.java @@ -150,7 +150,6 @@ void hierarchyOfInjections() { * This assumes the presence of module(s) + application(s) to handle all bindings, with effectively no lookups! */ @Test - @Disabled("Disabled, as this required maven plugin, to be added in a later PR") void noServiceActivationRequiresLookupWhenApplicationIsPresent() { Counter counter = lookupCounter(); long initialCount = counter.count();