Skip to content

Commit

Permalink
Support for Injection Main class.
Browse files Browse the repository at this point in the history
  • Loading branch information
tomas-langer committed Oct 23, 2024
1 parent 454f18a commit b03a59a
Show file tree
Hide file tree
Showing 16 changed files with 938 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@ public sealed interface ElementSignature permits ElementSignatures.FieldSignatur
ElementSignatures.MethodSignature,
ElementSignatures.ParameterSignature,
ElementSignatures.NoSignature {
/**
* A field signature.
*
* @param type type of the field
* @param name name of the field
* @return a new field signature
*/
static ElementSignature createField(TypeName type, String name) {
return ElementSignatures.createField(type, name);
}

/**
* A constructor signature.
*
* @param parameters list of types of parameters
* @return a new constructor signature
*/
static ElementSignature createConstructor(List<TypeName> parameters) {
return ElementSignatures.createConstructor(parameters);
}

/**
* A method signature.
*
* @param returnType return type of the method
* @param name name of the method
* @param parameters parameter types of the method
* @return a new method signature
*/
static ElementSignature createMethod(TypeName returnType, String name, List<TypeName> parameters) {
return ElementSignatures.createMethod(returnType, name, parameters);
}

/**
* Type of the element. Resolves as follows:
* <ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,26 @@ private Injection() {
Class<? extends Annotation> value() default Singleton.class;
}

/**
* Marks a custom main class (should be declared as {@code abstract}).
* This is a required annotation to make sure the generated main class extends the user's main class.
* Note that the custom main class MUST extend the {@code io.helidon.service.inject.InjectionMain} to customize
* startup sequence.
* The name {@code ApplicationMain} is reserved by service registry, and cannot be used for your custom main class;
* the generated name can be modified through {@code Maven} plugin configuration and annotation processor configuration.
* <p>
* The generated main class adds registration of all available services to injection config,
* and disables service discovery to prevent any and all reflection done by service registry itself.
* <p>
* To have correct startup of your application, use the {@code ApplicationMain} as your application entry point
* (such as a {@code mainClass} configured in {@code pom.xml} when using Maven and Helidon application parents).
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Main {
}

/**
* Provides an ability to create more than one service instance from a single service definition.
* This is useful when the cardinality can only be determined at runtime.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.service.inject.codegen;

import java.util.List;
import java.util.Set;

import io.helidon.codegen.CodegenUtil;
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.TypeName;
import io.helidon.common.types.TypeNames;

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_REGISTRY;
import static io.helidon.service.inject.codegen.InjectCodegenTypes.STRING_ARRAY;

/**
* Utility for {@value #CLASS_NAME} class generation.
*/
final class ApplicationMainGenerator {
/**
* Default class name of the generated main class.
*/
public static final String CLASS_NAME = "ApplicationMain";
/**
* Signature of the {@code serviceDescriptors} method (this method is abstract in InjectionMain).
*/
private static final ElementSignature METHOD_SERVICE_DESCRIPTORS = ElementSignature.createMethod(
TypeNames.PRIMITIVE_VOID,
"serviceDescriptors",
List.of(INJECT_CONFIG_BUILDER));
/**
* Signature of the {@code discoverServices} method.
*/
private static final ElementSignature METHOD_DISCOVER_SERVICES = ElementSignature.createMethod(
TypeNames.PRIMITIVE_BOOLEAN,
"discoverServices",
List.of());
/**
* Signature of the {@code runLevels} method.
*/
private static final ElementSignature METHOD_RUN_LEVELS = ElementSignature.createMethod(
DOUBLE_ARRAY,
"runLevels",
List.of(INJECT_CONFIG, INJECT_REGISTRY));

private ApplicationMainGenerator() {
}

/**
* Generate the common parts of the type.
*
* <ul>
* <li>Class declaration with javadoc, @Generated, and copyright, with name {@value #CLASS_NAME}</li>
* <li>Protected constructor with javadoc</li>
* <li>{@code public static void main} method with javadoc</li>
* </ul>
*
* @param generator generator type name
* @param declaredSignatures signatures declared on custom main class (if any)
* @param superType super type of the generated class
* @param generatedType the type to generate
* @param discoverServices whether to discover services (false when all services are manually registered to the
* builder)
* @param addRunLevels whether to add run levels from discovered services
* @param serviceDescriptorsHandler handler of the service descriptor method
* @param runLevelHandler handler of the run level method
* @return class model builder
*/
public static ClassModel.Builder generate(TypeName generator,
Set<ElementSignature> declaredSignatures,
TypeName superType,
TypeName generatedType,
boolean discoverServices,
boolean addRunLevels,
CodeGeneratorHandler serviceDescriptorsHandler,
CodeGeneratorHandler runLevelHandler) {
ClassModel.Builder classModel = ClassModel.builder()
.type(generatedType)
.accessModifier(AccessModifier.PUBLIC)
.copyright(CodegenUtil.copyright(generator,
generator,
generatedType))
.addAnnotation(CodegenUtil.generatedAnnotation(generator,
generatedType,
generatedType,
"1",
""))
.superType(superType)
.addDescriptionLine("Main class generated for Helidon Inject Application.")
.isFinal(true);

classModel.addConstructor(ctr -> ctr
.accessModifier(AccessModifier.PACKAGE_PRIVATE));

classModel.addMethod(main -> main
.accessModifier(AccessModifier.PUBLIC)
.isStatic(true)
.returnType(TypeNames.PRIMITIVE_VOID)
.description("Start the application.")
.name("main")
.addParameter(args -> args
.type(STRING_ARRAY)
.description("Command line arguments.")
.name("args"))
.update(it -> mainMethodBody(generatedType, it)));

if (!declaredSignatures.contains(METHOD_SERVICE_DESCRIPTORS)) {
// only create this method if it is not created by the user
classModel.addMethod(methodModel -> methodModel
.name("serviceDescriptors")
.addAnnotation(Annotations.OVERRIDE)
.accessModifier(AccessModifier.PROTECTED)
.returnType(TypeNames.PRIMITIVE_VOID)
.addParameter(config -> config
.type(INJECT_CONFIG_BUILDER)
.name("config"))
.update(it -> serviceDescriptorsHandler.handle(classModel, it, "config")));
}

if (discoverServices && !declaredSignatures.contains(METHOD_DISCOVER_SERVICES)) {
classModel.addMethod(discoverServicesMethod -> discoverServicesMethod
.name("discoverServices")
.addAnnotation(Annotations.OVERRIDE)
.accessModifier(AccessModifier.PROTECTED)
.returnType(TypeNames.PRIMITIVE_BOOLEAN)
.addContentLine("return true;"));
}

if (addRunLevels && !declaredSignatures.contains(METHOD_RUN_LEVELS)) {
classModel.addMethod(runLevels -> runLevels
.name("runLevels")
.addAnnotation(Annotations.OVERRIDE)
.accessModifier(AccessModifier.PUBLIC)
.returnType(DOUBLE_ARRAY)
.addParameter(config -> config
.type(INJECT_CONFIG)
.name("config"))
.addParameter(registry -> registry
.type(INJECT_REGISTRY)
.name("registry"))
.addContentLine("return new double[] {")
.update(it -> runLevelHandler.handle(classModel, it, "config"))
.addContentLine("};"));
}

return classModel;
}

private static void mainMethodBody(TypeName type, Method.Builder method) {
method.addContent("new ")
.addContent(type)
.addContentLine("().start(args);");
}

/**
* Handler to generated method serviceDescriptors in {@value #CLASS_NAME} class.
*/
@FunctionalInterface
public interface CodeGeneratorHandler {
/**
* Handle the class model (to allow adding constants and helper methods), and method model (to add the body).
*
* @param classModel class model of the generated main
* @param methodModel method model of the serviceDescriptors method
* @param configParamName name of the parameter of {@code InjectConfig.Builder}
*/
void handle(ClassModel.Builder classModel, Method.Builder methodModel, String configParamName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.helidon.service.inject.codegen;

import io.helidon.common.types.TypeName;
import io.helidon.common.types.TypeNames;

/**
* Types for code generation from Helidon Service Inject API and Helidon Service Inject.
Expand Down Expand Up @@ -63,6 +64,10 @@ public class InjectCodegenTypes {
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.RunLevel}.
*/
public static final TypeName INJECTION_RUN_LEVEL = TypeName.create("io.helidon.service.inject.api.Injection.RunLevel");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Main}.
*/
public static final TypeName INJECTION_MAIN = TypeName.create("io.helidon.service.inject.api.Injection.Main");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InjectionPointFactory}.
*/
Expand Down Expand Up @@ -120,6 +125,27 @@ public class InjectCodegenTypes {
*/
public static final TypeName INJECT_SERVICE_DESCRIPTOR =
TypeName.create("io.helidon.service.inject.api.InjectServiceDescriptor");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectConfig.Builder}.
*/
public static final TypeName INJECT_CONFIG_BUILDER =
TypeName.create("io.helidon.service.inject.InjectConfig.Builder");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.InjectConfig}.
*/
public static final TypeName INJECT_CONFIG =
TypeName.create("io.helidon.service.inject.InjectConfig");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InjectRegistry}.
*/
public static final TypeName INJECT_REGISTRY =
TypeName.create("io.helidon.service.inject.api.InjectRegistry");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.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.api.InvocationException}.
*/
Expand Down Expand Up @@ -210,6 +236,22 @@ public class InjectCodegenTypes {
public static final TypeName INJECT_G_EVENT_OBSERVER_REGISTRATION =
TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.EventObserverRegistration");

/**
* {@link io.helidon.common.types.TypeName} for String array.
*/
public static final TypeName STRING_ARRAY = TypeName.builder()
.from(TypeNames.STRING)
.array(true)
.build();
/**
* {@link io.helidon.common.types.TypeName} for primitive double array.
*/
public static final TypeName DOUBLE_ARRAY = TypeName.builder()
.from(TypeNames.PRIMITIVE_DOUBLE)
.array(true)
.build();


private InjectCodegenTypes() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.helidon.service.inject.codegen;

import java.util.Set;
import java.util.function.Function;

import io.helidon.codegen.Option;
import io.helidon.common.GenericType;
Expand Down Expand Up @@ -48,6 +49,43 @@ public final class InjectOptions {
TypeName::create,
new GenericType<Set<TypeName>>() { });

/**
* 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.
*/
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.
* Defaults to false. In case a custom main class is present (annotated with Injection.Main), this option is ignored.
* <p>
* 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<Boolean> 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() {
}
}
Loading

0 comments on commit b03a59a

Please sign in to comment.