Skip to content

Commit

Permalink
Maven plugin to generate bindings and Main class.
Browse files Browse the repository at this point in the history
Update to tests to validate this all works together.
  • Loading branch information
tomas-langer committed Nov 4, 2024
1 parent b03a59a commit 4781036
Show file tree
Hide file tree
Showing 40 changed files with 3,363 additions and 38 deletions.
4 changes: 4 additions & 0 deletions all/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,10 @@
<groupId>io.helidon.service.inject</groupId>
<artifactId>helidon-service-inject</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.service.inject</groupId>
<artifactId>helidon-service-inject-maven-plugin</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.metadata</groupId>
<artifactId>helidon-metadata-hson</artifactId>
Expand Down
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,11 @@
<artifactId>helidon-service-inject</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.service.inject</groupId>
<artifactId>helidon-service-inject-maven-plugin</artifactId>
<version>${helidon.version}</version>
</dependency>

<!-- Metadata -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@
* New service descriptor metadata with its class code.
*/
public interface DescriptorClassCode {
/**
* Create a new instance.
*
* @param classCode class code that contains necessary information for the generated class.
* @param registryType type of registry that generates the descriptor (core, inject)
* @param weight weight of the service this descriptor describes
* @param contracts contracts of the service (i.e. {@code MyContract})
* @param factoryContracts factory contracts of this service (i.e. {@code Supplier<MyContract>})
* @return a new class code of service descriptor
*/
static DescriptorClassCode create(ClassCode classCode,
String registryType,
double weight,
Set<ResolvedType> contracts,
Set<ResolvedType> factoryContracts) {
return new DescriptorClassCodeImpl(classCode,
registryType,
weight,
contracts,
factoryContracts);
}

/**
* New source code information.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
/**
* Generates a service descriptor.
*/
class GenerateServiceDescriptor {
public class GenerateServiceDescriptor {
static final TypeName SET_OF_RESOLVED_TYPES = TypeName.builder(TypeNames.SET)
.addTypeArgument(TypeNames.RESOLVED_TYPE_NAME)
.build();
Expand Down Expand Up @@ -89,11 +89,11 @@ private GenerateServiceDescriptor(TypeName generator,
* @param service service to create a descriptor for
* @return class model builder of the service descriptor
*/
static ClassModel.Builder generate(TypeName generator,
RegistryCodegenContext ctx,
RegistryRoundContext roundContext,
Collection<TypeInfo> allServices,
TypeInfo service) {
public static ClassModel.Builder generate(TypeName generator,
RegistryCodegenContext ctx,
RegistryRoundContext roundContext,
Collection<TypeInfo> allServices,
TypeInfo service) {
return new GenerateServiceDescriptor(generator,
ctx,
roundContext,
Expand Down
24 changes: 24 additions & 0 deletions service/inject/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Helidon Inject includes:
- [Aspect Oriented Programming (interceptors)](#interceptors)
- [Events](events)
- [Programmatic Lookup](#programmatic-lookup)
- [Startup](#startup)
- [Other](#other)
- [Glossary](#glossary)

Expand Down Expand Up @@ -361,6 +362,29 @@ Lookup parameter options:
- `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments)
- `Lookup` - a full search criteria for a registry lookup

# Startup

The following options are available to start a service registry (and the application):

1. Use API to create an `io.helidon.service.inject.InjectRegistryManager`
2. Use the Helidon startup class `io.helidon.Main`, which will use the injection main class through service loader
3. Use a generated main class, by default named `ApplicationMain` in the main package of the application (supports customization)

## Generated Main Class

To generate a main class, the Helidon Service Inject Maven plugin must be configured.
This is expected to be configured only for an application (i.e. not for library modules) - this is the reason we do not generate it automatically.

The generated main class will contain full, reflection less configuration of the service registry. It registers all services directly through API, and disables service discovery from classpath.

The Main class can also be customized; to do this:
1. Create a custom class (let's call it `CustomMain` as an example)
2. The class must extends the injection main class (`public abstract class CustomMain extends InjectionMain`)
3. The class must be annotated with `@Injection.Main`, so it is discovered by annotation processor
4. Implement any desired methods; the generated class will only implement `serviceDescriptors(InjectConfig.Builder configBuilder)` (always), and `discoverServices()` (if created from the Maven plugin)

For details on how to configure your build, see [Maven Plugin](../maven-plugin/README.md).

# Other

## API types quick reference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ static void addContract(Lookup.BuilderBase<?, ?> builder, Class<?> contract) {
builder.addContract(ResolvedType.create(contract));
}

/**
* The managed services advertised types (i.e., typically its interfaces).
*
* @param builder builder instance
* @param contract contract the service implements
* @see Lookup#contracts()
*/
@Prototype.BuilderMethod
static void addContract(Lookup.BuilderBase<?, ?> builder, TypeName contract) {
builder.addContract(ResolvedType.create(contract));
}

/**
* The managed service implementation type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,35 @@
package io.helidon.service.inject.codegen;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import io.helidon.codegen.CodegenException;
import io.helidon.codegen.CodegenUtil;
import io.helidon.codegen.ElementInfoPredicates;
import io.helidon.codegen.classmodel.ClassModel;
import io.helidon.codegen.classmodel.Method;
import io.helidon.common.types.AccessModifier;
import io.helidon.common.types.Annotations;
import io.helidon.common.types.ElementSignature;
import io.helidon.common.types.TypeInfo;
import io.helidon.common.types.TypeName;
import io.helidon.common.types.TypeNames;
import io.helidon.common.types.TypedElementInfo;

import static io.helidon.service.inject.codegen.InjectCodegenTypes.DOUBLE_ARRAY;
import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_CONFIG;
import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_CONFIG_BUILDER;
import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_MAIN;
import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_REGISTRY;
import static io.helidon.service.inject.codegen.InjectCodegenTypes.STRING_ARRAY;
import static java.util.function.Predicate.not;

/**
* Utility for {@value #CLASS_NAME} class generation.
*/
final class ApplicationMainGenerator {
public final class ApplicationMainGenerator {
/**
* Default class name of the generated main class.
*/
Expand Down Expand Up @@ -87,6 +95,7 @@ private ApplicationMainGenerator() {
* @param runLevelHandler handler of the run level method
* @return class model builder
*/
@SuppressWarnings("checkstyle:ParameterNumber") // all parameters are mandatory
public static ClassModel.Builder generate(TypeName generator,
Set<ElementSignature> declaredSignatures,
TypeName superType,
Expand Down Expand Up @@ -167,6 +176,67 @@ public static ClassModel.Builder generate(TypeName generator,
return classModel;
}

/**
* Provides all relevant signatures that may override methods from {@code InjectionMain}.
*
* @param customMain type to analyze
* @return set of method signatures that are non-private, non-static
*/
public static Set<ElementSignature> declaredSignatures(TypeInfo customMain) {
return customMain.elementInfo()
.stream()
.filter(ElementInfoPredicates::isMethod)
.filter(not(ElementInfoPredicates::isStatic))
.filter(not(ElementInfoPredicates::isPrivate))
.map(TypedElementInfo::signature)
.collect(Collectors.toUnmodifiableSet());
}

/**
* Validate a type, to make sure it is a valid custom main class.
*
* @param customMain type to validate
*/
public static void validate(TypeInfo customMain) {
Optional<TypeInfo> superType = customMain.superTypeInfo();
if (superType.isEmpty()) {
throw new CodegenException("Custom main class must directly extend " + INJECT_MAIN.fqName() + ", but "
+ customMain.typeName().fqName() + " does not extend any class",
customMain.originatingElementValue());
}
if (!superType.get().typeName().equals(INJECT_MAIN)) {
throw new CodegenException("Custom main class must directly extend " + INJECT_MAIN.fqName() + ", but "
+ customMain.typeName().fqName() + " extends " + superType.get().typeName(),
customMain.originatingElementValue());
}
if (customMain.accessModifier() == AccessModifier.PRIVATE) {
throw new CodegenException("Custom main class must be accessible (non-private) class, but "
+ customMain.typeName().fqName() + " is private.",
customMain.originatingElementValue());
}
if (customMain.elementInfo()
.stream()
.filter(ElementInfoPredicates::isMethod)
.filter(ElementInfoPredicates::isStatic)
.filter(not(ElementInfoPredicates::isPrivate))
.filter(ElementInfoPredicates.elementName("main"))
.anyMatch(ElementInfoPredicates.hasParams(TypeName.create(String[].class)))) {
throw new CodegenException("Custom main class must not declare a static main(String[]) method, as it is code "
+ "generated into the ApplicationMain class, but "
+ customMain.typeName().fqName() + " declares it.",
customMain.originatingElementValue());
}
if (customMain.elementInfo()
.stream()
.filter(ElementInfoPredicates::isConstructor)
.filter(not(ElementInfoPredicates::isPrivate))
.noneMatch(ElementInfoPredicates.hasParams())) {
throw new CodegenException("Custom main class must have an accessible no-argument constructor, but "
+ customMain.typeName().fqName() + " does not.",
customMain.originatingElementValue());
}
}

private static void mainMethodBody(TypeName type, Method.Builder method) {
method.addContent("new ")
.addContent(type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private void addMergeQualifiers(ClassModel.Builder classModel) {
.addContentLine("}")
.addContent("var qualifierSet = new ")
.addContent(HashSet.class)
.addContentLine("(QUALIFIERS);")
.addContentLine("<>(QUALIFIERS);")
.addContent("qualifierSet.addAll(")
.addContent(Set.class)
.addContentLine(".of(qualifiers));")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,26 @@ public class InjectCodegenTypes {
*/
public static final TypeName INJECT_REGISTRY =
TypeName.create("io.helidon.service.inject.api.InjectRegistry");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectRegistryManager}.
*/
public static final TypeName INJECT_REGISTRY_MANAGER =
TypeName.create("io.helidon.service.inject.InjectRegistryManager");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectionMain}.
*/
public static final TypeName INJECT_MAIN =
TypeName.create("io.helidon.service.inject.InjectionMain");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.Binding}.
*/
public static final TypeName INJECT_BINDING =
TypeName.create("io.helidon.service.inject.Binding");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectionPlanBinder}.
*/
public static final TypeName INJECT_PLAN_BINDER =
TypeName.create("io.helidon.service.inject.InjectionPlanBinder");

/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InvocationException}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,14 @@ public final class InjectOptions {
* Name of the generated Main class for Injection. Defaults to
* {@value ApplicationMainGenerator#CLASS_NAME}.
* The same property must be provided to the maven plugin, to correctly update the generated class.
* To configure package name, use {@link io.helidon.codegen.CodegenOptions#CODEGEN_PACKAGE} option.
*/
public static final Option<String> 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);
/**
* Package name of the generated Main class for Injection.
* This is only needed if there is no custom main class AND the package name cannot be determined from processed classes,
* OR it was determined wrongly.
*/
public static final Option<String> APPLICATION_MAIN_PACKAGE_NAME =
Option.create("helidon.inject.application.main.package.name",
"Package name of the generated Main class for Helidon Injection.",
ApplicationMainGenerator.CLASS_NAME,
Function.identity(),
GenericType.STRING);

/**
* Whether to generate main class for Helidon Injection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class InjectionExtension implements RegistryCodegenExtension {
.stream()
.map(it -> it.create(codegenContext))
.toList();
this.packageName = InjectOptions.APPLICATION_MAIN_PACKAGE_NAME.findValue(options)
this.packageName = CodegenOptions.CODEGEN_PACKAGE.findValue(options)
.orElse(null);
this.mainClassGenerated = !options.enabled(InjectOptions.APPLICATION_MAIN_GENERATE);
}
Expand Down Expand Up @@ -430,7 +430,7 @@ private void generateScopeDescriptor(RegistryRoundContext roundContext, TypeInfo
private void generateMain() {
if (packageName == null) {
throw new CodegenException("Cannot determine package name for the generated main class. "
+ "Please use option " + InjectOptions.APPLICATION_MAIN_PACKAGE_NAME.name()
+ "Please use option " + CodegenOptions.CODEGEN_PACKAGE.name()
+ " to specify it");
}
// generate main class if it doe not exist
Expand Down Expand Up @@ -467,33 +467,21 @@ private void generateMain(RegistryRoundContext roundCtx, Collection<TypeInfo> cu
customMain.originatingElementValue());
}

// TODO validate that the custom main class extends InjectionMain
// validate it does not declare `main` method
// validate it has accessible no-arg constructor
// validate it is accessible (at least package local static class)
// add generation to processing over if not generated here (and make sure a conflicting name does not exist)
//

// 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();
var declaredSignatures = customMain.elementInfo()
.stream()
.filter(ElementInfoPredicates::isMethod)
.filter(not(ElementInfoPredicates::isStatic))
.filter(not(ElementInfoPredicates::isPrivate))
.map(TypedElementInfo::signature)
.collect(Collectors.toUnmodifiableSet());
ApplicationMainGenerator.validate(customMain);
var declaredSignatures = ApplicationMainGenerator.declaredSignatures(customMain);

ClassModel.Builder applicationMain = ApplicationMainGenerator.generate(GENERATOR,
declaredSignatures,
customMain.typeName(),
generatedType,
false,
true,
false,
(a, b, c) -> {
},
Expand Down
Loading

0 comments on commit 4781036

Please sign in to comment.