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:
+ *
+ * {@link #configBuilder(String[])} to prepare the configuration builder from
+ * {@link io.helidon.common.config.Config}
+ * {@link #beforeServiceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} to update the builder
+ * {@link #serviceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} for code generated setup
+ * {@link #afterServiceDescriptors(io.helidon.service.inject.InjectConfig.Builder)} to update the builder
+ * {@link #init(InjectConfig)} to initialize the service registry
+ *
+ *
+ * @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 super String, Throwable> 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();