diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml index 2ffa7a7e31d..06916523fe1 100644 --- a/.github/actions/common/action.yml +++ b/.github/actions/common/action.yml @@ -71,10 +71,9 @@ runs: git config --global core.eol lf - name: Set up GraalVM if: ${{ inputs.native-image == 'true' }} - uses: graalvm/setup-graalvm@v1.2.1 + uses: graalvm/setup-graalvm@v1.2.4 with: - java-version: ${{ env.JAVA_VERSION }} - version: ${{ env.GRAALVM_VERSION }} + java-version: ${{ env.GRAALVM_VERSION || env.JAVA_VERSION }} components: ${{ env.GRAALVM_COMPONENTS }} check-for-updates: 'false' set-java-home: 'false' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6300c09bf91..cdf932620ec 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,6 +30,7 @@ on: env: JAVA_VERSION: 21 + GRAALVM_VERSION: 21.0.3 JAVA_DISTRO: oracle MAVEN_ARGS: | -B -fae -e @@ -337,7 +338,7 @@ jobs: strategy: matrix: os: [ ubuntu-20.04, macos-14 ] - module: [ mp-1, mp-2, mp-3, se-1 ] + module: [ mp-1, mp-2, mp-3, se-1, inject ] include: - { os: ubuntu-20.04, platform: linux } - { os: macos-14, platform: macos } diff --git a/all/pom.xml b/all/pom.xml index 009c6cc6f34..a4d6e458d54 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -1128,6 +1128,18 @@ io.helidon.service helidon-service-codegen + + io.helidon.service.inject + helidon-service-inject-codegen + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + io.helidon.metadata helidon-metadata-hson diff --git a/bom/pom.xml b/bom/pom.xml index 8b6af76b14d..60d895a39ed 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1491,6 +1491,21 @@ helidon-service-codegen ${helidon.version} + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-api + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject + ${helidon.version} + diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java index 2336ba4426e..22e13bb88b5 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java @@ -122,157 +122,13 @@ public void processingOver(RoundContext roundContext) { } } - private void updateServiceLoaderResource() { - CodegenFiler filer = ctx.filer(); - FilerTextResource serviceLoaderResource = filer.textResource("META-INF/helidon/service.loader"); - List lines = new ArrayList<>(serviceLoaderResource.lines()); - if (lines.isEmpty()) { - lines.add("# List of service contracts we want to support either from service registry, or from service loader"); - } - boolean modified = false; - for (String serviceLoaderContract : this.serviceLoaderContracts) { - if (!lines.contains(serviceLoaderContract)) { - modified = true; - lines.add(serviceLoaderContract); - } - } - - if (modified) { - serviceLoaderResource.lines(lines); - serviceLoaderResource.write(); - } - } - - private void process(RoundContext roundContext, TypeInfo blueprint) { - TypeContext typeContext = TypeContext.create(ctx, blueprint); - AnnotationDataBlueprint blueprintDef = typeContext.blueprintData(); - AnnotationDataConfigured configuredData = typeContext.configuredData(); - TypeContext.PropertyData propertyData = typeContext.propertyData(); - TypeContext.TypeInformation typeInformation = typeContext.typeInfo(); - CustomMethods customMethods = typeContext.customMethods(); - - TypeInfo typeInfo = typeInformation.blueprintType(); - TypeName prototype = typeContext.typeInfo().prototype(); - String ifaceName = prototype.className(); - List typeGenericArguments = blueprintDef.typeArguments(); - String typeArgumentString = createTypeArgumentString(typeGenericArguments); - - // prototype interface (with inner class Builder) - ClassModel.Builder classModel = ClassModel.builder() - .type(prototype) - .classType(ElementKind.INTERFACE) - .copyright(CodegenUtil.copyright(GENERATOR, - typeInfo.typeName(), - prototype)); - - String javadocString = blueprintDef.javadoc(); - List typeArguments = new ArrayList<>(); - if (javadocString == null) { - classModel.description("Interface generated from definition. Please add javadoc to the definition interface."); - typeGenericArguments.forEach(arg -> typeArguments.add(TypeArgument.builder() - .token(arg.className()) - .build())); - } else { - Javadoc javadoc = Javadoc.parse(blueprintDef.javadoc()); - classModel.javadoc(javadoc); - typeGenericArguments.forEach(arg -> { - TypeArgument.Builder tokenBuilder = TypeArgument.builder().token(arg.className()); - if (javadoc.genericsTokens().containsKey(arg.className())) { - tokenBuilder.description(javadoc.genericsTokens().get(arg.className())); - } - typeArguments.add(tokenBuilder.build()); - }); - } - typeArguments.forEach(classModel::addGenericArgument); - - if (blueprintDef.builderPublic()) { - classModel.addJavadocTag("see", "#builder()"); - } - if (!propertyData.hasRequired() && blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { - classModel.addJavadocTag("see", "#create()"); - } - - typeContext.typeInfo() - .annotationsToGenerate() - .forEach(annotation -> classModel.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation))); - - classModel.addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, - typeInfo.typeName(), - prototype, - "1", - "")); - - if (typeContext.blueprintData().prototypePublic()) { - classModel.accessModifier(AccessModifier.PUBLIC); - } else { - classModel.accessModifier(AccessModifier.PACKAGE_PRIVATE); - } - blueprintDef.extendsList() - .forEach(classModel::addInterface); - - generateCustomConstants(customMethods, classModel); - - TypeName builderTypeName = TypeName.builder() - .from(TypeName.create(prototype.fqName() + ".Builder")) - .typeArguments(prototype.typeArguments()) - .build(); - - - // static Builder builder() - addBuilderMethod(classModel, builderTypeName, typeArguments, ifaceName); - - // static Builder builder(T instance) - addCopyBuilderMethod(classModel, builderTypeName, prototype, typeArguments, ifaceName, typeArgumentString); - - // static T create(Config config) - addCreateFromConfigMethod(blueprintDef, - configuredData, - prototype, - typeArguments, - ifaceName, - typeArgumentString, - classModel); - - // static X create() - addCreateDefaultMethod(blueprintDef, propertyData, classModel, prototype, ifaceName, typeArgumentString, typeArguments); - - generateCustomMethods(classModel, builderTypeName, prototype, customMethods); - - // abstract class BuilderBase... - GenerateAbstractBuilder.generate(classModel, - typeInformation.prototype(), - typeInformation.runtimeObject().orElseGet(typeInformation::prototype), - typeArguments, - typeContext); - // class Builder extends BuilderBase ... - GenerateBuilder.generate(classModel, - typeInformation.prototype(), - typeInformation.runtimeObject().orElseGet(typeInformation::prototype), - typeArguments, - typeContext.blueprintData().isFactory(), - typeContext); - - roundContext.addGeneratedType(prototype, - classModel, - blueprint.typeName(), - blueprint.originatingElementValue()); - - if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { - for (PrototypeProperty property : typeContext.propertyData().properties()) { - if (property.configuredOption().provider()) { - this.serviceLoaderContracts.add(property.configuredOption().providerType().genericTypeName().fqName()); - } - } - } - } - private static void addCreateDefaultMethod(AnnotationDataBlueprint blueprintDef, - TypeContext.PropertyData propertyData, - ClassModel.Builder classModel, - TypeName prototype, - String ifaceName, - String typeArgumentString, - List typeArguments) { + TypeContext.PropertyData propertyData, + ClassModel.Builder classModel, + TypeName prototype, + String ifaceName, + String typeArgumentString, + List typeArguments) { if (blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { /* static X create() @@ -291,12 +147,12 @@ static X create() } private static void addCreateFromConfigMethod(AnnotationDataBlueprint blueprintDef, - AnnotationDataConfigured configuredData, - TypeName prototype, - List typeArguments, - String ifaceName, - String typeArgumentString, - ClassModel.Builder classModel) { + AnnotationDataConfigured configuredData, + TypeName prototype, + List typeArguments, + String ifaceName, + String typeArgumentString, + ClassModel.Builder classModel) { if (blueprintDef.createFromConfigPublic() && configuredData.configured()) { Method.Builder method = Method.builder() .name("create") @@ -322,11 +178,11 @@ private static void addCreateFromConfigMethod(AnnotationDataBlueprint blueprintD } private static void addCopyBuilderMethod(ClassModel.Builder classModel, - TypeName builderTypeName, - TypeName prototype, - List typeArguments, - String ifaceName, - String typeArgumentString) { + TypeName builderTypeName, + TypeName prototype, + List typeArguments, + String ifaceName, + String typeArgumentString) { classModel.addMethod(builder -> { builder.isStatic(true) .name("builder") @@ -341,9 +197,9 @@ private static void addCopyBuilderMethod(ClassModel.Builder classModel, } private static void addBuilderMethod(ClassModel.Builder classModel, - TypeName builderTypeName, - List typeArguments, - String ifaceName) { + TypeName builderTypeName, + List typeArguments, + String ifaceName) { classModel.addMethod(builder -> { builder.isStatic(true) .name("builder") @@ -380,8 +236,9 @@ private static void generateCustomMethods(ClassModel.Builder classModel, // in that case compare just by classname (leap of faith...) if (typeName.packageName().isBlank()) { String className = typeName.className(); - if (!(className.equals(prototype.className()) - || className.equals(builderTypeName.className()))) { + if (!( + className.equals(prototype.className()) + || className.equals(builderTypeName.className()))) { // based on class names continue; } @@ -437,6 +294,159 @@ private static void generateCustomMethods(ClassModel.Builder classModel, } } + private void updateServiceLoaderResource() { + CodegenFiler filer = ctx.filer(); + FilerTextResource serviceLoaderResource = filer.textResource("META-INF/helidon/service.loader"); + List lines = new ArrayList<>(serviceLoaderResource.lines()); + if (lines.isEmpty()) { + lines.add("# List of service contracts we want to support either from service registry, or from service loader"); + } + boolean modified = false; + for (String serviceLoaderContract : this.serviceLoaderContracts) { + if (!lines.contains(serviceLoaderContract)) { + modified = true; + lines.add(serviceLoaderContract); + } + } + + if (modified) { + serviceLoaderResource.lines(lines); + serviceLoaderResource.write(); + } + } + + private void process(RoundContext roundContext, TypeInfo blueprint) { + TypeContext typeContext = TypeContext.create(ctx, blueprint); + AnnotationDataBlueprint blueprintDef = typeContext.blueprintData(); + AnnotationDataConfigured configuredData = typeContext.configuredData(); + TypeContext.PropertyData propertyData = typeContext.propertyData(); + TypeContext.TypeInformation typeInformation = typeContext.typeInfo(); + CustomMethods customMethods = typeContext.customMethods(); + + TypeInfo typeInfo = typeInformation.blueprintType(); + TypeName prototype = typeContext.typeInfo().prototype(); + String ifaceName = prototype.className(); + List typeGenericArguments = blueprintDef.typeArguments(); + String typeArgumentString = createTypeArgumentString(typeGenericArguments); + + // prototype interface (with inner class Builder) + ClassModel.Builder classModel = ClassModel.builder() + .type(prototype) + .classType(ElementKind.INTERFACE) + .copyright(CodegenUtil.copyright(GENERATOR, + typeInfo.typeName(), + prototype)); + + String javadocString = blueprintDef.javadoc(); + List typeArguments = new ArrayList<>(); + Javadoc javadoc; + if (javadocString == null) { + javadoc = Javadoc.parse("Interface generated from definition. Please add javadoc to the " + + "definition interface."); + } else { + javadoc = Javadoc.parse(blueprintDef.javadoc()); + } + classModel.javadoc(javadoc); + + typeGenericArguments.forEach(arg -> { + TypeArgument.Builder tokenBuilder = TypeArgument.builder() + .token(arg.className()); + if (!arg.upperBounds().isEmpty()) { + arg.upperBounds().forEach(tokenBuilder::addBound); + } + if (javadoc.genericsTokens().containsKey(arg.className())) { + tokenBuilder.description(javadoc.genericsTokens().get(arg.className())); + } + typeArguments.add(tokenBuilder.build()); + }); + + List typeArgumentNames = typeArguments.stream() + .map(it -> TypeName.createFromGenericDeclaration(it.className())) + .collect(Collectors.toList()); + typeArguments.forEach(classModel::addGenericArgument); + + if (blueprintDef.builderPublic()) { + classModel.addJavadocTag("see", "#builder()"); + } + if (!propertyData.hasRequired() && blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { + classModel.addJavadocTag("see", "#create()"); + } + + typeContext.typeInfo() + .annotationsToGenerate() + .forEach(annotation -> classModel.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation))); + + classModel.addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + typeInfo.typeName(), + prototype, + "1", + "")); + + if (typeContext.blueprintData().prototypePublic()) { + classModel.accessModifier(AccessModifier.PUBLIC); + } else { + classModel.accessModifier(AccessModifier.PACKAGE_PRIVATE); + } + blueprintDef.extendsList() + .forEach(classModel::addInterface); + + generateCustomConstants(customMethods, classModel); + + TypeName builderTypeName = TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".Builder")) + .typeArguments(prototype.typeArguments()) + .build(); + + // static Builder builder() + addBuilderMethod(classModel, builderTypeName, typeArguments, ifaceName); + + // static Builder builder(T instance) + addCopyBuilderMethod(classModel, builderTypeName, prototype, typeArguments, ifaceName, typeArgumentString); + + // static T create(Config config) + addCreateFromConfigMethod(blueprintDef, + configuredData, + prototype, + typeArguments, + ifaceName, + typeArgumentString, + classModel); + + // static X create() + addCreateDefaultMethod(blueprintDef, propertyData, classModel, prototype, ifaceName, typeArgumentString, typeArguments); + + generateCustomMethods(classModel, builderTypeName, prototype, customMethods); + + // abstract class BuilderBase... + GenerateAbstractBuilder.generate(classModel, + typeInformation.prototype(), + typeInformation.runtimeObject().orElseGet(typeInformation::prototype), + typeArguments, + typeArgumentNames, + typeContext); + // class Builder extends BuilderBase ... + GenerateBuilder.generate(classModel, + typeInformation.prototype(), + typeInformation.runtimeObject().orElseGet(typeInformation::prototype), + typeArguments, + typeArgumentNames, + typeContext.blueprintData().isFactory(), + typeContext); + + roundContext.addGeneratedType(prototype, + classModel, + blueprint.typeName(), + blueprint.originatingElementValue()); + + if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { + for (PrototypeProperty property : typeContext.propertyData().properties()) { + if (property.configuredOption().provider()) { + this.serviceLoaderContracts.add(property.configuredOption().providerType().genericTypeName().fqName()); + } + } + } + } + private Collection addBlueprintsForValidation(Set blueprints) { List result = new ArrayList<>(); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java index cfd9931b2c3..cadf5f55a2e 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java @@ -84,17 +84,17 @@ static FactoryMethods create(CodegenContext ctx, private static Optional builder(CodegenContext ctx, TypeHandler typeHandler, Set builderCandidates) { - if (typeHandler.actualType().equals(OBJECT)) { + if (typeHandler.actualType().equals(OBJECT) + || typeHandler.actualType().primitive() + || typeHandler.actualType().generic()) { return Optional.empty(); } + builderCandidates.add(typeHandler.actualType()); FactoryMethod found = null; FactoryMethod secondary = null; for (TypeName builderCandidate : builderCandidates) { - if (typeHandler.actualType().primitive()) { - // primitive methods do not have builders - continue; - } + TypeInfo typeInfo = ctx.typeInfo(builderCandidate.genericTypeName()).orElse(null); if (typeInfo == null) { if (secondary == null) { diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java index 27a75cd7f0e..4da0d457c6f 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java @@ -59,6 +59,7 @@ static void generate(ClassModel.Builder classModel, TypeName prototype, TypeName runtimeType, List typeArguments, + List typeArgumentNames, TypeContext typeContext) { Optional superType = typeContext.typeInfo() .superPrototype(); @@ -73,7 +74,7 @@ static void generate(ClassModel.Builder classModel, .description("type of the builder extending this abstract builder") .bound(TypeName.builder() .from(TypeName.create(prototype.fqName() + ".BuilderBase")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) .build())) @@ -107,7 +108,7 @@ static void generate(ClassModel.Builder classModel, // method "from(prototype)" fromInstanceMethod(builder, typeContext, prototype); - fromBuilderMethod(builder, typeContext, typeArguments); + fromBuilderMethod(builder, typeContext, typeArgumentNames); // method preBuildPrototype() - handles providers, decorator preBuildPrototypeMethod(builder, typeContext); @@ -126,7 +127,7 @@ static void generate(ClassModel.Builder classModel, true); // before the builder class is finished, we also generate a protected implementation - generatePrototypeImpl(builder, typeContext, typeArguments); + generatePrototypeImpl(builder, typeContext, typeArguments, typeArgumentNames); }); } @@ -431,7 +432,8 @@ private static void fromInstanceMethod(InnerClass.Builder builder, TypeContext t private static void fromBuilderMethod(InnerClass.Builder classBuilder, TypeContext typeContext, - List arguments) { + List arguments) { + TypeName prototype = typeContext.typeInfo().prototype(); TypeName parameterType = TypeName.builder() .from(TypeName.create(prototype.fqName() + ".BuilderBase")) @@ -823,7 +825,8 @@ private static void requiredValidation(Method.Builder validateBuilder, TypeConte private static void generatePrototypeImpl(InnerClass.Builder classBuilder, TypeContext typeContext, - List typeArguments) { + List typeArguments, + List typeArgumentNames) { Optional superPrototype = typeContext.typeInfo() .superPrototype(); TypeName prototype = typeContext.typeInfo().prototype(); @@ -864,7 +867,7 @@ private static void generatePrototypeImpl(InnerClass.Builder classBuilder, .addParameter(param -> param.name("builder") .type(TypeName.builder() .from(TypeName.create(ifaceName + ".BuilderBase")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .addTypeArgument(TypeArgument.create("?")) .addTypeArgument(TypeArgument.create("?")) .build()) diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java index 6a5e780bbf3..21438e8f9a9 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java @@ -40,12 +40,13 @@ static void generate(ClassModel.Builder classBuilder, TypeName prototype, TypeName runtimeType, List typeArguments, + List typeArgumentNames, boolean isFactory, TypeContext typeContext) { classBuilder.addInnerClass(builder -> { TypeName builderType = TypeName.builder() .from(TypeName.create(prototype.fqName() + ".Builder")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .build(); typeArguments.forEach(builder::addGenericArgument); builder.name("Builder") @@ -53,7 +54,7 @@ static void generate(ClassModel.Builder classBuilder, .description("Fluent API builder for {@link " + runtimeType.className() + "}.") .superType(TypeName.builder() .from(TypeName.create(prototype.fqName() + ".BuilderBase")) - .addTypeArguments(typeArguments) + .addTypeArguments(typeArgumentNames) .addTypeArgument(builderType) .addTypeArgument(prototype) .build()) diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java index 4acd5b5975d..775878131ab 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java @@ -22,10 +22,12 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import io.helidon.codegen.CodegenException; import io.helidon.codegen.CodegenValidator; import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.codegen.classmodel.Field; @@ -38,8 +40,38 @@ import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.codegen.Types.OPTION_DEFAULT; +import static io.helidon.common.types.TypeNames.BOXED_BOOLEAN; +import static io.helidon.common.types.TypeNames.BOXED_BYTE; +import static io.helidon.common.types.TypeNames.BOXED_CHAR; +import static io.helidon.common.types.TypeNames.BOXED_DOUBLE; +import static io.helidon.common.types.TypeNames.BOXED_FLOAT; +import static io.helidon.common.types.TypeNames.BOXED_INT; +import static io.helidon.common.types.TypeNames.BOXED_LONG; +import static io.helidon.common.types.TypeNames.BOXED_SHORT; +import static io.helidon.common.types.TypeNames.BOXED_VOID; +import static io.helidon.common.types.TypeNames.PRIMITIVE_BOOLEAN; +import static io.helidon.common.types.TypeNames.PRIMITIVE_BYTE; +import static io.helidon.common.types.TypeNames.PRIMITIVE_CHAR; +import static io.helidon.common.types.TypeNames.PRIMITIVE_DOUBLE; +import static io.helidon.common.types.TypeNames.PRIMITIVE_FLOAT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_INT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_LONG; +import static io.helidon.common.types.TypeNames.PRIMITIVE_SHORT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_VOID; class TypeHandler { + private static final Map BOXED_TO_PRIMITIVE = Map.of( + BOXED_BOOLEAN, PRIMITIVE_BOOLEAN, + BOXED_BYTE, PRIMITIVE_BYTE, + BOXED_SHORT, PRIMITIVE_SHORT, + BOXED_INT, PRIMITIVE_INT, + BOXED_LONG, PRIMITIVE_LONG, + BOXED_CHAR, PRIMITIVE_CHAR, + BOXED_FLOAT, PRIMITIVE_FLOAT, + BOXED_DOUBLE, PRIMITIVE_DOUBLE, + BOXED_VOID, PRIMITIVE_VOID + ); + private final TypeName enclosingType; private final TypedElementInfo annotatedMethod; private final String name; @@ -74,14 +106,18 @@ static TypeHandler create(TypeName blueprintType, if (TypeNames.SUPPLIER.equals(returnType)) { return new TypeHandlerSupplier(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } + if (TypeNames.SET.equals(returnType)) { + checkTypeArgsSizeAndTypes(annotatedMethod, returnType, TypeNames.SET, 1); return new TypeHandlerSet(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } if (TypeNames.LIST.equals(returnType)) { + checkTypeArgsSizeAndTypes(annotatedMethod, returnType, TypeNames.LIST, 1); return new TypeHandlerList(blueprintType, annotatedMethod, name, getterName, setterName, returnType); } if (TypeNames.MAP.equals(returnType)) { + checkTypeArgsSizeAndTypes(annotatedMethod, returnType, TypeNames.MAP, 2); return new TypeHandlerMap(blueprintType, annotatedMethod, name, getterName, setterName, returnType, sameGeneric); } @@ -96,6 +132,12 @@ static TypeName toWildcard(TypeName typeName) { if (typeName.wildcard()) { return typeName; } + if (typeName.generic()) { + return TypeName.builder() + .className(typeName.className()) + .wildcard(true) + .build(); + } return TypeName.builder(typeName).wildcard(true).build(); } @@ -524,6 +566,30 @@ protected void declaredSetter(InnerClass.Builder classBuilder, classBuilder.addMethod(builder); } + protected TypeName toPrimitive(TypeName typeName) { + return Optional.ofNullable(BOXED_TO_PRIMITIVE.get(typeName)) + .orElse(typeName); + } + + private static void checkTypeArgsSizeAndTypes(TypedElementInfo annotatedMethod, + TypeName returnType, + TypeName collectionType, + int expectedTypeArgs) { + List typeNames = returnType.typeArguments(); + if (typeNames.size() != expectedTypeArgs) { + throw new CodegenException("Property of type " + collectionType.fqName() + " must have " + expectedTypeArgs + + " type arguments defined", + annotatedMethod.originatingElementValue()); + } + for (TypeName typeName : typeNames) { + if (typeName.wildcard()) { + throw new CodegenException("Property of type " + returnType.resolvedName() + " is not supported for builder," + + " as wildcards cannot be handled correctly in setters", + annotatedMethod.originatingElementValue()); + } + } + } + private T singleDefault(List defaultValues) { if (defaultValues.isEmpty()) { throw new IllegalArgumentException("Default values configured for " + name() + " are empty, one value is expected."); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java index cabba3b18a6..31ebdb6acca 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java @@ -239,8 +239,13 @@ String generateMapListFromConfig(FactoryMethods factoryMethods) { @Override TypeName argumentTypeName() { + TypeName type = actualType(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive() || type.array()) { + return declaredType(); + } + return TypeName.builder(collectionType) - .addTypeArgument(toWildcard(actualType())) + .addTypeArgument(toWildcard(type)) .build(); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java index d55faaf9ca0..8914159cb96 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java @@ -134,9 +134,18 @@ void generateFromConfig(Method.Builder method, @Override TypeName argumentTypeName() { + TypeName firstType = declaredType().typeArguments().get(0); + if (!(TypeNames.STRING.equals(firstType) || toPrimitive(firstType).primitive() || firstType.array())) { + firstType = toWildcard(firstType); + } + TypeName secondType = declaredType().typeArguments().get(1); + if (!(TypeNames.STRING.equals(secondType) || toPrimitive(secondType).primitive() || secondType.array())) { + secondType = toWildcard(secondType); + } + return TypeName.builder(MAP) - .addTypeArgument(toWildcard(declaredType().typeArguments().get(0))) - .addTypeArgument(toWildcard(declaredType().typeArguments().get(1))) + .addTypeArgument(firstType) + .addTypeArgument(secondType) .build(); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java index b81dbc7f547..ecb5a1dca30 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java @@ -17,7 +17,6 @@ package io.helidon.builder.codegen; import java.util.Iterator; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -27,47 +26,18 @@ import io.helidon.codegen.classmodel.Javadoc; import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.codegen.Types.CHAR_ARRAY; import static io.helidon.codegen.CodegenUtil.capitalize; -import static io.helidon.common.types.TypeNames.BOXED_BOOLEAN; -import static io.helidon.common.types.TypeNames.BOXED_BYTE; -import static io.helidon.common.types.TypeNames.BOXED_CHAR; -import static io.helidon.common.types.TypeNames.BOXED_DOUBLE; -import static io.helidon.common.types.TypeNames.BOXED_FLOAT; -import static io.helidon.common.types.TypeNames.BOXED_INT; -import static io.helidon.common.types.TypeNames.BOXED_LONG; -import static io.helidon.common.types.TypeNames.BOXED_SHORT; -import static io.helidon.common.types.TypeNames.BOXED_VOID; import static io.helidon.common.types.TypeNames.OPTIONAL; -import static io.helidon.common.types.TypeNames.PRIMITIVE_BOOLEAN; -import static io.helidon.common.types.TypeNames.PRIMITIVE_BYTE; -import static io.helidon.common.types.TypeNames.PRIMITIVE_CHAR; -import static io.helidon.common.types.TypeNames.PRIMITIVE_DOUBLE; -import static io.helidon.common.types.TypeNames.PRIMITIVE_FLOAT; -import static io.helidon.common.types.TypeNames.PRIMITIVE_INT; -import static io.helidon.common.types.TypeNames.PRIMITIVE_LONG; -import static io.helidon.common.types.TypeNames.PRIMITIVE_SHORT; -import static io.helidon.common.types.TypeNames.PRIMITIVE_VOID; // declaration in builder is always non-generic, so no need to modify default values class TypeHandlerOptional extends TypeHandler.OneTypeHandler { - private static final Map BOXED_TO_PRIMITIVE = Map.of( - BOXED_BOOLEAN, PRIMITIVE_BOOLEAN, - BOXED_BYTE, PRIMITIVE_BYTE, - BOXED_SHORT, PRIMITIVE_SHORT, - BOXED_INT, PRIMITIVE_INT, - BOXED_LONG, PRIMITIVE_LONG, - BOXED_CHAR, PRIMITIVE_CHAR, - BOXED_FLOAT, PRIMITIVE_FLOAT, - BOXED_DOUBLE, PRIMITIVE_DOUBLE, - BOXED_VOID, PRIMITIVE_VOID - ); - TypeHandlerOptional(TypeName blueprintType, TypedElementInfo annotatedMethod, String name, String getterName, String setterName, TypeName declaredType) { @@ -98,14 +68,12 @@ Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilde @Override TypeName argumentTypeName() { TypeName type = actualType(); - if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive()) { - return TypeName.builder(OPTIONAL) - .addTypeArgument(type) - .build(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive() || type.array()) { + return declaredType(); } return TypeName.builder(OPTIONAL) - .addTypeArgument(toWildcard(actualType())) + .addTypeArgument(toWildcard(type)) .build(); } @@ -238,8 +206,14 @@ void setters(InnerClass.Builder classBuilder, private void declaredSetter(InnerClass.Builder classBuilder, TypeName returnType, Javadoc blueprintJavadoc) { + boolean generic = !actualType().typeArguments().isEmpty(); // declared setter - optional is package local, field is never optional in builder classBuilder.addMethod(builder -> builder.name(setterName()) + .update(it -> { + if (generic) { + it.addAnnotation(Annotation.create(SuppressWarnings.class, "unchecked")); + } + }) .accessModifier(AccessModifier.PACKAGE_PRIVATE) .description(blueprintJavadoc.content()) .returnType(returnType, "updated builder instance") @@ -283,9 +257,4 @@ private String optionalSuffix(TypeName typeName) { } return ""; } - - private TypeName toPrimitive(TypeName typeName) { - return Optional.ofNullable(BOXED_TO_PRIMITIVE.get(typeName)) - .orElse(typeName); - } } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java index ff22e7c5cb0..0abfd028e27 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java @@ -55,8 +55,13 @@ Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilde @Override TypeName argumentTypeName() { + TypeName type = actualType(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive() || type.array()) { + return declaredType(); + } + return TypeName.builder(SUPPLIER) - .addTypeArgument(toWildcard(actualType())) + .addTypeArgument(toWildcard(type)) .build(); } diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java index 2d744cc8b85..f55d1fbba88 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * 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. @@ -80,7 +80,13 @@ static TypeName toWildcard(TypeName typeName) { if (typeName.wildcard()) { return typeName; } - return TypeName.builder(typeName).wildcard(true).build(); + if (typeName.equals(TypeNames.STRING)) { + return typeName; + } + if (typeName.typeArguments().isEmpty()) { + return TypeName.builder(typeName).wildcard(true).build(); + } + return typeName; } protected static TypeName collectionImplType(TypeName typeName) { diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java index b424700b30f..813e7071927 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * 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. @@ -215,9 +215,15 @@ String generateMapListFromConfig(FactoryMethods factoryMethods) { @Override TypeName argumentTypeName() { - return TypeName.builder(collectionType) - .addTypeArgument(toWildcard(actualType())) - .build(); + if (actualType().equals(TypeNames.OBJECT)) { + return TypeName.builder(collectionType) + .addTypeArgument(TypeName.builder() + .from(TypeNames.OBJECT) + .wildcard(true) + .build()) + .build(); + } + return declaredType(); } @Override diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GenericsBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GenericsBlueprint.java new file mode 100644 index 00000000000..a2af630974c --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GenericsBlueprint.java @@ -0,0 +1,41 @@ +/* + * 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.builder.test.testsubjects; + +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +// test all the funny generics +@Prototype.Blueprint +interface GenericsBlueprint { + @Option.Singular + Set tValues(); + + @Option.Singular + Set xValues(); + + @Option.Singular + Map mappedValues(); + + Optional> complicatedValue(); +} diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/GenericTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/GenericTest.java new file mode 100644 index 00000000000..56302b9f099 --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/GenericTest.java @@ -0,0 +1,96 @@ +/* + * 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.builder.test; + +import java.io.Serializable; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.builder.test.testsubjects.Generics; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +class GenericTest { + + @Test + void genericsTest() { + Generics.Builder builder = Generics.builder(); + builder.addTValue(new ImplOfT("firstTValue")); + builder.addTValue(new ImplOfT("secondTValue")); + builder.addXValue(new ImplOfT("firstXValue")); + builder.addXValue(new ImplOfT("secondXValue")); + builder.putMappedValue(new ImplOfT("key"), new ImplOfT("value")); + builder.putMappedValue(new ImplOfT("key2"), new ImplOfT("value2")); + builder.complicatedValue(new Supply()); + + Generics generics = builder.build(); + + assertThat(generics.tValues(), hasItems(new ImplOfT("firstTValue"), new ImplOfT("secondTValue"))); + assertThat(generics.xValues(), hasItems(new ImplOfT("firstXValue"), new ImplOfT("secondXValue"))); + assertThat(generics.complicatedValue(), not(Optional.empty())); + assertThat(generics.complicatedValue().get().get(), is(new ImplOfT("supplied"))); + assertThat(generics.mappedValues().size(), is(2)); + } + + private static class Supply implements Supplier { + @Override + public ImplOfT get() { + return new ImplOfT("supplied"); + } + } + private static class ImplOfT implements CharSequence, Serializable { + private final String delegate; + + private ImplOfT(String delegate) { + this.delegate = delegate; + } + + @Override + public int length() { + return delegate.length(); + } + + @Override + public char charAt(int index) { + return delegate.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return delegate.subSequence(start, end); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ImplOfT implOfT)) { + return false; + } + return Objects.equals(delegate, implOfT.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java new file mode 100644 index 00000000000..e962800d60a --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.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.common.types; + +import java.lang.reflect.Type; + +/** + * A wrapper for {@link io.helidon.common.types.TypeName} that uses the resolved name for equals and hashCode. + * This allows us to collect interfaces including type arguments. + * + * @see TypeName#resolvedName() + */ +public interface ResolvedType { + /** + * Create a type name from a type (such as class). + * + * @param type the type + * @return type name for the provided type + */ + static ResolvedType create(Type type) { + return new ResolvedTypeImpl(TypeName.create(type)); + } + + /** + * Creates a type name from a fully qualified class name. + * + * @param typeName the FQN of the class type + * @return the TypeName for the provided type name + */ + static ResolvedType create(String typeName) { + return new ResolvedTypeImpl(TypeName.create(typeName)); + } + + /** + * Create a type name from a type name. + * + * @param typeName the type + * @return type name for the provided type + */ + static ResolvedType create(TypeName typeName) { + if (typeName instanceof ResolvedType rt) { + return rt; + } + return new ResolvedTypeImpl(typeName); + } + + /** + * Provides the underlying type name that backs this resolved type. + * + * @return the type name this resolved type represents + */ + TypeName type(); +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java new file mode 100644 index 00000000000..24c73cd5fc6 --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java @@ -0,0 +1,72 @@ +/* + * 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.common.types; + +class ResolvedTypeImpl implements ResolvedType, Comparable { + private final TypeName typeName; + private final String resolvedName; + private final boolean noTypes; + + ResolvedTypeImpl(TypeName typeName) { + this.typeName = typeName; + this.resolvedName = typeName.resolvedName(); + this.noTypes = typeName.typeArguments().isEmpty(); + } + + @Override + public TypeName type() { + return typeName; + } + + @Override + public int hashCode() { + return noTypes ? typeName.hashCode() : resolvedName.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ResolvedType other)) { + return false; + } + if (other instanceof ResolvedTypeImpl rti) { + return resolvedName.equals(rti.resolvedName); + } + return other.type().resolvedName().equals(resolvedName); + } + + @Override + public int compareTo(ResolvedType o) { + int diff = resolvedName.compareTo(o.type().resolvedName()); + if (diff != 0) { + // different name + return diff; + } + diff = Boolean.compare(typeName.primitive(), o.type().primitive()); + if (diff != 0) { + return diff; + } + return Boolean.compare(typeName.array(), o.type().array()); + } + + @Override + public String toString() { + return resolvedName; + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 704c34d139c..e744bb97bae 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -35,12 +35,34 @@ interface TypeInfoBlueprint extends Annotated { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @return the type name */ @Option.Required TypeName typeName(); + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
    + *
  • {@link TypeName#packageName()}
  • + *
  • {@link io.helidon.common.types.TypeName#className()}
  • + *
  • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
  • + *
+ * + * @return raw type of this type info + */ + TypeName rawType(); + + /** + * The declared type name, including type parameters. + * + * @return type name with declared type parameters + */ + TypeName declaredType(); + /** * Description, such as javadoc, if available. * diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java index a054c002053..952cd31cc0e 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * 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. @@ -68,6 +68,18 @@ public void decorate(TypeInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); + + // new methods, simplify for tests + if (target.rawType().isEmpty()) { + target.typeName() + .map(TypeName::genericTypeName) + .ifPresent(target::rawType); + } + if (target.declaredType().isEmpty()) { + // this may not be correct, but is correct for all types that do not have any declaration of generics + // so it simplifies a lot of use cases + target.rawType().ifPresent(target::declaredType); + } } } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java index 1d9d5fa7118..d712863b5b4 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java @@ -44,7 +44,7 @@ *
  • {@link #declaredName()} and {@link #resolvedName()}.
  • * */ -@Prototype.Blueprint +@Prototype.Blueprint(decorator = TypeNameSupport.Decorator.class) @Prototype.CustomMethods(TypeNameSupport.class) @Prototype.Implement("java.lang.Comparable") interface TypeNameBlueprint { @@ -137,11 +137,39 @@ default String classNameWithEnclosingNames() { * if {@link #typeArguments()} exist, this list MUST exist and have the same size and order (it maps the name to the type). * * @return type parameter names as declared on this type, or names that represent the {@link #typeArguments()} + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information */ @Option.Singular @Option.Redundant + @Deprecated(forRemoval = true, since = "4.2.0") List typeParameters(); + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of lower bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List lowerBounds(); + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of upper bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List upperBounds(); + /** * Indicates whether this type is a {@code java.util.List}. * diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java index e8a9d38809b..7c87d4eaced 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.LinkedList; @@ -23,9 +24,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; final class TypeNameSupport { private static final TypeName PRIMITIVE_BOOLEAN = TypeName.create(boolean.class); @@ -141,59 +144,57 @@ static String fqName(TypeName instance) { @Prototype.PrototypeMethod @Prototype.Annotated("java.lang.Override") // defined on blueprint static String resolvedName(TypeName instance) { - String name = calcName(instance, "."); - boolean isObject = Object.class.getName().equals(name) || "?".equals(name); - StringBuilder nameBuilder = (isObject) - ? new StringBuilder(instance.wildcard() ? "?" : name) - : new StringBuilder(instance.wildcard() ? "? extends " + name : name); - - if (!instance.typeArguments().isEmpty()) { - nameBuilder.append("<"); - int i = 0; - for (TypeName param : instance.typeArguments()) { - if (i > 0) { - nameBuilder.append(", "); - } - nameBuilder.append(param.resolvedName()); - i++; - } - nameBuilder.append(">"); + if (instance.generic() || instance.wildcard()) { + return resolveGenericName(instance); } - - if (instance.array()) { - nameBuilder.append("[]"); - } - - return nameBuilder.toString(); + return resolveClassName(instance); } /** * Update builder from the provided type. * * @param builder builder to update - * @param type type to get information (package name, class name, primitive, array) + * @param type type to get information (package name, class name, primitive, array) */ @Prototype.BuilderMethod static void type(TypeName.BuilderBase builder, Type type) { Objects.requireNonNull(type); if (type instanceof Class classType) { - Class componentType = classType.isArray() ? classType.getComponentType() : classType; - builder.packageName(componentType.getPackageName()); - builder.className(componentType.getSimpleName()); - builder.primitive(componentType.isPrimitive()); - builder.array(classType.isArray()); - - Class enclosingClass = classType.getEnclosingClass(); - LinkedList enclosingTypes = new LinkedList<>(); - while (enclosingClass != null) { - enclosingTypes.addFirst(enclosingClass.getSimpleName()); - enclosingClass = enclosingClass.getEnclosingClass(); + updateFromClass(builder, classType); + return; + } + Type reflectGenericType = type; + + if (type instanceof GenericType gt) { + if (gt.isClass()) { + // simple case - just a class + updateFromClass(builder, gt.rawType()); + return; + } else { + // complex case - has generic type arguments + reflectGenericType = gt.type(); } - builder.enclosingNames(enclosingTypes); - } else { - // todo - throw new IllegalArgumentException("Currently we only support class as a parameter, but got: " + type); } + + // translate the generic type into type name + if (reflectGenericType instanceof ParameterizedType pt) { + Type raw = pt.getRawType(); + if (raw instanceof Class theClass) { + updateFromClass(builder, theClass); + } else { + throw new IllegalArgumentException("Raw type of a ParameterizedType is not a class: " + raw.getClass().getName() + + ", for " + pt.getTypeName()); + } + + Type[] actualTypeArguments = pt.getActualTypeArguments(); + for (Type actualTypeArgument : actualTypeArguments) { + builder.addTypeArgument(TypeName.create(actualTypeArgument)); + } + return; + } + + throw new IllegalArgumentException("We can only create a type from a class, GenericType, or a ParameterizedType," + + " but got: " + reflectGenericType.getClass().getName()); } /** @@ -309,6 +310,71 @@ static TypeName createFromGenericDeclaration(String genericAliasTypeName) { .build(); } + private static String resolveGenericName(TypeName instance) { + // ?, ? super Something; ? extends Something + String prefix = instance.wildcard() ? "?" : instance.className(); + if (instance.upperBounds().isEmpty() && instance.lowerBounds().isEmpty()) { + return prefix; + } + if (instance.lowerBounds().isEmpty()) { + return prefix + " extends " + instance.upperBounds() + .stream() + .map(it -> { + if (it.generic()) { + return it.wildcard() ? "?" : it.className(); + } + return it.resolvedName(); + }) + .collect(Collectors.joining(" & ")); + } + TypeName lowerBound = instance.lowerBounds().getFirst(); + if (lowerBound.generic()) { + return prefix + " super " + (lowerBound.wildcard() ? "?" : lowerBound.className()); + } + return prefix + " super " + lowerBound.resolvedName(); + + } + + private static String resolveClassName(TypeName instance) { + String name = calcName(instance, "."); + StringBuilder nameBuilder = new StringBuilder(name); + + if (!instance.typeArguments().isEmpty()) { + nameBuilder.append("<"); + int i = 0; + for (TypeName param : instance.typeArguments()) { + if (i > 0) { + nameBuilder.append(", "); + } + nameBuilder.append(param.resolvedName()); + i++; + } + nameBuilder.append(">"); + } + + if (instance.array()) { + nameBuilder.append("[]"); + } + + return nameBuilder.toString(); + } + + private static void updateFromClass(TypeName.BuilderBase builder, Class classType) { + Class componentType = classType.isArray() ? classType.getComponentType() : classType; + builder.packageName(componentType.getPackageName()); + builder.className(componentType.getSimpleName()); + builder.primitive(componentType.isPrimitive()); + builder.array(classType.isArray()); + + Class enclosingClass = classType.getEnclosingClass(); + LinkedList enclosingTypes = new LinkedList<>(); + while (enclosingClass != null) { + enclosingTypes.addFirst(enclosingClass.getSimpleName()); + enclosingClass = enclosingClass.getEnclosingClass(); + } + builder.enclosingNames(enclosingTypes); + } + private static String calcName(TypeName instance, String typeSeparator) { String className; if (instance.enclosingNames().isEmpty()) { @@ -320,4 +386,38 @@ private static String calcName(TypeName instance, String typeSeparator) { return (instance.primitive() || instance.packageName().isEmpty()) ? className : instance.packageName() + "." + className; } + + static class Decorator implements Prototype.BuilderDecorator> { + @Override + public void decorate(TypeName.BuilderBase target) { + fixWildcards(target); + } + + private void fixWildcards(TypeName.BuilderBase target) { + // handle wildcards correct + if (target.wildcard()) { + if (target.upperBounds().size() == 1 && target.lowerBounds().isEmpty()) { + // backward compatible for (? extends X) + TypeName upperBound = target.upperBounds().getFirst(); + target.className(upperBound.className()); + target.packageName(upperBound.packageName()); + target.enclosingNames(upperBound.enclosingNames()); + } + // wildcard set, if package + class name as well, set them as upper bounds + if (target.className().isPresent() + && !target.className().get().equals("?") + && target.upperBounds().isEmpty() + && target.lowerBounds().isEmpty()) { + TypeName upperBound = TypeName.builder() + .from(target) + .wildcard(false) + .build(); + if (!upperBound.equals(TypeNames.OBJECT)) { + target.addUpperBound(upperBound); + } + } + target.generic(true); + } + } + } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java index f539e5849af..caf18dbf866 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java @@ -171,6 +171,10 @@ public final class TypeNames { * Type name of the type name. */ public static final TypeName TYPE_NAME = TypeName.create(TypeName.class); + /** + * Type name of the resolved type name. + */ + public static final TypeName RESOLVED_TYPE_NAME = TypeName.create(ResolvedType.class); /** * Type name of typed element info. */ diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java index af10b1337d5..7716e72b89e 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.processing.AbstractProcessor; @@ -100,8 +101,7 @@ public boolean process(Set annotations, RoundEnvironment // we want everything to execute in the classloader of this type, so service loaders // use the classpath of the annotation processor, and not some "random" classloader, such as a maven one try { - doProcess(annotations, roundEnv); - return true; + return doProcess(annotations, roundEnv); } catch (CodegenException e) { Object originatingElement = e.originatingElement() .orElse(null); @@ -120,12 +120,12 @@ public boolean process(Set annotations, RoundEnvironment } } - private void doProcess(Set annotations, RoundEnvironment roundEnv) { + private boolean doProcess(Set annotations, RoundEnvironment roundEnv) { ctx.logger().log(TRACE, "Process annotations: " + annotations + ", processing over: " + roundEnv.processingOver()); if (roundEnv.processingOver()) { codegen.processingOver(); - return; + return annotations.isEmpty(); } Set usedAnnotations = usedAnnotations(annotations); @@ -133,34 +133,39 @@ private void doProcess(Set annotations, RoundEnvironment if (usedAnnotations.isEmpty()) { // no annotations, no types, still call the codegen, maybe it has something to do codegen.process(List.of()); - return; + return annotations.isEmpty(); } List allTypes = discoverTypes(usedAnnotations, roundEnv); codegen.process(allTypes); + + return usedAnnotations.stream() + .map(UsedAnnotation::annotationElement) + .collect(Collectors.toSet()) + .equals(annotations); } private Set usedAnnotations(Set annotations) { - var exactTypes = codegen.supportedAnnotations() - .stream() - .map(TypeName::fqName) - .collect(Collectors.toSet()); - var prefixes = codegen.supportedAnnotationPackagePrefixes(); + var typePredicate = typePredicate(codegen.supportedAnnotations(), codegen.supportedAnnotationPackagePrefixes()); + var metaPredicate = typePredicate(codegen.supportedMetaAnnotations(), Set.of()); Set result = new HashSet<>(); for (TypeElement annotation : annotations) { TypeName typeName = TypeName.create(annotation.getQualifiedName().toString()); + Set supportedAnnotations = new HashSet<>(); + // first check direct support (through exact type or prefix) + if (typePredicate.test(typeName)) { + supportedAnnotations.add(typeName); + } /* find meta annotations that are supported: - annotation that annotates the current annotation */ - Set supportedAnnotations = new HashSet<>(); - if (supportedAnnotation(exactTypes, prefixes, typeName)) { - supportedAnnotations.add(typeName); - } - addSupportedAnnotations(exactTypes, prefixes, supportedAnnotations, typeName); + addSupportedAnnotations(metaPredicate, supportedAnnotations, typeName); + + // and add all the annotations if (!supportedAnnotations.isEmpty()) { result.add(new UsedAnnotation(typeName, annotation, supportedAnnotations)); } @@ -169,21 +174,23 @@ private Set usedAnnotations(Set annotatio return result; } - private boolean supportedAnnotation(Set exactTypes, Set prefixes, TypeName annotationType) { - if (exactTypes.contains(annotationType.fqName())) { - return true; - } - String packagePrefix = annotationType.packageName() + "."; - for (String prefix : prefixes) { - if (packagePrefix.startsWith(prefix)) { + private Predicate typePredicate(Set typeNames, Set prefixes) { + return typeName -> { + if (typeNames.contains(typeName)) { return true; } - } - return false; + + String packagePrefix = typeName.packageName() + "."; + for (String prefix : prefixes) { + if (packagePrefix.startsWith(prefix)) { + return true; + } + } + return false; + }; } - private void addSupportedAnnotations(Set exactTypes, - Set prefixes, + private void addSupportedAnnotations(Predicate typeNamePredicate, Set supportedAnnotations, TypeName annotationType) { Optional foundInfo = AptTypeInfoFactory.create(ctx, annotationType); @@ -192,9 +199,9 @@ private void addSupportedAnnotations(Set exactTypes, List annotations = annotationInfo.annotations(); for (Annotation annotation : annotations) { TypeName typeName = annotation.typeName(); - if (supportedAnnotation(exactTypes, prefixes, typeName)) { + if (typeNamePredicate.test(typeName)) { if (supportedAnnotations.add(typeName)) { - addSupportedAnnotations(exactTypes, prefixes, supportedAnnotations, typeName); + addSupportedAnnotations(typeNamePredicate, supportedAnnotations, typeName); } } } diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java index 19248d1600f..83ddf357e66 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java @@ -19,8 +19,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -33,10 +37,14 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; import static io.helidon.common.types.TypeName.createFromGenericDeclaration; @@ -70,6 +78,10 @@ public static Optional createTypeName(DeclaredType type) { * none or error) */ public static Optional createTypeName(TypeMirror typeMirror) { + return createTypeName(new HashSet<>(), typeMirror); + } + + private static Optional createTypeName(Set inProgress, TypeMirror typeMirror) { TypeKind kind = typeMirror.getKind(); if (kind.isPrimitive()) { Class type = switch (kind) { @@ -92,9 +104,35 @@ public static Optional createTypeName(TypeMirror typeMirror) { return Optional.of(TypeName.create(void.class)); } case TYPEVAR -> { - return Optional.of(createFromGenericDeclaration(typeMirror.toString())); + if (!inProgress.add(typeMirror)) { + return Optional.empty(); // prevent infinite loop + } + + try { + var builder = TypeName.builder(createFromGenericDeclaration(typeMirror.toString())); + + var typeVar = ((TypeVariable) typeMirror); + handleBounds(inProgress, typeVar.getUpperBound(), builder::addUpperBound); + handleBounds(inProgress, typeVar.getLowerBound(), builder::addLowerBound); + + return Optional.of(builder.build()); + } finally { + inProgress.remove(typeMirror); + } + } + case WILDCARD -> { + WildcardType vt = ((WildcardType) typeMirror); + var builder = TypeName.builder() + .generic(true) + .wildcard(true) + .className("?"); + + handleBounds(inProgress, vt.getExtendsBound(), builder::addUpperBound); + handleBounds(inProgress, vt.getSuperBound(), builder::addLowerBound); + + return Optional.of(builder.build()); } - case WILDCARD, ERROR -> { + case ERROR -> { return Optional.of(TypeName.create(typeMirror.toString())); } // this is most likely a type that is code generated as part of this round, best effort @@ -107,7 +145,7 @@ public static Optional createTypeName(TypeMirror typeMirror) { } if (typeMirror instanceof ArrayType arrayType) { - return Optional.of(TypeName.builder(createTypeName(arrayType.getComponentType()).orElseThrow()) + return Optional.of(TypeName.builder(createTypeName(inProgress, arrayType.getComponentType()).orElseThrow()) .array(true) .build()); } @@ -115,21 +153,47 @@ public static Optional createTypeName(TypeMirror typeMirror) { if (typeMirror instanceof DeclaredType declaredType) { List typeParams = declaredType.getTypeArguments() .stream() - .map(AptTypeFactory::createTypeName) + .map(it -> createTypeName(inProgress, it)) .flatMap(Optional::stream) .collect(Collectors.toList()); - TypeName result = createTypeName(declaredType.asElement()).orElse(null); + TypeName result = createTypeName(inProgress, declaredType.asElement()).orElse(null); if (typeParams.isEmpty() || result == null) { return Optional.ofNullable(result); } + if (!inProgress.add(typeMirror)) { + return Optional.empty(); // prevent infinite loop + } return Optional.of(TypeName.builder(result).typeArguments(typeParams).build()); } throw new IllegalStateException("Unknown type mirror: " + typeMirror); } + private static void handleBounds(Set processed, TypeMirror boundMirror, Consumer boundHandler) { + if (boundMirror == null) { + return; + } + if (boundMirror.getKind() != TypeKind.NULL) { + if (boundMirror.getKind() == TypeKind.INTERSECTION) { + IntersectionType it = (IntersectionType) boundMirror; + it.getBounds() + .stream() + .filter(Predicate.not(processed::equals)) + .map(typeMirror -> createTypeName(processed, typeMirror)) + .flatMap(Optional::stream) + .filter(Predicate.not(TypeNames.OBJECT::equals)) + .forEach(boundHandler); + + } else { + createTypeName(processed, boundMirror) + .filter(Predicate.not(TypeNames.OBJECT::equals)) + .ifPresent(boundHandler); + } + } + } + /** * Create type from type mirror. The element is needed to correctly map * type arguments to type parameters. @@ -139,7 +203,7 @@ public static Optional createTypeName(TypeMirror typeMirror) { * @return type for the provided values */ public static Optional createTypeName(TypeElement element, TypeMirror mirror) { - Optional result = AptTypeFactory.createTypeName(mirror); + Optional result = createTypeName(new HashSet<>(), mirror); if (result.isEmpty()) { return result; } @@ -167,12 +231,16 @@ public static Optional createTypeName(TypeElement element, TypeMirror * @return the associated type name instance */ public static Optional createTypeName(Element type) { + return createTypeName(new HashSet<>(), type); + } + + private static Optional createTypeName(Set processed, Element type) { if (type instanceof VariableElement) { - return createTypeName(type.asType()); + return createTypeName(processed, type.asType()); } - if (type instanceof ExecutableElement) { - return createTypeName(((ExecutableElement) type).getReturnType()); + if (type instanceof ExecutableElement ee) { + return createTypeName(processed, ee.getReturnType()); } List classNames = new ArrayList<>(); diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java index 70146921928..93c08f0fde5 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java @@ -430,6 +430,7 @@ private static Optional createUncached(AptContext ctx, // this is probably forward referencing a generated type, ignore return Optional.empty(); } + TypeName declaredTypeName = declaredTypeName(ctx, genericTypeName); List annotations = createAnnotations(ctx, foundType, elementUtils); @@ -451,10 +452,13 @@ private static Optional createUncached(AptContext ctx, annotationsOnTypeOrElements, it)); + Set modifiers = toModifierNames(typeElement.getModifiers()); TypeInfo.Builder builder = TypeInfo.builder() .originatingElement(typeElement) .typeName(typeName) + .rawType(genericTypeName) + .declaredType(declaredTypeName) .kind(kind(typeElement.getKind())) .annotations(annotations) .inheritedAnnotations(inheritedAnnotations) @@ -547,6 +551,12 @@ private static Optional createUncached(AptContext ctx, } } + private static TypeName declaredTypeName(AptContext ctx, TypeName typeName) { + TypeElement typeElement = ctx.aptEnv().getElementUtils().getTypeElement(typeName.fqName()); + // we know this type exists, we do not have to check for null + return AptTypeFactory.createTypeName(typeElement.asType()).orElseThrow(); + } + private static void collectEnclosedElements(Predicate elementPredicate, List elementsWeCareAbout, List otherElements, diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java index e13b6783f07..86cde78ce0e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java @@ -34,8 +34,13 @@ void addImports(ImportOrganizer.Builder imports) { annotations.forEach(annotation -> annotation.addImports(imports)); } - List annotations() { - return annotations; + /** + * List of annotations on this component. + * + * @return annotations + */ + public List annotations() { + return List.copyOf(annotations); } abstract static class Builder, T extends AnnotatedComponent> extends CommonComponent.Builder { @@ -67,11 +72,7 @@ public B addDescriptionLine(String line) { * @return updated builder instance */ public B addAnnotation(io.helidon.common.types.Annotation annotation) { - return addAnnotation(newAnnot -> { - newAnnot.type(annotation.typeName()); - annotation.values() - .forEach(newAnnot::addParameter); - }); + return addAnnotation(Annotation.create(annotation)); } /** diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java index 6fb06591d3c..b84bbf95365 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java @@ -31,10 +31,12 @@ public final class Annotation extends CommonComponent { private final List parameters; + private final io.helidon.common.types.Annotation commonAnnotation; private Annotation(Builder builder) { super(builder); this.parameters = List.copyOf(builder.parameters.values()); + this.commonAnnotation = builder.commonAnntation; } /** @@ -88,6 +90,35 @@ public static Annotation parse(String annotationDefinition) { return builder.build(); } + /** + * Create a class model annotation from common types annotation. + * + * @param annotation annotation to process + * @return a new class model annotation + */ + public static Annotation create(io.helidon.common.types.Annotation annotation) { + return builder().from(annotation).build(); + } + + /** + * Convert class model annotation to Helidon Common Types annotation. + * + * @return common types annotation + */ + public io.helidon.common.types.Annotation toTypesAnnotation() { + if (this.commonAnnotation != null) { + return commonAnnotation; + } + var builder = io.helidon.common.types.Annotation.builder() + .typeName(type().genericTypeName()); + + for (AnnotationParameter parameter : parameters) { + builder.putValue(parameter.name(), parameter.value()); + } + + return builder.build(); + } + @Override void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { @@ -128,6 +159,7 @@ void addImports(ImportOrganizer.Builder imports) { public static final class Builder extends CommonComponent.Builder { private final Map parameters = new LinkedHashMap<>(); + private io.helidon.common.types.Annotation commonAnntation; private Builder() { } @@ -210,6 +242,13 @@ public Builder addParameter(AnnotationParameter parameter) { return this; } + Builder from(io.helidon.common.types.Annotation annotation) { + this.commonAnntation = annotation; + type(annotation.typeName()); + annotation.values() + .forEach(this::addParameter); + return this; + } } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java index cd6bdfb00b5..1b07dd3633d 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java @@ -72,6 +72,10 @@ void writeValue(ModelWriter writer, ImportOrganizer imports) throws IOException writer.write(resolveValueToString(imports, type(), objectValue)); } + Object value() { + return objectValue; + } + private static Set resolveImports(Object value) { Set imports = new HashSet<>(); diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java index 611f3826c0f..0784de2055f 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java @@ -19,10 +19,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -81,36 +83,100 @@ public abstract class ClassBase extends AnnotatedComponent { this.superType = builder.superType; } - private static int methodCompare(Method method1, Method method2) { - if (method1.accessModifier() == method2.accessModifier()) { - return 0; - } else { - return method1.accessModifier().compareTo(method2.accessModifier()); - } + /** + * All declared fields. + * + * @return fields + */ + public List fields() { + return List.copyOf(fields); } - private static int fieldComparator(Field field1, Field field2) { - //This is here for ordering purposes. - if (field1.accessModifier() == field2.accessModifier()) { - if (field1.isFinal() == field2.isFinal()) { - if (field1.type().simpleTypeName().equals(field2.type().simpleTypeName())) { - if (field1.type().resolvedTypeName().equals(field2.type().resolvedTypeName())) { - return field1.name().compareTo(field2.name()); - } - return field1.type().resolvedTypeName().compareTo(field2.type().resolvedTypeName()); - } else if (field1.type().simpleTypeName().equalsIgnoreCase(field2.type().simpleTypeName())) { - //To ensure that types with the types with the same name, - //but with the different capital letters, will not be mixed - return field1.type().simpleTypeName().compareTo(field2.type().simpleTypeName()); - } - //ignoring case sensitivity to ensure primitive types are properly sorted - return field1.type().simpleTypeName().compareToIgnoreCase(field2.type().simpleTypeName()); - } - //final fields should be before non-final - return Boolean.compare(field2.isFinal(), field1.isFinal()); - } else { - return field1.accessModifier().compareTo(field2.accessModifier()); - } + /** + * All declared methods. + * + * @return methods + */ + public List methods() { + return List.copyOf(methods); + } + + /** + * All declared inner classes. + * + * @return inner classes + */ + public List innerClasses() { + return List.copyOf(innerClasses); + } + + /** + * All declared constructors. + * + * @return constructors + */ + public List constructors() { + return List.copyOf(constructors); + } + + /** + * Kind of this type. + * + * @return kind + */ + public ElementKind kind() { + return switch (classType) { + case CLASS -> ElementKind.CLASS; + case INTERFACE -> ElementKind.INTERFACE; + }; + } + + /** + * Type name of the super class (if this is a class and it extends another class). + * + * @return super type + */ + public Optional superTypeName() { + return Optional.ofNullable(superType) + .map(Type::genericTypeName); + } + + /** + * Implemented interfaces. + * + * @return interfaces this type implements (or extends, if this is an interface) + */ + public List interfaceTypeNames() { + return interfaces.stream() + .map(Type::genericTypeName) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * Is this a final class. + * + * @return whether this class is final + */ + public boolean isFinal() { + return isFinal; + } + + /** + * Is this an abstract class. + * + * @return whether this class is abstract + */ + public boolean isAbstract() { + return isAbstract; + } + + /** + * Is this a static class. + * + * @return whether this class is static + */ + public boolean isStatic() { + return isStatic; } @Override @@ -179,6 +245,67 @@ void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrgani writer.write("}"); } + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + fields.forEach(field -> field.addImports(imports)); + staticFields.forEach(field -> field.addImports(imports)); + methods.forEach(method -> method.addImports(imports)); + staticMethods.forEach(method -> method.addImports(imports)); + interfaces.forEach(imp -> imp.addImports(imports)); + constructors.forEach(constructor -> constructor.addImports(imports)); + genericParameters.forEach(param -> param.addImports(imports)); + innerClasses.forEach(innerClass -> { + imports.from(innerClass.imports()); + innerClass.addImports(imports); + }); + if (superType != null) { + superType.addImports(imports); + } + } + + ClassType classType() { + return classType; + } + + Map innerClassesMap() { + Map result = new HashMap<>(); + innerClasses.forEach(innerClass -> result.put(innerClass.name(), innerClass)); + return result; + } + + private static int methodCompare(Method method1, Method method2) { + if (method1.accessModifier() == method2.accessModifier()) { + return 0; + } else { + return method1.accessModifier().compareTo(method2.accessModifier()); + } + } + + private static int fieldComparator(Field field1, Field field2) { + //This is here for ordering purposes. + if (field1.accessModifier() == field2.accessModifier()) { + if (field1.isFinal() == field2.isFinal()) { + if (field1.type().simpleTypeName().equals(field2.type().simpleTypeName())) { + if (field1.type().resolvedTypeName().equals(field2.type().resolvedTypeName())) { + return field1.name().compareTo(field2.name()); + } + return field1.type().resolvedTypeName().compareTo(field2.type().resolvedTypeName()); + } else if (field1.type().simpleTypeName().equalsIgnoreCase(field2.type().simpleTypeName())) { + //To ensure that types with the types with the same name, + //but with the different capital letters, will not be mixed + return field1.type().simpleTypeName().compareTo(field2.type().simpleTypeName()); + } + //ignoring case sensitivity to ensure primitive types are properly sorted + return field1.type().simpleTypeName().compareToIgnoreCase(field2.type().simpleTypeName()); + } + //final fields should be before non-final + return Boolean.compare(field2.isFinal(), field1.isFinal()); + } else { + return field1.accessModifier().compareTo(field2.accessModifier()); + } + } + private void writeGenericParameters(ModelWriter writer, Set declaredTokens, ImportOrganizer imports) throws IOException { writer.write("<"); @@ -261,29 +388,6 @@ private void writeInnerClasses(ModelWriter writer, Set declaredTokens, I writer.decreasePaddingLevel(); } - @Override - void addImports(ImportOrganizer.Builder imports) { - super.addImports(imports); - fields.forEach(field -> field.addImports(imports)); - staticFields.forEach(field -> field.addImports(imports)); - methods.forEach(method -> method.addImports(imports)); - staticMethods.forEach(method -> method.addImports(imports)); - interfaces.forEach(imp -> imp.addImports(imports)); - constructors.forEach(constructor -> constructor.addImports(imports)); - genericParameters.forEach(param -> param.addImports(imports)); - innerClasses.forEach(innerClass -> { - imports.from(innerClass.imports()); - innerClass.addImports(imports); - }); - if (superType != null) { - superType.addImports(imports); - } - } - - ClassType classType() { - return classType; - } - /** * Fluent API builder for {@link ClassBase}. * @@ -687,5 +791,9 @@ B isStatic(boolean isStatic) { ImportOrganizer.Builder importOrganizer() { return importOrganizer; } + + Map innerClasses() { + return innerClasses; + } } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java index 4b935235ab7..bb8b61a358b 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java @@ -17,6 +17,9 @@ import java.io.IOException; import java.io.Writer; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; import io.helidon.common.types.AccessModifier; @@ -209,6 +212,49 @@ public Builder type(TypeName type) { return this; } - } + /** + * Find if the provided type name is handled as part of this generated class. + * + * @param typeName type name to look for + * @return class base that matches the provided type name + */ + public Optional find(TypeName typeName) { + if (!typeName.packageName().equals(packageName)) { + return Optional.empty(); + } + if (typeName.classNameWithEnclosingNames().equals(name())) { + return Optional.of(build()); + } + + List enclosingNames = typeName.enclosingNames(); + if (enclosingNames.isEmpty()) { + // did not hit above, will not hit below + return Optional.empty(); + } + String topLevel = enclosingNames.getFirst(); + if (!topLevel.equals(name())) { + // not an inner class of this class + return Optional.empty(); + } + // look for inner classes, ignoring this class + Map innerClasses = super.innerClasses(); + + InnerClass inProgress = null; + for (int i = 1; i < enclosingNames.size(); i++) { + String enclosingName = enclosingNames.get(i); + InnerClass found = innerClasses.get(enclosingName); + if (found == null) { + return Optional.empty(); + } + inProgress = found; + innerClasses = inProgress.innerClassesMap(); + } + if (inProgress == null) { + return Optional.ofNullable(innerClasses.get(typeName.className())); + } + // we found an inner class that matches the full hierarchy + return Optional.ofNullable(inProgress.innerClassesMap().get(typeName.className())); + } + } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java index 1dc0c9f07c0..d0467c753d4 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java @@ -33,7 +33,12 @@ abstract class CommonComponent extends DescribableComponent { this.javadoc = builder.javadocBuilder.build(builder); } - String name() { + /** + * Name of this component. + * + * @return component name + */ + public String name() { return name; } @@ -41,7 +46,12 @@ Javadoc javadoc() { return javadoc; } - AccessModifier accessModifier() { + /** + * Access modifier of this component. + * + * @return access modifier + */ + public AccessModifier accessModifier() { return accessModifier; } @@ -216,6 +226,11 @@ B accessModifier(AccessModifier accessModifier) { return identity(); } + /** + * Name of this component. + * + * @return component name + */ String name() { return name; } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java index bded3b90bc0..64b6e4fcc4e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java @@ -153,6 +153,11 @@ public int hashCode() { return Objects.hash(isArray(), typeName.resolvedName()); } + @Override + TypeName typeName() { + return typeName; + } + static final class Builder extends ModelComponent.Builder { private final List typeParams = new ArrayList<>(); private TypeName typeName; diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java index 4237f75f8ce..109c787e529 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java @@ -19,6 +19,7 @@ import java.util.List; import io.helidon.common.types.Annotation; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; @@ -110,6 +111,25 @@ default T addContentCreate(TypeName typeName) { return addContent(""); } + /** + * Add content that creates a new {@link io.helidon.common.types.ResolvedType} in the generated code that is the same as the + * type name provided. + *

    + * To create a type name without type arguments (such as when used with {@code .class}), use + * {@link io.helidon.common.types.TypeName#genericTypeName()}. + *

    + * The generated content will be similar to: {@code TypeName.create("some.type.Name")} + * + * @param type type name to code generate + * @return updated builder instance + */ + default T addContentCreate(ResolvedType type) { + return addContent(ResolvedType.class) + .addContent(".create(\"") + .addContent(type.resolvedName()) + .addContent("\")"); + } + /** * Add content that creates a new {@link io.helidon.common.types.Annotation} in the generated code that is the same as the * annotation provided. diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java index 956a8f6efcb..d9976eb00e6 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java @@ -36,10 +36,25 @@ Type type() { return type; } - List description() { + /** + * Description (javadoc) of this component. + * + * @return description lines + */ + public List description() { return description; } + /** + * Type name of this component. + * + * @return type name + */ + public TypeName typeName() { + return type().typeName(); + } + + @Override void addImports(ImportOrganizer.Builder imports) { if (includeImport() && type != null) { diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java index 3e1866d86a8..5f3bda7881b 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.helidon.common.types.AccessModifier; import io.helidon.common.types.TypeName; @@ -54,10 +55,10 @@ void addImports(ImportOrganizer.Builder imports) { void writeThrows(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { - if (!exceptions().isEmpty()) { + if (!exceptionTypes().isEmpty()) { writer.write(" throws "); boolean first = true; - for (Type exception : exceptions()) { + for (Type exception : exceptionTypes()) { if (first) { first = false; } else { @@ -76,11 +77,27 @@ void writeBody(ModelWriter writer, ImportOrganizer imports) throws IOException { writer.write("\n"); } - List parameters() { - return parameters; + /** + * List of method parameters. + * + * @return parameters + */ + public List parameters() { + return List.copyOf(parameters); + } + + /** + * List of thrown exceptions. + * + * @return exceptions + */ + public List exceptions() { + return exceptions.stream() + .map(Type::genericTypeName) + .collect(Collectors.toUnmodifiableList()); } - List exceptions() { + List exceptionTypes() { return exceptions; } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java index 3f1b3f2ae26..e80365f381d 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java @@ -97,10 +97,6 @@ void addImports(ImportOrganizer.Builder imports) { defaultValue.addImports(imports); } - boolean isStatic() { - return isStatic; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -130,10 +126,33 @@ public String toString() { return accessModifier().modifierName() + " " + type().fqTypeName() + " " + name(); } - boolean isFinal() { + /** + * Is this field final. + * + * @return whether this is a final field + */ + public boolean isFinal() { return isFinal; } + /** + * Is this field static. + * + * @return whether this is a static field + */ + public boolean isStatic() { + return isStatic; + } + + /** + * Is this field volatile. + * + * @return whether this is a volatile field + */ + public boolean isVolatile() { + return isVolatile; + } + /** * Fluent API builder for {@link Field}. */ diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java index f02d54fdfb1..7f5723a3657 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java @@ -191,10 +191,43 @@ void addImports(ImportOrganizer.Builder imports) { type().addImports(imports); } - boolean isStatic() { + /** + * Is this a static method. + * + * @return whether this method is static + */ + public boolean isStatic() { return isStatic; } + /** + * Is this a final method. + * + * @return whether this method is final + */ + public boolean isFinal() { + return isFinal; + } + + /** + * Is this an abstract method. + * + * @return whether this method is abstract + */ + public boolean isAbstract() { + return isAbstract; + } + + /** + * Is this a default method (of an interface). + * + * @return whether this method is default + */ + + public boolean isDefault() { + return isDefault; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java index bfde677d9d3..e6bc9bea10e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java @@ -46,20 +46,6 @@ public static Builder builder() { return new Builder(); } - @Override - void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) - throws IOException { - for (Annotation annotation : annotations()) { - annotation.writeComponent(writer, declaredTokens, imports, classType); - writer.write(" "); - } - type().writeComponent(writer, declaredTokens, imports, classType); - if (vararg) { - writer.write("..."); - } - writer.write(" " + name()); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -83,17 +69,36 @@ public String toString() { return "Parameter{type=" + type().fqTypeName() + ", simpleType=" + type().simpleTypeName() + ", name=" + name() + "}"; } - List description() { + /** + * Description (javadoc lines) of this parameter. + * + * @return parameter description + */ + public List description() { return description; } + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, declaredTokens, imports, classType); + writer.write(" "); + } + type().writeComponent(writer, declaredTokens, imports, classType); + if (vararg) { + writer.write("..."); + } + writer.write(" " + name()); + } + /** * Fluent API builder for {@link Parameter}. */ public static final class Builder extends AnnotatedComponent.Builder { - private boolean vararg = false; private final List description = new ArrayList<>(); + private boolean vararg = false; private Builder() { } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java index 45e24c4100f..3ab5280599e 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java @@ -15,8 +15,8 @@ */ package io.helidon.codegen.classmodel; +import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import io.helidon.common.types.TypeName; @@ -37,15 +37,22 @@ static Type fromTypeName(TypeName typeName) { .type(typeName) .build(); } else if (typeName.wildcard()) { - boolean isObject = typeName.name().equals("?") || Object.class.getName().equals(typeName.name()); - if (isObject) { - return TypeArgument.create("?"); - } else { + List upperBounds = typeName.upperBounds(); + if (upperBounds.isEmpty()) { + if (typeName.lowerBounds().isEmpty()) { + return TypeArgument.create("?"); + } return TypeArgument.builder() .token("?") - .bound(extractBoundTypeName(typeName.genericTypeName())) + .bound(typeName.lowerBounds().getFirst()) + .lowerBound(true) .build(); } + + return TypeArgument.builder() + .token("?") + .bound(upperBounds.getFirst()) + .build(); } return ConcreteType.builder() .type(typeName) @@ -58,24 +65,10 @@ static Type fromTypeName(TypeName typeName) { return typeBuilder.build(); } - private static String extractBoundTypeName(TypeName instance) { - String name = calcName(instance); - StringBuilder nameBuilder = new StringBuilder(name); - - if (!instance.typeArguments().isEmpty()) { - nameBuilder.append('<') - .append(instance.typeArguments() - .stream() - .map(TypeName::resolvedName) - .collect(Collectors.joining(", "))) - .append('>'); - } + abstract TypeName typeName(); - if (instance.array()) { - nameBuilder.append("[]"); - } - - return nameBuilder.toString(); + private static String extractBoundTypeName(TypeName instance) { + return instance.resolvedName(); } private static String calcName(TypeName instance) { diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java index e44e09c6c06..f18cbbfffef 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java @@ -16,10 +16,12 @@ package io.helidon.codegen.classmodel; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import io.helidon.common.types.TypeName; @@ -29,14 +31,16 @@ public final class TypeArgument extends Type implements TypeName { private final TypeName token; - private final Type bound; + private final List bounds; private final List description; + private final boolean isLowerBound; private TypeArgument(Builder builder) { super(builder); this.token = builder.tokenBuilder.build(); - this.bound = builder.bound; + this.bounds = List.copyOf(builder.bounds); this.description = builder.description; + this.isLowerBound = builder.isLowerBound; } /** @@ -65,25 +69,45 @@ public TypeName boxed() { @Override public TypeName genericTypeName() { - if (bound == null) { - return null; + if (bounds.isEmpty()) { + return this; } - return bound.genericTypeName(); + return TypeName.builder() + .from(this) + .typeArguments(List.of()) + .typeParameters(List.of()) + .build(); } @Override void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { writer.write(token.className()); - if (bound != null) { + if (bounds.isEmpty()) { + return; + } + + if (isLowerBound) { + writer.write(" super "); + } else { writer.write(" extends "); - bound.writeComponent(writer, declaredTokens, imports, classType); + } + + if (bounds.size() == 1) { + bounds.getFirst().writeComponent(writer, declaredTokens, imports, classType); + return; + } + for (int i = 0; i < bounds.size(); i++) { + if (i != 0) { + writer.write(" & "); + } + bounds.get(i).writeComponent(writer, declaredTokens, imports, classType); } } @Override void addImports(ImportOrganizer.Builder imports) { - if (bound != null) { + for (Type bound : bounds) { bound.addImports(imports); } } @@ -176,12 +200,25 @@ public List typeParameters() { return List.of(); } + @Override + public List lowerBounds() { + // not yet supported + return List.of(); + } + + @Override + public List upperBounds() { + return bounds.stream() + .map(Type::typeName) + .collect(Collectors.toUnmodifiableList()); + } + @Override public String toString() { - if (bound == null) { + if (bounds.isEmpty()) { return "Token: " + token.className(); } - return "Token: " + token.className() + " Bound: " + bound; + return "Token: " + token.className() + " Bound: " + bounds; } @Override @@ -194,12 +231,12 @@ public boolean equals(Object o) { } TypeArgument typeArgument1 = (TypeArgument) o; return Objects.equals(token, typeArgument1.token) - && Objects.equals(bound, typeArgument1.bound); + && Objects.equals(bounds, typeArgument1.bounds); } @Override public int hashCode() { - return Objects.hash(token, bound); + return Objects.hash(token, bounds); } @Override @@ -207,6 +244,11 @@ public int compareTo(TypeName o) { return token.compareTo(o); } + @Override + TypeName typeName() { + return this; + } + /** * Fluent API builder for {@link TypeArgument}. */ @@ -214,7 +256,9 @@ public static final class Builder extends Type.Builder { private final TypeName.Builder tokenBuilder = TypeName.builder() .generic(true); - private Type bound; + private final List bounds = new ArrayList<>(); + + private boolean isLowerBound; private List description = List.of(); private Builder() { @@ -252,6 +296,18 @@ public Builder bound(Class bound) { return bound(TypeName.create(bound)); } + /** + * Bound is by default an upper bounds (presented as {@code extends} in code). + * By specifying that we use a {@code lowerBound}, the keyword will be {@code super}. + * + * @param lowerBound whether the specified bound is a lower bound (defaults to upper bound); ignore if no bound + * @return updated builder instance + */ + public Builder lowerBound(boolean lowerBound) { + this.isLowerBound = lowerBound; + return this; + } + /** * Type this argument is bound to. * @@ -259,7 +315,18 @@ public Builder bound(Class bound) { * @return updated builder instance */ public Builder bound(TypeName bound) { - this.bound = Type.fromTypeName(bound); + this.bounds.add(Type.fromTypeName(bound)); + return this; + } + + /** + * Type this argument is bound to (may have more than one for intersection types). + * + * @param bound argument bound + * @return updated builder instance + */ + public Builder addBound(TypeName bound) { + this.bounds.add(Type.fromTypeName(bound)); return this; } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ClassModelFactory.java b/codegen/codegen/src/main/java/io/helidon/codegen/ClassModelFactory.java new file mode 100644 index 00000000000..1135a3df4eb --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ClassModelFactory.java @@ -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.codegen; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.codegen.classmodel.Annotation; +import io.helidon.codegen.classmodel.ClassBase; +import io.helidon.codegen.classmodel.Constructor; +import io.helidon.codegen.classmodel.Executable; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.classmodel.Parameter; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Transforms class model to a {@link io.helidon.common.types.TypeInfo}. + */ +final class ClassModelFactory { + private ClassModelFactory() { + } + + static TypeInfo create(RoundContext ctx, + TypeName requestedTypeName, + ClassBase requestedType) { + + var builder = TypeInfo.builder() + .typeName(requestedTypeName) + .kind(requestedType.kind()) + .accessModifier(requestedType.accessModifier()) + .description(String.join("\n", requestedType.description())); + + for (Annotation annotation : requestedType.annotations()) { + builder.addAnnotation(annotation.toTypesAnnotation()); + } + + requestedType.superTypeName() + .flatMap(ctx::typeInfo) + .ifPresent(builder::superTypeInfo); + + List typeNames = requestedType.interfaceTypeNames(); + for (TypeName typeName : typeNames) { + ctx.typeInfo(typeName).ifPresent(builder::addInterfaceTypeInfo); + } + for (Field field : requestedType.fields()) { + addField(builder, field); + } + for (Constructor constructor : requestedType.constructors()) { + addConstructor(requestedTypeName, builder, constructor); + } + for (Method method : requestedType.methods()) { + addMethod(builder, method); + } + + for (ClassBase innerClass : requestedType.innerClasses()) { + addInnerClass(requestedTypeName, builder, innerClass); + } + + return builder.build(); + } + + private static void addInnerClass(TypeName requestedTypeName, TypeInfo.Builder builder, ClassBase innerClass) { + + builder.addElementInfo(innerInfo -> innerInfo + .typeName(innerClassTypeName(requestedTypeName, innerClass.name())) + .kind(ElementKind.CLASS) + .elementName(innerClass.name()) + .accessModifier(innerClass.accessModifier()) + .update(it -> { + if (innerClass.isStatic()) { + it.addElementModifier(Modifier.STATIC); + } + if (innerClass.isAbstract()) { + it.addElementModifier(Modifier.ABSTRACT); + } + if (innerClass.isFinal()) { + it.addElementModifier(Modifier.FINAL); + } + }) + .description(String.join("\n", innerClass.description())) + .update(it -> addAnnotations(it, innerClass.annotations())) + ); + } + + private static TypeName innerClassTypeName(TypeName requestedTypeName, String name) { + return TypeName.builder(requestedTypeName) + .addEnclosingName(requestedTypeName.className()) + .className(name) + .build(); + } + + private static void addMethod(TypeInfo.Builder builder, Method method) { + builder.addElementInfo(methodInfo -> methodInfo + .kind(ElementKind.METHOD) + .elementName(method.name()) + .accessModifier(method.accessModifier()) + .update(it -> { + if (method.isStatic()) { + it.addElementModifier(Modifier.STATIC); + } + if (method.isFinal()) { + it.addElementModifier(Modifier.FINAL); + } + if (method.isAbstract()) { + it.addElementModifier(Modifier.ABSTRACT); + } + if (method.isDefault()) { + it.addElementModifier(Modifier.DEFAULT); + } + }) + .description(String.join("\n", method.description())) + .update(it -> addAnnotations(it, method.annotations())) + .update(it -> processExecutable(it, method)) + .typeName(method.typeName()) + ); + } + + private static void processExecutable(TypedElementInfo.Builder builder, Executable executable) { + for (Parameter parameter : executable.parameters()) { + builder.addParameterArgument(arg -> arg + .kind(ElementKind.PARAMETER) + .elementName(parameter.name()) + .typeName(parameter.typeName()) + .description(String.join("\n", parameter.description())) + .update(it -> addAnnotations(it, parameter.annotations())) + ); + } + builder.addThrowsChecked(executable.exceptions() + .stream() + .collect(Collectors.toUnmodifiableSet())); + } + + private static void addConstructor(TypeName typeName, TypeInfo.Builder builder, Constructor constructor) { + builder.addElementInfo(ctrInfo -> ctrInfo + .typeName(typeName) + .kind(ElementKind.CONSTRUCTOR) + .accessModifier(constructor.accessModifier()) + .description(String.join("\n", constructor.description())) + .update(it -> addAnnotations(it, constructor.annotations())) + .update(it -> processExecutable(it, constructor)) + ); + } + + private static void addField(TypeInfo.Builder builder, Field field) { + builder.addElementInfo(fieldInfo -> fieldInfo + .typeName(field.typeName()) + .kind(ElementKind.FIELD) + .accessModifier(field.accessModifier()) + .elementName(field.name()) + .description(String.join("\n", field.description())) + .update(it -> addAnnotations(it, field.annotations())) + .update(it -> { + if (field.isStatic()) { + it.addElementModifier(Modifier.STATIC); + } + if (field.isFinal()) { + it.addElementModifier(Modifier.FINAL); + } + if (field.isVolatile()) { + it.addElementModifier(Modifier.VOLATILE); + } + }) + ); + } + + private static void addAnnotations(TypedElementInfo.Builder element, List annotations) { + for (Annotation annotation : annotations) { + element.addAnnotation(annotation.toTypesAnnotation()); + } + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java b/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java index 9641304b1ef..bad63f59bf3 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java @@ -20,9 +20,9 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; import java.util.function.Predicate; @@ -32,6 +32,7 @@ import io.helidon.codegen.spi.CodegenExtension; import io.helidon.codegen.spi.CodegenExtensionProvider; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; @@ -58,52 +59,48 @@ public class Codegen { SUPPORTED_APT_OPTIONS = Set.copyOf(supportedOptions); } - private final Map> typeToExtensions = new HashMap<>(); - private final Map> extensionPredicates = new IdentityHashMap<>(); private final CodegenContext ctx; - private final List extensions; + private final List extensions; private final Set supportedAnnotations; + private final Set supportedMetaAnnotations; private final Set supportedPackagePrefixes; private Codegen(CodegenContext ctx, TypeName generator) { this.ctx = ctx; + Set supportedAnnotations = new HashSet<>(ctx.mapperSupportedAnnotations()); + Set supportedMetaAnnotations = new HashSet<>(); + Set supportedPackagePrefixes = new HashSet<>(); + this.extensions = EXTENSIONS.stream() .map(it -> { CodegenExtension extension = it.create(this.ctx, generator); - for (TypeName typeName : it.supportedAnnotations()) { - typeToExtensions.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(extension); - } - Collection packages = it.supportedAnnotationPackages(); - if (!packages.isEmpty()) { - extensionPredicates.put(extension, discoveryPredicate(packages)); - } + Set extensionAnnotations = it.supportedAnnotations(); + Set extensionPackages = it.supportedAnnotationPackages(); + Set extensionMetaAnnotations = it.supportedMetaAnnotations(); - return extension; - }) - .toList(); + supportedAnnotations.addAll(extensionAnnotations); + supportedMetaAnnotations.addAll(extensionMetaAnnotations); + supportedPackagePrefixes.addAll(extensionPackages); - // handle supported annotations and package prefixes - Set packagePrefixes = new HashSet<>(); - Set annotations = new HashSet<>(ctx.mapperSupportedAnnotations()); + Predicate annotationPredicate = discoveryPredicate(extensionAnnotations, + extensionPackages); - for (CodegenExtensionProvider extension : EXTENSIONS) { - annotations.addAll(extension.supportedAnnotations()); + return new ExtensionInfo(extension, + annotationPredicate, + extensionMetaAnnotations); + }) + .toList(); - ctx.mapperSupportedAnnotationPackages() - .stream() - .map(Codegen::toPackagePrefix) - .forEach(packagePrefixes::add); - } ctx.mapperSupportedAnnotationPackages() .stream() .map(Codegen::toPackagePrefix) - .forEach(packagePrefixes::add); + .forEach(supportedPackagePrefixes::add); - this.supportedAnnotations = Set.copyOf(annotations); - this.supportedPackagePrefixes = Set.copyOf(packagePrefixes); + this.supportedAnnotations = Set.copyOf(supportedAnnotations); + this.supportedPackagePrefixes = Set.copyOf(supportedPackagePrefixes); + this.supportedMetaAnnotations = Set.copyOf(supportedMetaAnnotations); } /** @@ -144,12 +141,11 @@ public void process(List allTypes) { // type info list will contain all mapped annotations, so this is the state we can do annotation processing on List annotatedTypes = annotatedTypes(allTypes); - for (CodegenExtension extension : extensions) { + for (var extension : extensions) { // and now for each extension, we discover types that contain annotations supported by that extension - // and create a new round context for each extension - - RoundContextImpl roundCtx = createRoundContext(annotatedTypes, extension); - extension.process(roundCtx); + // and create a new round context + RoundContextImpl roundCtx = createRoundContext(annotatedTypes, extension, toWrite); + extension.extension().process(roundCtx); toWrite.addAll(roundCtx.newTypes()); } @@ -163,9 +159,9 @@ public void processingOver() { List toWrite = new ArrayList<>(); // do processing over in each extension - for (CodegenExtension extension : extensions) { - RoundContextImpl roundCtx = createRoundContext(List.of(), extension); - extension.processingOver(roundCtx); + for (var extension : extensions) { + RoundContextImpl roundCtx = createRoundContext(List.of(), extension, toWrite); + extension.extension().processingOver(roundCtx); toWrite.addAll(roundCtx.newTypes()); } @@ -191,11 +187,25 @@ public Set supportedAnnotationPackagePrefixes() { return supportedPackagePrefixes; } - private static Predicate discoveryPredicate(Collection packages) { - List prefixes = packages.stream() + /** + * A set of annotation types that may annotate annotation types. + * + * @return set of meta annotations for annotations to be processed + */ + public Set supportedMetaAnnotations() { + return supportedMetaAnnotations; + } + + private static Predicate discoveryPredicate(Set extensionAnnotations, + Collection extensionPackages) { + List prefixes = extensionPackages.stream() .map(it -> it.endsWith(".*") ? it.substring(0, it.length() - 2) : it) .toList(); + return typeName -> { + if (extensionAnnotations.contains(typeName)) { + return true; + } String packageName = typeName.packageName(); for (String prefix : prefixes) { if (packageName.startsWith(prefix)) { @@ -236,45 +246,65 @@ private void writeNewTypes(List toWrite) { } } - private RoundContextImpl createRoundContext(List annotatedTypes, CodegenExtension extension) { - Set extAnnots = new HashSet<>(); - Map> extAnnotToType = new HashMap<>(); - Map extTypes = new HashMap<>(); + private RoundContextImpl createRoundContext(List annotatedTypes, + ExtensionInfo extension, + List newTypes) { + Set availableAnnotations = new HashSet<>(); + Map> annotationToTypes = new HashMap<>(); + Map processedTypes = new HashMap<>(); + Map> metaAnnotationToAnnotations = new HashMap<>(); + // now go through all available annotated types and make sure we only include the ones required by this extension for (TypeInfoAndAnnotations annotatedType : annotatedTypes) { - for (TypeName typeName : annotatedType.annotations()) { - boolean added = false; - List validExts = this.typeToExtensions.get(typeName); - if (validExts != null) { - for (CodegenExtension validExt : validExts) { - if (validExt == extension) { - extAnnots.add(typeName); - extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(annotatedType.typeInfo()); - extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); - added = true; - } - } - } - if (!added) { - Predicate predicate = this.extensionPredicates.get(extension); - if (predicate != null && predicate.test(typeName)) { - extAnnots.add(typeName); - extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(annotatedType.typeInfo()); - extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); - } + for (TypeName annotationType : annotatedType.annotations()) { + boolean metaAnnotated = metaAnnotations(extension, metaAnnotationToAnnotations, annotationType); + if (metaAnnotated || extension.supportedAnnotationsPredicate().test(annotationType)) { + availableAnnotations.add(annotationType); + processedTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo()); + annotationToTypes.computeIfAbsent(annotationType, k -> new ArrayList<>()) + .add(annotatedType.typeInfo()); + // annotation is meta-annotated with a supported meta-annotation, + // or we support the annotation type, or it is prefixed by the package prefix } } } return new RoundContextImpl( ctx, - Set.copyOf(extAnnots), - Map.copyOf(extAnnotToType), - List.copyOf(extTypes.values())); + newTypes, + Set.copyOf(availableAnnotations), + Map.copyOf(annotationToTypes), + Map.copyOf(metaAnnotationToAnnotations), + List.copyOf(processedTypes.values())); + } + + private boolean metaAnnotations(ExtensionInfo extension, + Map> metaAnnotationToAnnotations, + TypeName annotationType) { + Optional annotationInfo = ctx.typeInfo(annotationType); + if (annotationInfo.isEmpty()) { + return false; + } + TypeInfo annotationTypeInfo = annotationInfo.get(); + + boolean metaAnnotated = false; + for (TypeName metaAnnotation : extension.supportedMetaAnnotations()) { + for (Annotation anAnnotation : annotationTypeInfo.allAnnotations()) { + if (anAnnotation.typeName().equals(metaAnnotation)) { + metaAnnotated = true; + metaAnnotationToAnnotations.computeIfAbsent(metaAnnotation, k -> new HashSet<>()) + .add(annotationType); + } + } + } + return metaAnnotated; } private record TypeInfoAndAnnotations(TypeInfo typeInfo, Set annotations) { } + + private record ExtensionInfo(CodegenExtension extension, + Predicate supportedAnnotationsPredicate, + Set supportedMetaAnnotations) { + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java index 5a4b2c80579..0133d82af47 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java @@ -66,7 +66,8 @@ default Optional moduleName() { CodegenLogger logger(); /** - * Current code generation scope. Usually guessed from the environment, can be overridden using {@link CodegenOptions#CODEGEN_SCOPE} + * Current code generation scope. Usually guessed from the environment, can be overridden using + * {@link CodegenOptions#CODEGEN_SCOPE} * * @return scope */ @@ -80,10 +81,12 @@ default Optional moduleName() { CodegenOptions options(); /** - * Discover information about the provided type. + * Discover information about the provided type. This method only checks existing classes in the + * system, and ignored classes created as part of the current processing round. * * @param typeName type name to discover * @return discovered type information, or empty if the type cannot be discovered + * @see io.helidon.codegen.RoundContext#typeInfo(io.helidon.common.types.TypeName) */ Optional typeInfo(TypeName typeName); @@ -144,4 +147,13 @@ default Optional moduleName() { * @return set of supported options */ Set> supportedOptions(); + + /** + * Get the unique name for the element within the provided type. + * + * @param type type that owns the element + * @param element the element + * @return unique name for the element (will always start with the element name) + */ + String uniqueName(TypeInfo type, TypedElementInfo element); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java index 711644ca5a8..19856a969ca 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java @@ -16,8 +16,10 @@ package io.helidon.codegen; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -29,12 +31,17 @@ import io.helidon.codegen.spi.TypeMapper; import io.helidon.codegen.spi.TypeMapperProvider; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; /** * Base of codegen context implementation taking care of the common parts of the API. */ public abstract class CodegenContextBase implements CodegenContext { + // class -> method name -> element signature + private final Map> uniqueNames = new HashMap<>(); private final List elementMappers; private final List typeMappers; private final List annotationMappers; @@ -149,6 +156,13 @@ public CodegenOptions options() { return options; } + @Override + public String uniqueName(TypeInfo type, TypedElementInfo element) { + return uniqueNames.computeIfAbsent(type.typeName(), it -> new HashMap<>()) + .computeIfAbsent(element.elementName(), ElementSignatures::new) + .uniqueName(element.signature()); + } + private static void addSupported(CodegenProvider provider, Set> supportedOptions, Set supportedPackages, @@ -160,4 +174,23 @@ private static void addSupported(CodegenProvider provider, .map(it -> it.endsWith(".*") ? it : it + ".*") .forEach(supportedPackages::add); } + + private static class ElementSignatures { + private final Map names = new HashMap<>(); + private final String name; + + private ElementSignatures(String name) { + this.name = name; + } + + public String uniqueName(ElementSignature signature) { + int size = names.size(); + if (names.containsKey(signature)) { + return names.get(signature); + } + String nextName = size == 0 ? name : name + "_" + size; + names.put(signature, nextName); + return nextName; + } + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java index e2055d1e816..afaf4a72e4b 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java @@ -107,4 +107,9 @@ public Set mapperSupportedAnnotationPackages() { public Set> supportedOptions() { return delegate.supportedOptions(); } + + @Override + public String uniqueName(TypeInfo type, TypedElementInfo element) { + return delegate.uniqueName(type, element); + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java b/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java index 007f272f325..cfb5bfd1119 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java @@ -48,6 +48,16 @@ public static boolean isMethod(TypedElementInfo element) { return ElementKind.METHOD == element.kind(); } + /** + * Predicate for constructor element kind. + * + * @param element typed element info to test + * @return whether the element represents a constructor + */ + public static boolean isConstructor(TypedElementInfo element) { + return ElementKind.CONSTRUCTOR == element.kind(); + } + /** * Predicate for field element kind. * @@ -68,6 +78,16 @@ public static boolean isStatic(TypedElementInfo element) { return element.elementModifiers().contains(Modifier.STATIC); } + /** + * Predicate for abstract modifier. + * + * @param element typed element info to test + * @return whether the element has abstract modifier + */ + public static boolean isAbstract(TypedElementInfo element) { + return element.elementModifiers().contains(Modifier.ABSTRACT); + } + /** * Predicate for private modifier. * diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java index 1cf876ec26e..1533e55b7be 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java @@ -77,4 +77,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name); } + + @Override + public String toString() { + return name + "(" + defaultValue + ")"; + } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java index aadcecaf8c0..34ed3dcf3ff 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Optional; +import java.util.Set; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.common.types.TypeInfo; @@ -52,6 +53,17 @@ public interface RoundContext { */ Collection annotatedTypes(TypeName annotationType); + /** + * Annotation types present on wanted types, annotated with the specific "meta" annotation. + * + * @param metaAnnotation annotations annotated with the provided annotation + * @return annotation types + */ + default Collection annotatedAnnotations(TypeName metaAnnotation) { + // default implementation for backward compatibility reasons + return Set.of(); + } + /** * All elements annotated with a specific annotation. * @@ -81,9 +93,24 @@ public interface RoundContext { * annotations. * Whether another extension was already called depends on its {@link io.helidon.codegen.spi.CodegenExtensionProvider} * weight. + * This method will return top level class model builder if the type represents an inner class of it. * * @param type type of the generated type * @return class model of the new type if any */ Optional generatedType(TypeName type); + + /** + * Discover information about the provided type. + *

    + * In case the type was generated by this processing round (even in another extension), + * the type info will reflect the current state of the + * class model builder that was registered with + * {@link io.helidon.codegen.RoundContext#addGeneratedType(io.helidon.common.types.TypeName, + * io.helidon.codegen.classmodel.ClassModel.Builder, io.helidon.common.types.TypeName, Object...)}. + * + * @param typeName type name to discover + * @return discovered type information, or empty if the type cannot be discovered + */ + Optional typeInfo(TypeName typeName); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java index 1e765544a71..4169b1ea0ba 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.Set; +import io.helidon.codegen.classmodel.ClassBase; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; @@ -32,17 +33,23 @@ class RoundContextImpl implements RoundContext { private final Map newTypes = new HashMap<>(); private final Map> annotationToTypes; + private final Map> metaAnnotated; private final List types; private final CodegenContext ctx; + private final List newTypesFromPreviousExtensions; private final Collection annotations; RoundContextImpl(CodegenContext ctx, + List newTypes, Set annotations, Map> annotationToTypes, + Map> metaAnnotated, List types) { this.ctx = ctx; + this.newTypesFromPreviousExtensions = newTypes; this.annotations = annotations; this.annotationToTypes = annotationToTypes; + this.metaAnnotated = metaAnnotated; this.types = types; } @@ -95,6 +102,11 @@ public Collection annotatedTypes(TypeName annotationType) { return result; } + @Override + public Collection annotatedAnnotations(TypeName metaAnnotation) { + return Optional.ofNullable(metaAnnotated.get(metaAnnotation)).orElseGet(Set::of); + } + @Override public void addGeneratedType(TypeName type, ClassModel.Builder newClass, @@ -108,6 +120,37 @@ public Optional generatedType(TypeName type) { return Optional.ofNullable(newTypes.get(type)).map(ClassCode::classModel); } + @Override + public Optional typeInfo(TypeName typeName) { + var found = ctx.typeInfo(typeName); + if (found.isPresent()) { + return found; + } + + return generatedClass(typeName) + .map(it -> ClassModelFactory.create( + this, + typeName, + it)); + } + + private Optional generatedClass(TypeName typeName) { + for (ClassCode classCode : newTypes.values()) { + Optional inProgress = classCode.classModel().find(typeName); + if (inProgress.isPresent()) { + return inProgress; + } + } + for (ClassCode classCode : newTypesFromPreviousExtensions) { + Optional inProgress = classCode.classModel().find(typeName); + if (inProgress.isPresent()) { + return inProgress; + } + } + + return Optional.empty(); + } + Collection newTypes() { return newTypes.values(); } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java index 67cedf9f803..f110ecd820d 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java @@ -87,6 +87,11 @@ public int hashCode() { return Objects.hash(name); } + @Override + public String toString() { + return name + "(" + defaultValue + ")"; + } + private Set toSet(String[] strings) { return Stream.of(strings) .map(String::trim) diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java index 674997b5f6e..0734834bd8e 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java @@ -44,6 +44,8 @@ default Set> supportedOptions() { * Annotations that are supported. * * @return set of annotation types + * @see io.helidon.codegen.RoundContext#annotatedTypes(io.helidon.common.types.TypeName) + * @see io.helidon.codegen.RoundContext#annotatedElements(io.helidon.common.types.TypeName) */ default Set supportedAnnotations() { return Set.of(); @@ -57,4 +59,15 @@ default Set supportedAnnotations() { default Set supportedAnnotationPackages() { return Set.of(); } + + /** + * Inherited annotations that are supported. + * If an annotation is annotated with this "meta" annotation, it is considered supported. + * + * @return set of meta annotation types + * @see io.helidon.codegen.RoundContext#annotatedAnnotations(io.helidon.common.types.TypeName) + */ + default Set supportedMetaAnnotations() { + return Set.of(); + } } diff --git a/common/config/src/main/java/io/helidon/common/config/Config.java b/common/config/src/main/java/io/helidon/common/config/Config.java index 5089bcf0368..3773aa59d87 100644 --- a/common/config/src/main/java/io/helidon/common/config/Config.java +++ b/common/config/src/main/java/io/helidon/common/config/Config.java @@ -35,6 +35,17 @@ static Config empty() { return EmptyConfig.EMPTY; } + /** + * Create a new instance of configuration from the default configuration sources. + * In case there is no {@link io.helidon.common.config.spi.ConfigProvider} available, returns + * {@link #empty()}. + * + * @return a new configuration + */ + static Config create() { + return GlobalConfig.create(); + } + /** * Returns the fully-qualified key of the {@code Config} node. *

    diff --git a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java index fd661893a5c..387dfdc2a85 100644 --- a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java +++ b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -45,7 +45,7 @@ public final class GlobalConfig { return EMPTY; } // there is a valid provider, let's use its default configuration - return providers.get(0) + return providers.getFirst() .create(); }); private static final AtomicReference CONFIG = new AtomicReference<>(); @@ -100,4 +100,16 @@ public static Config config(Supplier config, boolean overwrite) { } return CONFIG.get(); } + + static Config create() { + List providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class)) + .asList(); + // no implementations available, use empty configuration + if (providers.isEmpty()) { + return EMPTY; + } + // there is a valid provider, let's use its default configuration + return providers.getFirst() + .create(); + } } diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java index 5a08fac818d..494ad5f7007 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java @@ -179,6 +179,17 @@ public List typeParameters() { return List.of(); } + @Override + public List lowerBounds() { + // not yet supported + return List.of(); + } + + @Override + public List upperBounds() { + return List.of(bound.genericTypeName()); + } + @Override public String toString() { if (bound == null) { diff --git a/common/types/README.md b/common/types/README.md index 1fb2e39a0a4..b83dbbed607 100644 --- a/common/types/README.md +++ b/common/types/README.md @@ -5,4 +5,94 @@ Language types abstraction used during annotation processing (and instead of ref As types are required for annotation processors, they cannot be generated using annotation processors for builder. To work around this cyclic dependency problem, there is a module `builder/tests/common-types` that contains the correct blueprints and static methods to generate the code required for this module. -If a change is needed, generate the code using that module, and copy all the types (blueprint, static methods, and generated classes) here. \ No newline at end of file +If a change is needed, generate the code using that module, and copy all the types (blueprint, static methods, and generated classes) here. + + +# TypeName + +TypeName represents a type (class, interface, record). +Its `equals` and `hashCode` methods ignore generics (i.e. `Supplier` and `Supplier` are equal and have +the same hashCode - type erasure like behavior). + +If there is a requirement to compare based on generic declaration, use `ResolvedType`. + +## Handling of generics + +Depending on how a type name is created, the generic information may be available: + +1. `TypeName.create(SomeType.class)` - contains "raw" information - package name, class name +2. `TypeName.create(Type)` - when created from a `io.helidon.common.GenericType`, or `java.lang.reflect.ParameterizedType`, the type will contain type arguments (i.e. for `GenericType>` there will be a type `List` with type argument `String`) +3. Through codegen factories (annotation processing, classpath scanning, reflection) - see below + +TypeName is created for: + +1. Type declaration (`class MyClass...` - regardless of generics) - raw type name, accessible through `TypeInfo.rawType()`, or `TypeInfo.typeName()` if the type info was created for a raw type +2. Type declaration (`class MyClass`) - with all declared type parameters, accessible through `TypeInfo.declaredType()` +3. A type usage (`implements Supplier`) for the example above - with all type parameter information, accessible through `TypeInfo.typeName()` on type info of superclass or implemented interface +4. Wildcard usage (`List`) in parameter arguments + +Raw type: +```yaml +package: "com.example" +class-name: "MyClass" +``` + +Declared type (`MyClass`) : +```yaml +package: "com.example" +class-name: "MyClass" +type-parameters: # list of type names + - class-name: "X" + generic: true + upper-bounds: # list of type names - if not present, `Object` is expected (for ? extends X) + - class-name: "CharSequence" + - class-name: "Serializable" + lower-bounds: # list of type names - if not present, no lower bounds (for ? super X) + +``` +Type usage (`implements Supplier`): +```yaml +package: "java.util.function" +class-name: "Supplier" +type-parameters: # list of type names + - class-name: "X" + generic: true + upper-bounds: + - class-name: "CharSequence" + - class-name: "Serializable" +``` + +Type usage (`implements Supplier`): +```yaml +package: "java.util.function" +class-name: "Supplier" +type-parameters: # list of type names + - class-name: "CharSequence" +``` + +Wildcard usage (`List`): +```yaml +package: "java.util" +class-name: "List" +type-parameters: # list of type names + - class-name: "CharSequence" + package-name: "java.lang" + generic: true + wildcard: true + upper-bounds: + - class-name: "CharSequence" +``` + + +Wildcard usage (`List`): +```yaml +package: "java.util" +class-name: "List" +type-parameters: # list of type names + - class-name: "?" + generic: true + wildcard: true + lower-bounds: + - class-name: "String" +``` + diff --git a/common/types/src/main/java/io/helidon/common/types/ResolvedType.java b/common/types/src/main/java/io/helidon/common/types/ResolvedType.java new file mode 100644 index 00000000000..f78594ff1f0 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ResolvedType.java @@ -0,0 +1,74 @@ +/* + * 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.common.types; + +import java.lang.reflect.Type; + +/** + * A wrapper for {@link io.helidon.common.types.TypeName} that uses the resolved name for equals and hashCode. + * This allows us to collect interfaces including type arguments. + * + * @see TypeName#resolvedName() + */ +public interface ResolvedType { + /** + * Create a type name from a type (such as class). + * + * @param type the type + * @return type name for the provided type + */ + static ResolvedType create(Type type) { + return new ResolvedTypeImpl(TypeName.create(type)); + } + + /** + * Creates a type name from a fully qualified class name. + * + * @param typeName the FQN of the class type + * @return the TypeName for the provided type name + */ + static ResolvedType create(String typeName) { + return new ResolvedTypeImpl(TypeName.create(typeName)); + } + + /** + * Create a type name from a type name. + * + * @param typeName the type + * @return type name for the provided type + */ + static ResolvedType create(TypeName typeName) { + if (typeName instanceof ResolvedType rt) { + return rt; + } + return new ResolvedTypeImpl(typeName); + } + + /** + * Provides the underlying type name that backs this resolved type. + * + * @return the type name this resolved type represents + */ + TypeName type(); + + /** + * The resolved name including all type arguments. + * + * @return fully qualified class name with all type arguments + */ + String resolvedName(); +} diff --git a/common/types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java b/common/types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java new file mode 100644 index 00000000000..eddfb6a7d0d --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java @@ -0,0 +1,77 @@ +/* + * 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.common.types; + +class ResolvedTypeImpl implements ResolvedType, Comparable { + private final TypeName typeName; + private final String resolvedName; + private final boolean noTypes; + + ResolvedTypeImpl(TypeName typeName) { + this.typeName = typeName; + this.resolvedName = typeName.resolvedName(); + this.noTypes = typeName.typeArguments().isEmpty(); + } + + @Override + public TypeName type() { + return typeName; + } + + @Override + public String resolvedName() { + return resolvedName; + } + + @Override + public int hashCode() { + return noTypes ? typeName.hashCode() : resolvedName.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ResolvedType other)) { + return false; + } + if (other instanceof ResolvedTypeImpl rti) { + return resolvedName.equals(rti.resolvedName); + } + return other.type().resolvedName().equals(resolvedName); + } + + @Override + public int compareTo(ResolvedType o) { + int diff = resolvedName.compareTo(o.type().resolvedName()); + if (diff != 0) { + // different name + return diff; + } + diff = Boolean.compare(typeName.primitive(), o.type().primitive()); + if (diff != 0) { + return diff; + } + return Boolean.compare(typeName.array(), o.type().array()); + } + + @Override + public String toString() { + return resolvedName; + } +} diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java index cc027638df8..eff40c41b6b 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java @@ -86,6 +86,8 @@ abstract class BuilderBase builder) { builder.typeName().ifPresent(this::typeName); + builder.rawType().ifPresent(this::rawType); + builder.declaredType().ifPresent(this::declaredType); builder.description().ifPresent(this::description); builder.typeKind().ifPresent(this::typeKind); builder.kind().ifPresent(this::kind); @@ -200,6 +206,9 @@ public BUILDER from(TypeInfo.BuilderBase builder) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @param typeName the type name * @return updated builder instance @@ -213,6 +222,9 @@ public BUILDER typeName(TypeName typeName) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @param consumer consumer of builder for * the type name @@ -229,6 +241,9 @@ public BUILDER typeName(Consumer consumer) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @param supplier supplier of * the type name @@ -241,6 +256,107 @@ public BUILDER typeName(Supplier supplier) { return self(); } + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *

      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @param rawType raw type of this type info + * @return updated builder instance + * @see #rawType() + */ + public BUILDER rawType(TypeName rawType) { + Objects.requireNonNull(rawType); + this.rawType = rawType; + return self(); + } + + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @param consumer consumer of builder for + * raw type of this type info + * @return updated builder instance + * @see #rawType() + */ + public BUILDER rawType(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.rawType(builder.build()); + return self(); + } + + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @param supplier supplier of + * raw type of this type info + * @return updated builder instance + * @see #rawType() + */ + public BUILDER rawType(Supplier supplier) { + Objects.requireNonNull(supplier); + this.rawType(supplier.get()); + return self(); + } + + /** + * The declared type name, including type parameters. + * + * @param declaredType type name with declared type parameters + * @return updated builder instance + * @see #declaredType() + */ + public BUILDER declaredType(TypeName declaredType) { + Objects.requireNonNull(declaredType); + this.declaredType = declaredType; + return self(); + } + + /** + * The declared type name, including type parameters. + * + * @param consumer consumer of builder for + * type name with declared type parameters + * @return updated builder instance + * @see #declaredType() + */ + public BUILDER declaredType(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.declaredType(builder.build()); + return self(); + } + + /** + * The declared type name, including type parameters. + * + * @param supplier supplier of + * type name with declared type parameters + * @return updated builder instance + * @see #declaredType() + */ + public BUILDER declaredType(Supplier supplier) { + Objects.requireNonNull(supplier); + this.declaredType(supplier.get()); + return self(); + } + /** * Clear existing value of this property. * @@ -947,6 +1063,9 @@ public BUILDER addInheritedAnnotation(Consumer consumer) { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @return the type name */ @@ -954,6 +1073,29 @@ public Optional typeName() { return Optional.ofNullable(typeName); } + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @return the raw type + */ + public Optional rawType() { + return Optional.ofNullable(rawType); + } + + /** + * The declared type name, including type parameters. + * + * @return the declared type + */ + public Optional declaredType() { + return Optional.ofNullable(declaredType); + } + /** * Description, such as javadoc, if available. * @@ -1140,6 +1282,8 @@ public List inheritedAnnotations() { public String toString() { return "TypeInfoBuilder{" + "typeName=" + typeName + "," + + "rawType=" + rawType + "," + + "declaredType=" + declaredType + "," + "kind=" + kind + "," + "elementInfo=" + elementInfo + "," + "superTypeInfo=" + superTypeInfo + "," @@ -1166,6 +1310,12 @@ protected void validatePrototype() { if (typeName == null) { collector.fatal(getClass(), "Property \"typeName\" is required, but not set"); } + if (rawType == null) { + collector.fatal(getClass(), "Property \"rawType\" must not be null, but not set"); + } + if (declaredType == null) { + collector.fatal(getClass(), "Property \"declaredType\" must not be null, but not set"); + } if (typeKind == null) { collector.fatal(getClass(), "Property \"typeKind\" is required, but not set"); } @@ -1253,6 +1403,8 @@ protected static class TypeInfoImpl implements TypeInfo { private final Set elementModifiers; private final Set modifiers; private final String typeKind; + private final TypeName declaredType; + private final TypeName rawType; private final TypeName typeName; /** @@ -1262,6 +1414,8 @@ protected static class TypeInfoImpl implements TypeInfo { */ protected TypeInfoImpl(TypeInfo.BuilderBase builder) { this.typeName = builder.typeName().get(); + this.rawType = builder.rawType().get(); + this.declaredType = builder.declaredType().get(); this.description = builder.description(); this.typeKind = builder.typeKind().get(); this.kind = builder.kind().get(); @@ -1286,6 +1440,16 @@ public TypeName typeName() { return typeName; } + @Override + public TypeName rawType() { + return rawType; + } + + @Override + public TypeName declaredType() { + return declaredType; + } + @Override public Optional description() { return description; @@ -1370,6 +1534,8 @@ public List inheritedAnnotations() { public String toString() { return "TypeInfo{" + "typeName=" + typeName + "," + + "rawType=" + rawType + "," + + "declaredType=" + declaredType + "," + "kind=" + kind + "," + "elementInfo=" + elementInfo + "," + "superTypeInfo=" + superTypeInfo + "," @@ -1390,6 +1556,8 @@ public boolean equals(Object o) { return false; } return Objects.equals(typeName, other.typeName()) + && Objects.equals(rawType, other.rawType()) + && Objects.equals(declaredType, other.declaredType()) && Objects.equals(kind, other.kind()) && Objects.equals(elementInfo, other.elementInfo()) && Objects.equals(superTypeInfo, other.superTypeInfo()) @@ -1403,6 +1571,8 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(typeName, + rawType, + declaredType, kind, elementInfo, superTypeInfo, diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 704c34d139c..e744bb97bae 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -35,12 +35,34 @@ interface TypeInfoBlueprint extends Annotated { /** * The type name. + * This type name represents the type usage of this type + * (obtained from {@link TypeInfo#superTypeInfo()} or {@link TypeInfo#interfaceTypeInfo()}). + * In case this is a type info created from {@link io.helidon.common.types.TypeName}, this will be the type name returned. * * @return the type name */ @Option.Required TypeName typeName(); + /** + * The raw type name. This is a unique identification of a type, containing ONLY: + *
      + *
    • {@link TypeName#packageName()}
    • + *
    • {@link io.helidon.common.types.TypeName#className()}
    • + *
    • if relevant: {@link io.helidon.common.types.TypeName#enclosingNames()}
    • + *
    + * + * @return raw type of this type info + */ + TypeName rawType(); + + /** + * The declared type name, including type parameters. + * + * @return type name with declared type parameters + */ + TypeName declaredType(); + /** * Description, such as javadoc, if available. * diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java index a054c002053..952cd31cc0e 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * 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. @@ -68,6 +68,18 @@ public void decorate(TypeInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); + + // new methods, simplify for tests + if (target.rawType().isEmpty()) { + target.typeName() + .map(TypeName::genericTypeName) + .ifPresent(target::rawType); + } + if (target.declaredType().isEmpty()) { + // this may not be correct, but is correct for all types that do not have any declaration of generics + // so it simplifies a lot of use cases + target.rawType().ifPresent(target::declaredType); + } } } } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeName.java b/common/types/src/main/java/io/helidon/common/types/TypeName.java index daed5cb816a..a37c0e2cca6 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeName.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeName.java @@ -125,14 +125,18 @@ static TypeName createFromGenericDeclaration(String genericAliasTypeName) { abstract class BuilderBase, PROTOTYPE extends TypeName> implements Prototype.Builder { + private final List lowerBounds = new ArrayList<>(); private final List typeArguments = new ArrayList<>(); + private final List upperBounds = new ArrayList<>(); private final List enclosingNames = new ArrayList<>(); private final List typeParameters = new ArrayList<>(); private boolean array = false; private boolean generic = false; private boolean isEnclosingNamesMutated; + private boolean isLowerBoundsMutated; private boolean isTypeArgumentsMutated; private boolean isTypeParametersMutated; + private boolean isUpperBoundsMutated; private boolean primitive = false; private boolean wildcard = false; private String className; @@ -169,6 +173,14 @@ public BUILDER from(TypeName prototype) { typeParameters.clear(); } addTypeParameters(prototype.typeParameters()); + if (!isLowerBoundsMutated) { + lowerBounds.clear(); + } + addLowerBounds(prototype.lowerBounds()); + if (!isUpperBoundsMutated) { + upperBounds.clear(); + } + addUpperBounds(prototype.upperBounds()); return self(); } @@ -209,13 +221,30 @@ public BUILDER from(TypeName.BuilderBase builder) { typeParameters.clear(); addTypeParameters(builder.typeParameters); } + if (isLowerBoundsMutated) { + if (builder.isLowerBoundsMutated) { + addLowerBounds(builder.lowerBounds); + } + } else { + lowerBounds.clear(); + addLowerBounds(builder.lowerBounds); + } + if (isUpperBoundsMutated) { + if (builder.isUpperBoundsMutated) { + addUpperBounds(builder.upperBounds); + } + } else { + upperBounds.clear(); + addUpperBounds(builder.upperBounds); + } return self(); } /** * Update builder from the provided type. * - * @param type type to get information (package name, class name, primitive, array) + * @param type type to get information (package name, class name, primitive, array), can only be a class or a + * {@link io.helidon.common.GenericType} * @return updated builder instance */ public BUILDER type(Type type) { @@ -446,8 +475,10 @@ public BUILDER addTypeParameters(List typeParameters) { * * @param typeParameter type parameter names as declared on this type, or names that represent the {@link #typeArguments()} * @return updated builder instance + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information * @see #typeParameters() */ + @Deprecated(since = "4.2.0", forRemoval = true) public BUILDER addTypeParameter(String typeParameter) { Objects.requireNonNull(typeParameter); this.typeParameters.add(typeParameter); @@ -455,6 +486,152 @@ public BUILDER addTypeParameter(String typeParameter) { return self(); } + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param lowerBounds list of lower bounds of this type + * @return updated builder instance + * @see #lowerBounds() + */ + public BUILDER lowerBounds(List lowerBounds) { + Objects.requireNonNull(lowerBounds); + isLowerBoundsMutated = true; + this.lowerBounds.clear(); + this.lowerBounds.addAll(lowerBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param lowerBounds list of lower bounds of this type + * @return updated builder instance + * @see #lowerBounds() + */ + public BUILDER addLowerBounds(List lowerBounds) { + Objects.requireNonNull(lowerBounds); + isLowerBoundsMutated = true; + this.lowerBounds.addAll(lowerBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param lowerBound list of lower bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #lowerBounds() + */ + public BUILDER addLowerBound(TypeName lowerBound) { + Objects.requireNonNull(lowerBound); + this.lowerBounds.add(lowerBound); + isLowerBoundsMutated = true; + return self(); + } + + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param consumer list of lower bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #lowerBounds() + * @see #lowerBounds() + */ + public BUILDER addLowerBound(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.lowerBounds.add(builder.build()); + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param upperBounds list of upper bounds of this type + * @return updated builder instance + * @see #upperBounds() + */ + public BUILDER upperBounds(List upperBounds) { + Objects.requireNonNull(upperBounds); + isUpperBoundsMutated = true; + this.upperBounds.clear(); + this.upperBounds.addAll(upperBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param upperBounds list of upper bounds of this type + * @return updated builder instance + * @see #upperBounds() + */ + public BUILDER addUpperBounds(List upperBounds) { + Objects.requireNonNull(upperBounds); + isUpperBoundsMutated = true; + this.upperBounds.addAll(upperBounds); + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param upperBound list of upper bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #upperBounds() + */ + public BUILDER addUpperBound(TypeName upperBound) { + Objects.requireNonNull(upperBound); + this.upperBounds.add(upperBound); + isUpperBoundsMutated = true; + return self(); + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @param consumer list of upper bounds of this type + * @return updated builder instance + * @see io.helidon.common.types.TypeName#generic() + * @see #upperBounds() + * @see #upperBounds() + */ + public BUILDER addUpperBound(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.upperBounds.add(builder.build()); + return self(); + } + /** * Functions the same as {@link Class#getPackageName()}. * @@ -537,15 +714,48 @@ public List typeArguments() { * if {@link #typeArguments()} exist, this list MUST exist and have the same size and order (it maps the name to the type). * * @return the type parameters + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information */ + @Deprecated(since = "4.2.0", forRemoval = true) public List typeParameters() { return typeParameters; } + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return the lower bounds + * @see io.helidon.common.types.TypeName#generic() + * @see #lowerBounds() + * @see #lowerBounds() + */ + public List lowerBounds() { + return lowerBounds; + } + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return the upper bounds + * @see io.helidon.common.types.TypeName#generic() + * @see #upperBounds() + * @see #upperBounds() + */ + public List upperBounds() { + return upperBounds; + } + /** * Handles providers and decorators. */ protected void preBuildPrototype() { + new TypeNameSupport.Decorator().decorate(this); } /** @@ -568,7 +778,9 @@ protected static class TypeNameImpl implements TypeName { private final boolean generic; private final boolean primitive; private final boolean wildcard; + private final List lowerBounds; private final List typeArguments; + private final List upperBounds; private final List enclosingNames; private final List typeParameters; private final String className; @@ -589,6 +801,8 @@ protected TypeNameImpl(TypeName.BuilderBase builder) { this.wildcard = builder.wildcard(); this.typeArguments = List.copyOf(builder.typeArguments()); this.typeParameters = List.copyOf(builder.typeParameters()); + this.lowerBounds = List.copyOf(builder.lowerBounds()); + this.upperBounds = List.copyOf(builder.upperBounds()); } @Override @@ -671,6 +885,16 @@ public List typeParameters() { return typeParameters; } + @Override + public List lowerBounds() { + return lowerBounds; + } + + @Override + public List upperBounds() { + return upperBounds; + } + @Override public boolean equals(Object o) { if (o == this) { diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java index 1d9d5fa7118..d712863b5b4 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java @@ -44,7 +44,7 @@ *

  • {@link #declaredName()} and {@link #resolvedName()}.
  • * */ -@Prototype.Blueprint +@Prototype.Blueprint(decorator = TypeNameSupport.Decorator.class) @Prototype.CustomMethods(TypeNameSupport.class) @Prototype.Implement("java.lang.Comparable") interface TypeNameBlueprint { @@ -137,11 +137,39 @@ default String classNameWithEnclosingNames() { * if {@link #typeArguments()} exist, this list MUST exist and have the same size and order (it maps the name to the type). * * @return type parameter names as declared on this type, or names that represent the {@link #typeArguments()} + * @deprecated the {@link io.helidon.common.types.TypeName#typeArguments()} will contain all required information */ @Option.Singular @Option.Redundant + @Deprecated(forRemoval = true, since = "4.2.0") List typeParameters(); + /** + * Generic types that provide keyword {@code extends} will have a lower bound defined. + * Each lower bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of lower bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List lowerBounds(); + + /** + * Generic types that provide keyword {@code super} will have an upper bound defined. + * Upper bound may be a real type, or another generic type. + *

    + * This list may only have value if this is a generic type. + * + * @return list of upper bounds of this type + * @see io.helidon.common.types.TypeName#generic() + */ + @Option.Singular + @Option.Redundant + List upperBounds(); + /** * Indicates whether this type is a {@code java.util.List}. * diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java b/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java index e8a9d38809b..7c87d4eaced 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.LinkedList; @@ -23,9 +24,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; final class TypeNameSupport { private static final TypeName PRIMITIVE_BOOLEAN = TypeName.create(boolean.class); @@ -141,59 +144,57 @@ static String fqName(TypeName instance) { @Prototype.PrototypeMethod @Prototype.Annotated("java.lang.Override") // defined on blueprint static String resolvedName(TypeName instance) { - String name = calcName(instance, "."); - boolean isObject = Object.class.getName().equals(name) || "?".equals(name); - StringBuilder nameBuilder = (isObject) - ? new StringBuilder(instance.wildcard() ? "?" : name) - : new StringBuilder(instance.wildcard() ? "? extends " + name : name); - - if (!instance.typeArguments().isEmpty()) { - nameBuilder.append("<"); - int i = 0; - for (TypeName param : instance.typeArguments()) { - if (i > 0) { - nameBuilder.append(", "); - } - nameBuilder.append(param.resolvedName()); - i++; - } - nameBuilder.append(">"); + if (instance.generic() || instance.wildcard()) { + return resolveGenericName(instance); } - - if (instance.array()) { - nameBuilder.append("[]"); - } - - return nameBuilder.toString(); + return resolveClassName(instance); } /** * Update builder from the provided type. * * @param builder builder to update - * @param type type to get information (package name, class name, primitive, array) + * @param type type to get information (package name, class name, primitive, array) */ @Prototype.BuilderMethod static void type(TypeName.BuilderBase builder, Type type) { Objects.requireNonNull(type); if (type instanceof Class classType) { - Class componentType = classType.isArray() ? classType.getComponentType() : classType; - builder.packageName(componentType.getPackageName()); - builder.className(componentType.getSimpleName()); - builder.primitive(componentType.isPrimitive()); - builder.array(classType.isArray()); - - Class enclosingClass = classType.getEnclosingClass(); - LinkedList enclosingTypes = new LinkedList<>(); - while (enclosingClass != null) { - enclosingTypes.addFirst(enclosingClass.getSimpleName()); - enclosingClass = enclosingClass.getEnclosingClass(); + updateFromClass(builder, classType); + return; + } + Type reflectGenericType = type; + + if (type instanceof GenericType gt) { + if (gt.isClass()) { + // simple case - just a class + updateFromClass(builder, gt.rawType()); + return; + } else { + // complex case - has generic type arguments + reflectGenericType = gt.type(); } - builder.enclosingNames(enclosingTypes); - } else { - // todo - throw new IllegalArgumentException("Currently we only support class as a parameter, but got: " + type); } + + // translate the generic type into type name + if (reflectGenericType instanceof ParameterizedType pt) { + Type raw = pt.getRawType(); + if (raw instanceof Class theClass) { + updateFromClass(builder, theClass); + } else { + throw new IllegalArgumentException("Raw type of a ParameterizedType is not a class: " + raw.getClass().getName() + + ", for " + pt.getTypeName()); + } + + Type[] actualTypeArguments = pt.getActualTypeArguments(); + for (Type actualTypeArgument : actualTypeArguments) { + builder.addTypeArgument(TypeName.create(actualTypeArgument)); + } + return; + } + + throw new IllegalArgumentException("We can only create a type from a class, GenericType, or a ParameterizedType," + + " but got: " + reflectGenericType.getClass().getName()); } /** @@ -309,6 +310,71 @@ static TypeName createFromGenericDeclaration(String genericAliasTypeName) { .build(); } + private static String resolveGenericName(TypeName instance) { + // ?, ? super Something; ? extends Something + String prefix = instance.wildcard() ? "?" : instance.className(); + if (instance.upperBounds().isEmpty() && instance.lowerBounds().isEmpty()) { + return prefix; + } + if (instance.lowerBounds().isEmpty()) { + return prefix + " extends " + instance.upperBounds() + .stream() + .map(it -> { + if (it.generic()) { + return it.wildcard() ? "?" : it.className(); + } + return it.resolvedName(); + }) + .collect(Collectors.joining(" & ")); + } + TypeName lowerBound = instance.lowerBounds().getFirst(); + if (lowerBound.generic()) { + return prefix + " super " + (lowerBound.wildcard() ? "?" : lowerBound.className()); + } + return prefix + " super " + lowerBound.resolvedName(); + + } + + private static String resolveClassName(TypeName instance) { + String name = calcName(instance, "."); + StringBuilder nameBuilder = new StringBuilder(name); + + if (!instance.typeArguments().isEmpty()) { + nameBuilder.append("<"); + int i = 0; + for (TypeName param : instance.typeArguments()) { + if (i > 0) { + nameBuilder.append(", "); + } + nameBuilder.append(param.resolvedName()); + i++; + } + nameBuilder.append(">"); + } + + if (instance.array()) { + nameBuilder.append("[]"); + } + + return nameBuilder.toString(); + } + + private static void updateFromClass(TypeName.BuilderBase builder, Class classType) { + Class componentType = classType.isArray() ? classType.getComponentType() : classType; + builder.packageName(componentType.getPackageName()); + builder.className(componentType.getSimpleName()); + builder.primitive(componentType.isPrimitive()); + builder.array(classType.isArray()); + + Class enclosingClass = classType.getEnclosingClass(); + LinkedList enclosingTypes = new LinkedList<>(); + while (enclosingClass != null) { + enclosingTypes.addFirst(enclosingClass.getSimpleName()); + enclosingClass = enclosingClass.getEnclosingClass(); + } + builder.enclosingNames(enclosingTypes); + } + private static String calcName(TypeName instance, String typeSeparator) { String className; if (instance.enclosingNames().isEmpty()) { @@ -320,4 +386,38 @@ private static String calcName(TypeName instance, String typeSeparator) { return (instance.primitive() || instance.packageName().isEmpty()) ? className : instance.packageName() + "." + className; } + + static class Decorator implements Prototype.BuilderDecorator> { + @Override + public void decorate(TypeName.BuilderBase target) { + fixWildcards(target); + } + + private void fixWildcards(TypeName.BuilderBase target) { + // handle wildcards correct + if (target.wildcard()) { + if (target.upperBounds().size() == 1 && target.lowerBounds().isEmpty()) { + // backward compatible for (? extends X) + TypeName upperBound = target.upperBounds().getFirst(); + target.className(upperBound.className()); + target.packageName(upperBound.packageName()); + target.enclosingNames(upperBound.enclosingNames()); + } + // wildcard set, if package + class name as well, set them as upper bounds + if (target.className().isPresent() + && !target.className().get().equals("?") + && target.upperBounds().isEmpty() + && target.lowerBounds().isEmpty()) { + TypeName upperBound = TypeName.builder() + .from(target) + .wildcard(false) + .build(); + if (!upperBound.equals(TypeNames.OBJECT)) { + target.addUpperBound(upperBound); + } + } + target.generic(true); + } + } + } } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNames.java b/common/types/src/main/java/io/helidon/common/types/TypeNames.java index f539e5849af..caf18dbf866 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNames.java @@ -171,6 +171,10 @@ public final class TypeNames { * Type name of the type name. */ public static final TypeName TYPE_NAME = TypeName.create(TypeName.class); + /** + * Type name of the resolved type name. + */ + public static final TypeName RESOLVED_TYPE_NAME = TypeName.create(ResolvedType.class); /** * Type name of typed element info. */ diff --git a/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java b/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java index d6e4490f905..0f09f8bbe47 100644 --- a/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java +++ b/common/types/src/test/java/io/helidon/common/types/TypeNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * 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. @@ -62,6 +62,26 @@ void testNested() { assertThat(name.classNameWithEnclosingNames(), is("TestType.NestedType.DoubleNestedType")); } + @Test + void testGenericInnerType() { + String resolved = + "io.helidon.service.inject.api.Injection.ScopeHandler"; + + TypeName typeName = TypeName.builder() + .packageName("io.helidon.service.inject.api") + .className("ScopeHandler") + .addEnclosingName("Injection") + .addTypeArgument(TypeName.create("io.helidon.examples.inject.CustomScopeExample.MyScope")) + .build(); + + assertThat(typeName.resolvedName(), is(resolved)); + assertThat(typeName.fqName(), is("io.helidon.service.inject.api.Injection.ScopeHandler")); + + ResolvedType rt = ResolvedType.create(typeName); + assertThat(rt.type().resolvedName(), is(resolved)); + assertThat(rt.type().fqName(), is("io.helidon.service.inject.api.Injection.ScopeHandler")); + } + @Test void testNestedEquality() { TypeName first = create(TestType.NestedType.class); diff --git a/config/config/src/main/java/io/helidon/config/ConfigProvider.java b/config/config/src/main/java/io/helidon/config/ConfigProvider.java index d6ed2e86620..c4b2912bd9d 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigProvider.java +++ b/config/config/src/main/java/io/helidon/config/ConfigProvider.java @@ -16,10 +16,13 @@ package io.helidon.config; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.helidon.common.config.Config; import io.helidon.common.config.ConfigException; @@ -48,13 +51,17 @@ class ConfigProvider implements Config { .config(metaConfig.get().metaConfiguration()) .update(it -> configSources.get() .forEach(it::addSource)) + .update(it -> defaultConfigSources(it, configParsers)) .disableParserServices() .update(it -> configParsers.get() .forEach(it::addParser)) .disableFilterServices() .update(it -> configFilters.get() .forEach(it::addFilter)) - .disableMapperServices() + //.disableMapperServices() + // cannot do this for now, removed ConfigMapperProvider from service loaded services, config does it on its + // own + // ObjectConfigMapper is before EnumMapper, and both are before essential and built-in .update(it -> configMappers.get() .forEach(it::addMapper)) .build(); @@ -72,12 +79,12 @@ public Config root() { } @Override - public Config get(String key) throws ConfigException { + public Config get(String key) throws io.helidon.common.config.ConfigException { return config.get(key); } @Override - public Config detach() throws ConfigException { + public Config detach() throws io.helidon.common.config.ConfigException { return config.detach(); } @@ -107,27 +114,30 @@ public boolean hasValue() { } @Override - public ConfigValue as(Class type) { + public io.helidon.common.config.ConfigValue as(Class type) { return config.as(type); } @Override - public ConfigValue map(Function mapper) { + public io.helidon.common.config.ConfigValue map(Function mapper) { return config.map(mapper); } @Override - public ConfigValue> asList(Class type) throws ConfigException { + public io.helidon.common.config.ConfigValue> asList(Class type) throws + io.helidon.common.config.ConfigException { return config.asList(type); } @Override - public ConfigValue> mapList(Function mapper) throws ConfigException { + public io.helidon.common.config.ConfigValue> mapList(Function mapper) throws + io.helidon.common.config.ConfigException { return config.mapList(mapper); } @Override - public ConfigValue> asNodeList() throws ConfigException { + public io.helidon.common.config.ConfigValue> asNodeList() throws + io.helidon.common.config.ConfigException { return config.asNodeList(); } @@ -135,4 +145,23 @@ public ConfigValue> asNodeList() throws ConfigExcepti public ConfigValue> asMap() throws ConfigException { return config.asMap(); } + + private void defaultConfigSources(io.helidon.config.Config.Builder configBuilder, + Supplier> configParsers) { + + Set supportedSuffixes = configParsers.get() + .stream() + .map(ConfigParser::supportedSuffixes) + .flatMap(List::stream) + .collect(Collectors.toSet()); + + // profile source(s) before defaults + MetaConfigFinder.profile() + .ifPresent(profile -> MetaConfigFinder.configSources(new ArrayList<>(supportedSuffixes), + profile)); + // default config source(s) + MetaConfigFinder.configSources(new ArrayList<>(supportedSuffixes)) + .forEach(configBuilder::addSource); + + } } diff --git a/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java b/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java index 746ccd5338a..f93bc5ea638 100644 --- a/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java +++ b/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * 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. @@ -40,24 +40,30 @@ * * These conversions are intended to maximize ease-of-use for authors of config sources so the values need not be * upper-cased nor punctuated with underscores rather than the more conventional (in config at least) hyphen. - *

    *

    * The only hardship this imposes is if a confusingly-designed enum has values which differ only in case and the * string in the config source does not exactly match one of the enum value names. In such cases * the mapper will be unable to choose which enum value matches an ambiguous string. A developer faced with this * problem can simply provide her own explicit config mapping for that enum, for instance as a function parameter to * {@code Config#as}. - *

    - * */ @Weight(EnumMapperProvider.WEIGHT) -class EnumMapperProvider implements ConfigMapperProvider { +public class EnumMapperProvider implements ConfigMapperProvider { /** * Priority with which the enum mapper provider is added to the collection of providers (user- and Helidon-provided). */ static final double WEIGHT = Weighted.DEFAULT_WEIGHT; + /** + * Required constructor for {@link java.util.ServiceLoader}. + */ + public EnumMapperProvider() { + /* + This is now a "proper" service, to make this available also when using ServiceRegistry + */ + } + @Override public Map, Function> mappers() { return Map.of(); diff --git a/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java b/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java index ee95b176da7..f2a75d9805c 100644 --- a/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java +++ b/config/config/src/main/java/io/helidon/config/MetaConfigFinder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 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. @@ -26,6 +26,8 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; @@ -94,13 +96,7 @@ static Optional findConfigSource(Function supp return findSource(supportedMediaType, cl, CONFIG_PREFIX, "config source", supportedSuffixes); } - private static Optional findMetaConfigSource(Function supportedMediaType, - List supportedSuffixes) { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - Optional source; - - // check if meta configuration is configured using system property - String metaConfigFile = System.getProperty(META_CONFIG_SYSTEM_PROPERTY); + static Optional profile() { // check name of the profile String profileName = System.getenv(CONFIG_PROFILE_ENVIRONMENT_VARIABLE); if (profileName == null) { @@ -109,6 +105,44 @@ private static Optional findMetaConfigSource(Function configSources(List supportedSuffixes, + String profileName) { + return supportedSuffixes.stream() + .flatMap(suffix -> configSources("application-" + profileName + "." + suffix)) + .collect(Collectors.toUnmodifiableList()); + } + + static List configSources(List supportedSuffixes) { + return supportedSuffixes.stream() + .flatMap(suffix -> configSources("application." + suffix)) + .collect(Collectors.toUnmodifiableList()); + } + + static Stream configSources(String fileName) { + // we look on file system and on classpath, file system is more important + return Stream.concat(findFile(fileName, "default config source") + .stream(), + findAllClasspath(fileName)); + } + + private static Stream findAllClasspath(String fileName) { + return ConfigSources.classpathAll(fileName) + .stream() + .map(UrlConfigSource.Builder::build); + } + + private static Optional findMetaConfigSource(Function supportedMediaType, + List supportedSuffixes) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Optional source; + + // check if meta configuration is configured using system property + String metaConfigFile = System.getProperty(META_CONFIG_SYSTEM_PROPERTY); + // check name of the profile + String profileName = profile().orElse(null); if (metaConfigFile != null && profileName != null) { // we have both profile name and meta configuration file defined diff --git a/config/config/src/main/java/module-info.java b/config/config/src/main/java/module-info.java index 03790f9abcf..87e4ad08678 100644 --- a/config/config/src/main/java/module-info.java +++ b/config/config/src/main/java/module-info.java @@ -51,6 +51,8 @@ with io.helidon.config.PropertiesConfigParser; provides io.helidon.common.config.spi.ConfigProvider with io.helidon.config.HelidonConfigProvider; + provides io.helidon.config.spi.ConfigMapperProvider + with io.helidon.config.EnumMapperProvider; // needed when running with modules - to make private methods accessible opens io.helidon.config to weld.core.impl, io.helidon.microprofile.cdi; diff --git a/docs/src/main/asciidoc/se/grpc/server.adoc b/docs/src/main/asciidoc/se/grpc/server.adoc index 4a17d3ad90a..f1c90a9b83b 100644 --- a/docs/src/main/asciidoc/se/grpc/server.adoc +++ b/docs/src/main/asciidoc/se/grpc/server.adoc @@ -82,7 +82,7 @@ to do is register your services: include::{sourcedir}/se/grpc/ServerSnippets.java[tag=snippet_1, indent=0] ---- -<1> Register `GreetService` instance. +<1> Register `GreetFeature` instance. <2> Register `EchoService` instance. <3> Register `MathService` instance. <4> Register a custom unary gRPC route diff --git a/docs/src/main/asciidoc/se/guides/config.adoc b/docs/src/main/asciidoc/se/guides/config.adoc index 5289f00f63b..ebf71b36ecb 100644 --- a/docs/src/main/asciidoc/se/guides/config.adoc +++ b/docs/src/main/asciidoc/se/guides/config.adoc @@ -527,7 +527,7 @@ methods as described in xref:../config/hierarchical-features.adoc[Hierarchical C === Accessing Config Using Keys or Navigation -The simplest way to access configuration data is using a key, as shown below in the `GreetService` class. The +The simplest way to access configuration data is using a key, as shown below in the `GreetFeature` class. The key can be composite as shown below: [source,java] diff --git a/docs/src/main/asciidoc/se/guides/dbclient.adoc b/docs/src/main/asciidoc/se/guides/dbclient.adoc index c472b1b4fa5..f21c88b52e1 100644 --- a/docs/src/main/asciidoc/se/guides/dbclient.adoc +++ b/docs/src/main/asciidoc/se/guides/dbclient.adoc @@ -386,7 +386,7 @@ The application is ready to be built and run. mvn package ---- -Note that the tests are passing as the `GreetService` process was not modified. For the purposes of this demonstration, +Note that the tests are passing as the `GreetFeature` process was not modified. For the purposes of this demonstration, we only added independent new content to the existing application. Make sure H2 is running and start the Helidon quickstart with this command: diff --git a/docs/src/main/asciidoc/se/guides/tracing.adoc b/docs/src/main/asciidoc/se/guides/tracing.adoc index 07ce0424ef5..68fec81137c 100644 --- a/docs/src/main/asciidoc/se/guides/tracing.adoc +++ b/docs/src/main/asciidoc/se/guides/tracing.adoc @@ -388,7 +388,7 @@ call it.
    ---- -Make the following changes to the `GreetService` class. +Make the following changes to the `GreetFeature` class. 1. Add a `WebClient` field. + diff --git a/etc/checkstyle-suppressions.xml b/etc/checkstyle-suppressions.xml index 4bd5e4208af..7d1ccf9e6d7 100644 --- a/etc/checkstyle-suppressions.xml +++ b/etc/checkstyle-suppressions.xml @@ -47,6 +47,10 @@ record here. + + + diff --git a/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java b/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java index 9386dde6199..07798341e36 100644 --- a/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java +++ b/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java @@ -56,7 +56,7 @@ public class HelidonReflectionFeature implements Feature { private static final String AT_ENTITY = "jakarta.persistence.Entity"; private static final String AT_MAPPED_SUPERCLASS = "jakarta.persistence.MappedSuperclass"; - private static final String REGISTRY_DESCRIPTOR = "io.helidon.service.registry.GeneratedService$Descriptor"; + private static final String REGISTRY_DESCRIPTOR = "io.helidon.service.registry.ServiceDescriptor"; private final NativeTrace tracer = new NativeTrace(); private NativeUtil util; diff --git a/service/README.md b/service/README.md index 4925031ea7c..00ee19e08c6 100644 --- a/service/README.md +++ b/service/README.md @@ -20,10 +20,7 @@ obtained from a `ServiceRegistryManager`. ## Declare a service Use `io.helidon.service.registry.Service.Provider` annotation on your service provider type (implementation of a contract). -Use `io.helidon.service.registry.Service.Contract` on your contract interface (if not annotated, such an interface would not be -considered a contract and will not be discoverable using the registry - configurable). -Use `io.helidon.service.registry.Service.ExternalContracts` on your service provider type to -add other types as contracts, even if not annotated with `Contract` (i.e. to support third party libraries). Alternatively, + Alternatively, `java.util.function.Supplier` can also be used in this scenario. Use `io.helidon.service.registry.Service.Descriptor` to create a hand-crafted service descriptor (see below "Behind the scenes") @@ -86,6 +83,63 @@ class MyService3 implements Supplier> { } ``` +## Annotation processor setup + +To use Service registry code generator, you need to add the Helidon annotation processor and the service registry code generator to your annotation processor path. + +For Maven: +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + + + + +``` + +Additional options can be configured to customize the behavior. For example the default approach is that all contracts +are auto-discovered. We can switch contract discovery to annotated only, in such a case the following annotations are available: +Use `io.helidon.service.registry.Service.Contract` on your contract interface (if not annotated, such an interface would not be +considered a contract and will not be discoverable using the registry - configurable). +Use `io.helidon.service.registry.Service.ExternalContracts` on your service provider type to +add other types as contracts, even if not annotated with `Contract` (i.e. to support third party libraries). + +There is also an option to exclude specific types from being contracts (such as `Closeable` could be excluded). + +To enable this (Maven): + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + + + + -Ahelidon.registry.autoAddNonContractInterfaces=false + + -Ahelidon.registry.nonContractTypes=java.io.Serializable,java.lang.AutoCloseable,java.io.Closeable + + + + +``` + ## Behind the scenes For each service, Helidon generates a service descriptor (`ServiceProvider__ServiceDescriptor`). @@ -133,8 +187,6 @@ The format is as follows (using `//` to comment sections, not part of the format ] ``` -Example: +# Helidon Service Inject -``` -core:io.helidon.ContractImpl__ServiceDescriptor:101.3:io.helidon.Contract1,io.helidon.Contract2 -``` +See details in [Helidon Inject](inject/README.md). \ No newline at end of file diff --git a/service/codegen/README.md b/service/codegen/README.md new file mode 100644 index 00000000000..81f627e24de --- /dev/null +++ b/service/codegen/README.md @@ -0,0 +1,16 @@ +Service Codegen +--------------- + +# Supported annotations + +| Annotation | Processed by | Description | +|-------------------|------------------|-------------------------------------| +| @Service.Provider | ServiceExtension | Generates a core service descriptor | + + +# Supported options +Options can be configured as annotation processor options, when running via annotation processor. + +| Option | Used by | Description | +|-------------------------------------------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `helidon.registry.autoAddNonContractInterfaces` | ServiceExtension | If set to `true`, all implemented interfaces and super types are considered a contract; by default, `@Service.Contract` or `@Service.ExternalContracts` must be in place | \ No newline at end of file diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/CoreDependency.java b/service/codegen/src/main/java/io/helidon/service/codegen/CoreDependency.java new file mode 100644 index 00000000000..00721fb76d0 --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/CoreDependency.java @@ -0,0 +1,174 @@ +/* + * 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.codegen; + +import io.helidon.codegen.CodegenException; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Core dependency can only be a constructor parameter. + */ +class CoreDependency { + private final TypedElementInfo constructor; + // type of the dependency (such as Contract; Optional; List) + private final TypeName typeName; + private final TypeName contract; + // name of the dependency (parameter name) + private final String name; + // name of the constant in service descriptor (PARAM_0 = Dependency.builder()...) + private final String dependencyConstant; + // name of the constant in service descriptor declaring the contract to "inject" (TYPE_0 = TypeName.create...) + private final String contractTypeConstant; + // name of the constant in service descriptor declaring the generic type of contract to "inject" + // (GTYPE_0 = new GenericType<...>) + private final String genericTypeConstant; + // full type name (including optional, supplier etc.) + private final String typeNameConstant; + + CoreDependency(TypedElementInfo constructor, + TypeName typeName, + TypeName contract, + String name, + String dependencyConstant, + String contractTypeConstant, + String genericTypeConstant, + String typeNameConstant) { + this.constructor = constructor; + this.typeName = typeName; + this.contract = contract; + this.name = name; + this.dependencyConstant = dependencyConstant; + this.contractTypeConstant = contractTypeConstant; + this.genericTypeConstant = genericTypeConstant; + this.typeNameConstant = typeNameConstant; + } + + static CoreDependency create(RegistryCodegenContext ctx, + TypedElementInfo constructor, + TypedElementInfo parameter, + CoreTypeConstants constants, + int index) { + + String dependencyConstant = "PARAM_" + index; + TypeName paramTypeName = parameter.typeName(); + TypeName contract = contract(parameter, paramTypeName, false, false, false); + + String contractConstant = constants.typeNameConstant(ResolvedType.create(contract)); + String dependencyGenericTypeConstant = constants.genericTypeConstant(ResolvedType.create(paramTypeName)); + String typeNameConstant = constants.typeNameConstant(ResolvedType.create(paramTypeName)); + String paramName = parameter.elementName(); + + return new CoreDependency( + constructor, + paramTypeName, + contract, + paramName, + dependencyConstant, + contractConstant, + dependencyGenericTypeConstant, + typeNameConstant); + } + + public String typeNameConstant() { + return typeNameConstant; + } + + TypeName typeName() { + return typeName; + } + + TypeName contract() { + return contract; + } + + String name() { + return name; + } + + String dependencyConstant() { + return dependencyConstant; + } + + String contractTypeConstant() { + return contractTypeConstant; + } + + String genericTypeConstant() { + return genericTypeConstant; + } + + TypedElementInfo constructor() { + return constructor; + } + + /* + get the contract expected for this dependency + Dependency may be: + - Optional + - List + - Supplier + - Supplier> + - Supplier> + */ + private static TypeName contract(TypedElementInfo parameter, + TypeName typeName, + boolean isList, + boolean isOptional, + boolean isSupplier) { + String allowed = "Dependency can be only declared as either of: " + + "Contract, Optional, List, Supplier, " + + "Supplier, Supplier>"; + + if (typeName.isOptional()) { + if (isList || isOptional) { + throw new CodegenException(allowed, + parameter.originatingElementValue()); + } + if (typeName.typeArguments().isEmpty()) { + throw new CodegenException("Dependency with Optional type must have a declared type argument.", + parameter.originatingElementValue()); + } + return contract(parameter, typeName.typeArguments().getFirst(), false, true, isSupplier); + } + if (typeName.isList()) { + if (isList || isOptional) { + throw new CodegenException(allowed, + parameter.originatingElementValue()); + } + if (typeName.typeArguments().isEmpty()) { + throw new CodegenException("Dependency with List type must have a declared type argument.", + parameter.originatingElementValue()); + } + return contract(parameter, typeName.typeArguments().getFirst(), true, false, isSupplier); + } + if (typeName.isSupplier()) { + if (isSupplier || isOptional || isList) { + throw new CodegenException(allowed, + parameter.originatingElementValue()); + } + if (typeName.typeArguments().isEmpty()) { + throw new CodegenException("Dependency with Supplier type must have a declared type argument.", + parameter.originatingElementValue()); + } + return contract(parameter, typeName.typeArguments().getFirst(), isList, isOptional, true); + } + + return typeName; + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/SuperType.java b/service/codegen/src/main/java/io/helidon/service/codegen/CoreFactoryType.java similarity index 51% rename from service/codegen/src/main/java/io/helidon/service/codegen/SuperType.java rename to service/codegen/src/main/java/io/helidon/service/codegen/CoreFactoryType.java index ac189297b54..49aa46f4ee3 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/SuperType.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/CoreFactoryType.java @@ -16,21 +16,16 @@ package io.helidon.service.codegen; -import io.helidon.common.types.TypeInfo; -import io.helidon.common.types.TypeName; - /** - * Definition of a super type (if any). - * - * @param hasSupertype whether there is a super type (declared through {@code extends SuperType}) - * @param superDescriptorType type name of the service descriptor of the super type - * @param superType type info of the super type + * Factory type of the service. */ -record SuperType(boolean hasSupertype, - TypeName superDescriptorType, - TypeInfo superType, - boolean superTypeIsCore) { - static SuperType noSuperType() { - return new SuperType(false, null, null, false); - } +enum CoreFactoryType { + /** + * Service implements contract. + */ + SERVICE, + /** + * Service implements a {@link java.util.function.Supplier}. + */ + SUPPLIER } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/CoreService.java b/service/codegen/src/main/java/io/helidon/service/codegen/CoreService.java new file mode 100644 index 00000000000..42dc6c7e13a --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/CoreService.java @@ -0,0 +1,278 @@ +/* + * 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.codegen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +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.common.types.TypedElementInfo; + +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER; +import static java.util.function.Predicate.not; + +/** + * A service (as declared and annotated by @Service.Provider). + *

    + * A service may be a {@link java.util.function.Supplier} of instance, or direct contract implementation. + */ +class CoreService { + private static final TypedElementInfo DEFAULT_CONSTRUCTOR = TypedElementInfo.builder() + .typeName(TypeNames.OBJECT) + .accessModifier(AccessModifier.PUBLIC) + .kind(ElementKind.CONSTRUCTOR) + .build(); + + // whether this is an abstract class or not + private final boolean isAbstract; + // If this is a factory or not + private final CoreFactoryType factoryType; + // The class name of the service + private final TypeName serviceType; + // The class name of the service descriptor to be generated + private final TypeName descriptorType; + // If this service extends another service + private final ServiceSuperType superType; + // Required constructor "injection points" of this service + private final List dependencies; + private final CoreTypeConstants constants; + // Contracts provided by this service + private final Set contracts; + private final Set factoryContracts; + + CoreService(boolean isAbstract, + CoreFactoryType factoryType, + TypeName serviceType, + TypeName descriptorType, + ServiceSuperType superType, + List dependencies, + CoreTypeConstants constants, + Set contracts, + Set factoryContracts) { + this.isAbstract = isAbstract; + this.factoryType = factoryType; + this.serviceType = serviceType; + this.descriptorType = descriptorType; + this.superType = superType; + this.dependencies = dependencies; + this.constants = constants; + this.contracts = contracts; + this.factoryContracts = factoryContracts; + } + + static CoreService create(RegistryCodegenContext ctx, + RegistryRoundContext roundContext, + TypeInfo serviceInfo, + Collection allServices) { + + TypeName serviceType = serviceInfo.typeName(); + TypeName descriptorType = ctx.descriptorType(serviceType); + + Set directContracts = new HashSet<>(); + Set providedContracts = new HashSet<>(); + CoreFactoryType factoryType = CoreFactoryType.SERVICE; + + ServiceContracts serviceContracts = roundContext.serviceContracts(serviceInfo); + + // now we know which contracts are OK to use, and we can check the service types and real contracts + // service is a factory only if it implements the interface directly; this is never inherited + List typeInfos = serviceInfo.interfaceTypeInfo(); + Map implementedInterfaceTypes = new HashMap<>(); + typeInfos.forEach(it -> implementedInterfaceTypes.put(it.typeName(), it)); + + var response = serviceContracts.analyseFactory(TypeNames.SUPPLIER); + if (response.valid()) { + factoryType = CoreFactoryType.SUPPLIER; + directContracts.add(ResolvedType.create(response.factoryType())); + providedContracts.addAll(response.providedContracts()); + implementedInterfaceTypes.remove(TypeNames.SUPPLIER); + } + + // add direct contracts + HashSet processedDirectContracts = new HashSet<>(); + implementedInterfaceTypes.forEach((type, typeInfo) -> { + serviceContracts.addContracts(directContracts, + processedDirectContracts, + typeInfo); + }); + + // if we are a factory, our direct contracts are a different set (as it is satisfied by the instance directly + // and not by the factory method) + Set factoryContracts = (factoryType == CoreFactoryType.SUPPLIER) + ? directContracts + : Set.of(); + // and provided contracts are the "real" contracts of the provider + Set contracts = (factoryType == CoreFactoryType.SUPPLIER) + ? providedContracts + : directContracts; + + DependencyResult dependencyResult = gatherDependencies(ctx, serviceInfo); + ServiceSuperType superType = superType(ctx, serviceInfo, allServices); + + // the service metadata must contain all contracts the service provides (both provider and provided) + return new CoreService(isAbstract(serviceInfo), + factoryType, + serviceType, + descriptorType, + superType, + dependencyResult.result(), + dependencyResult.constants(), + contracts, + factoryContracts); + } + + boolean isAbstract() { + return isAbstract; + } + + CoreFactoryType factoryType() { + return factoryType; + } + + TypeName serviceType() { + return serviceType; + } + + TypeName descriptorType() { + return descriptorType; + } + + ServiceSuperType superType() { + return superType; + } + + List dependencies() { + return dependencies; + } + + Set contracts() { + return contracts; + } + + Set factoryContracts() { + return factoryContracts; + } + + CoreTypeConstants constants() { + return constants; + } + + // find super type if it is also a service (or has a service descriptor) + private static ServiceSuperType superType(RegistryCodegenContext ctx, TypeInfo serviceInfo, Collection services) { + Optional maybeSuperType = serviceInfo.superTypeInfo(); + if (maybeSuperType.isEmpty()) { + // this class does not have a super type + return ServiceSuperType.create(); + } + + // check if the super type is part of current annotation processing + TypeInfo superType = maybeSuperType.get(); + TypeName expectedSuperDescriptor = ctx.descriptorType(superType.typeName()); + TypeName superTypeToExtend = TypeName.builder(expectedSuperDescriptor) + .addTypeArgument(TypeName.create("T")) + .build(); + boolean isCore = superType.hasAnnotation(SERVICE_ANNOTATION_PROVIDER); + if (!isCore) { + throw new CodegenException("Service annotated with @Service.Provider extends invalid supertype," + + " the super type must also be a @Service.Provider. Type: " + + serviceInfo.typeName().fqName() + ", super type: " + + superType.typeName().fqName(), + serviceInfo.originatingElementValue()); + } + + for (TypeInfo service : services) { + if (service.typeName().equals(superType.typeName())) { + return ServiceSuperType.create(service, "core", superTypeToExtend); + } + } + // if not found in current list, try checking existing types + return ctx.typeInfo(expectedSuperDescriptor) + .map(it -> ServiceSuperType.create(superType, "core", superTypeToExtend)) + .orElseGet(ServiceSuperType::create); + } + + private static DependencyResult gatherDependencies(RegistryCodegenContext ctx, TypeInfo serviceInfo) { + TypedElementInfo constructor = constructor(serviceInfo); + + // core services only support inversion of control for constructor parameters + AtomicInteger dependencyIndex = new AtomicInteger(); + + List result = new ArrayList<>(); + CoreTypeConstants constants = new CoreTypeConstants(); + + for (TypedElementInfo param : constructor.parameterArguments()) { + result.add(CoreDependency.create(ctx, + constructor, + param, + constants, + dependencyIndex.getAndIncrement())); + } + + return new DependencyResult(result, constants); + } + + private static TypedElementInfo constructor(TypeInfo serviceInfo) { + var allConstructors = serviceInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isConstructor) + .collect(Collectors.toUnmodifiableList()); + + if (allConstructors.isEmpty()) { + // no constructor, use default + return DEFAULT_CONSTRUCTOR; + } + + var nonPrivateConstructors = allConstructors.stream() + .filter(not(ElementInfoPredicates::isPrivate)) + .collect(Collectors.toUnmodifiableList()); + + if (nonPrivateConstructors.isEmpty()) { + throw new CodegenException("Service does not contain any non-private constructor", + serviceInfo.originatingElementValue()); + } + if (allConstructors.size() > 1) { + throw new CodegenException("Service contains more than one non-private constructor", + serviceInfo.originatingElementValue()); + } + + return allConstructors.getFirst(); + } + + private static boolean isAbstract(TypeInfo serviceInfo) { + return serviceInfo.elementModifiers().contains(Modifier.ABSTRACT) + && serviceInfo.kind() == ElementKind.CLASS; + } + + private record DependencyResult(List result, CoreTypeConstants constants) { + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/CoreTypeConstants.java b/service/codegen/src/main/java/io/helidon/service/codegen/CoreTypeConstants.java new file mode 100644 index 00000000000..17c68ee56be --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/CoreTypeConstants.java @@ -0,0 +1,62 @@ +/* + * 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.codegen; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; + +// each service descriptor has unique types generated (no duplication) for injected contracts (both +// the contract type, and the generic type) +class CoreTypeConstants { + private final AtomicInteger genericTypeCounter = new AtomicInteger(); + private final Map genericConstants = new LinkedHashMap<>(); + private final AtomicInteger typeNameCounter = new AtomicInteger(); + private final Map typeNameConstants = new LinkedHashMap<>(); + + String genericTypeConstant(ResolvedType type) { + return genericConstants.computeIfAbsent(type, it -> "GTYPE_" + genericTypeCounter.getAndIncrement()); + } + + String typeNameConstant(ResolvedType type) { + return typeNameConstants.computeIfAbsent(type, it -> "TYPE_" + typeNameCounter.getAndIncrement()); + } + + List genericConstants() { + List result = new ArrayList<>(); + + genericConstants.forEach((type, constantName) -> result.add(new Constant(type.type(), constantName))); + + return result; + } + + List typeNameConstants() { + List result = new ArrayList<>(); + + typeNameConstants.forEach((type, constantName) -> result.add(new Constant(type.type(), constantName))); + + return result; + } + + record Constant(TypeName type, String constantName) { + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java b/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java index d9e5e06f406..8c340ce7f04 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCode.java @@ -19,12 +19,12 @@ import java.util.Set; import io.helidon.codegen.ClassCode; -import io.helidon.common.types.TypeName; +import io.helidon.common.types.ResolvedType; /** * New service descriptor metadata with its class code. */ -interface DescriptorClassCode { +public interface DescriptorClassCode { /** * New source code information. * @@ -51,5 +51,12 @@ interface DescriptorClassCode { * * @return contracts of the service */ - Set contracts(); + Set contracts(); + + /** + * Contracts of the class if it is a factory. + * + * @return factory contracts + */ + Set factoryContracts(); } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCodeImpl.java b/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCodeImpl.java index fc78e2781a4..653876f247a 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCodeImpl.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/DescriptorClassCodeImpl.java @@ -19,10 +19,11 @@ import java.util.Set; import io.helidon.codegen.ClassCode; -import io.helidon.common.types.TypeName; +import io.helidon.common.types.ResolvedType; record DescriptorClassCodeImpl(ClassCode classCode, String registryType, double weight, - Set contracts) implements DescriptorClassCode { + Set contracts, + Set factoryContracts) implements DescriptorClassCode { } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/FactoryAnalysisImpl.java b/service/codegen/src/main/java/io/helidon/service/codegen/FactoryAnalysisImpl.java new file mode 100644 index 00000000000..a6cee178885 --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/FactoryAnalysisImpl.java @@ -0,0 +1,42 @@ +/* + * 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.codegen; + +import java.util.Set; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; + +record FactoryAnalysisImpl(boolean valid, + TypeName factoryType, + TypeName providedType, + TypeInfo providedTypeInfo, + Set providedContracts) + implements ServiceContracts.FactoryAnalysis { + + FactoryAnalysisImpl() { + this(false, null, null, null, null); + } + + FactoryAnalysisImpl(TypeName factoryType, + TypeName providedType, + TypeInfo providedTypeInfo, + Set providedContracts) { + this(true, factoryType, providedType, providedTypeInfo, providedContracts); + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java b/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java index f1b21454c94..651ef707ffd 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java @@ -19,17 +19,15 @@ import java.util.Collection; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import io.helidon.codegen.CodegenException; import io.helidon.codegen.CodegenUtil; import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.TypeHierarchy; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.codegen.classmodel.Javadoc; import io.helidon.codegen.classmodel.Method; @@ -40,21 +38,18 @@ import io.helidon.common.types.Annotation; import io.helidon.common.types.Annotations; import io.helidon.common.types.ElementKind; -import io.helidon.common.types.Modifier; +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.common.types.TypedElementInfo; -import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER; -import static java.util.function.Predicate.not; - /** * Generates a service descriptor. */ class GenerateServiceDescriptor { - static final TypeName SET_OF_TYPES = TypeName.builder(TypeNames.SET) - .addTypeArgument(TypeNames.TYPE_NAME) + static final TypeName SET_OF_RESOLVED_TYPES = TypeName.builder(TypeNames.SET) + .addTypeArgument(TypeNames.RESOLVED_TYPE_NAME) .build(); private static final TypeName LIST_OF_DEPENDENCIES = TypeName.builder(TypeNames.LIST) .addTypeArgument(ServiceCodegenTypes.SERVICE_DEPENDENCY) @@ -62,85 +57,75 @@ class GenerateServiceDescriptor { private static final TypeName DESCRIPTOR_TYPE = TypeName.builder(ServiceCodegenTypes.SERVICE_DESCRIPTOR) .addTypeArgument(TypeName.create("T")) .build(); - private static final TypedElementInfo DEFAULT_CONSTRUCTOR = TypedElementInfo.builder() - .typeName(TypeNames.OBJECT) - .accessModifier(AccessModifier.PUBLIC) - .kind(ElementKind.CONSTRUCTOR) - .build(); private static final TypeName ANY_GENERIC_TYPE = TypeName.builder(TypeNames.GENERIC_TYPE) .addTypeArgument(TypeName.create("?")) .build(); private final TypeName generator; private final RegistryCodegenContext ctx; + private final RegistryRoundContext roundCtx; private final Collection services; private final TypeInfo typeInfo; - private final boolean autoAddContracts; private GenerateServiceDescriptor(TypeName generator, RegistryCodegenContext ctx, + RegistryRoundContext roundCtx, Collection allServices, TypeInfo service) { this.generator = generator; this.ctx = ctx; + this.roundCtx = roundCtx; this.services = allServices; this.typeInfo = service; - this.autoAddContracts = ServiceOptions.AUTO_ADD_NON_CONTRACT_INTERFACES.value(ctx.options()); } /** * Generate a service descriptor for the provided service type info. * - * @param generator type of the generator responsible for this event - * @param ctx context of code generation - * @param allServices all services processed in this round of processing - * @param service service to create a descriptor for + * @param generator type of the generator responsible for this event + * @param ctx context of code generation + * @param roundContext current round context + * @param allServices all services processed in this round of processing + * @param service service to create a descriptor for * @return class model builder of the service descriptor */ static ClassModel.Builder generate(TypeName generator, RegistryCodegenContext ctx, + RegistryRoundContext roundContext, Collection allServices, TypeInfo service) { - return new GenerateServiceDescriptor(generator, ctx, allServices, service) + return new GenerateServiceDescriptor(generator, + ctx, + roundContext, + allServices, + service) .generate(); } - static List declareCtrParamsAndGetThem(Method.Builder method, List params) { - List constructorParams = params.stream() - .filter(it -> it.kind() == ElementKind.CONSTRUCTOR) - .toList(); - + static void declareConstructorParams(Method.Builder method, List params) { // for each parameter, obtain its value from context - for (ParamDefinition param : constructorParams) { - method.addContent(param.declaredType()) + for (CoreDependency param : params) { + method.addContent(param.typeName()) .addContent(" ") - .addContent(param.ipParamName()) - .addContent(" = ") - .update(it -> param.assignmentHandler().accept(it)) - .addContentLine(";"); + .addContent(param.name()) + .addContent(" = ctx__helidonRegistry.dependency(") + .addContent(param.dependencyConstant()) + .addContentLine(");"); } if (!params.isEmpty()) { method.addContentLine(""); } - return constructorParams; } private ClassModel.Builder generate() { - TypeName serviceType = typeInfo.typeName(); - if (typeInfo.kind() == ElementKind.INTERFACE) { - throw new CodegenException("We can only generated service descriptors for classes, interface was requested: ", - typeInfo.originatingElement().orElse(serviceType)); + throw new CodegenException("We can only generate service descriptors for classes, interface was requested: ", + typeInfo.originatingElementValue()); } - boolean isAbstractClass = typeInfo.elementModifiers().contains(Modifier.ABSTRACT) - && typeInfo.kind() == ElementKind.CLASS; - SuperType superType = superType(typeInfo, services); - - // this must result in generating a service descriptor file - TypeName descriptorType = ctx.descriptorType(serviceType); - - List params = params(typeInfo, constructor(typeInfo)); + CoreService service = CoreService.create(ctx, roundCtx, typeInfo, services); + TypeName serviceType = service.serviceType(); + TypeName descriptorType = service.descriptorType(); ClassModel.Builder classModel = ClassModel.builder() .copyright(CodegenUtil.copyright(generator, @@ -160,204 +145,61 @@ private ClassModel.Builder generate() { // we need to keep insertion order, as constants may depend on each other .sortStaticFields(false); - Map genericTypes = genericTypes(classModel, params); - Set contracts = new HashSet<>(); - Set collectedFullyQualifiedContracts = new HashSet<>(); - contracts(typeInfo, autoAddContracts, contracts, collectedFullyQualifiedContracts); - // declare the class - if (superType.hasSupertype()) { - classModel.superType(superType.superDescriptorType()); + if (service.superType().present()) { + classModel.superType(service.superType().descriptorType()); } else { classModel.addInterface(DESCRIPTOR_TYPE); } - // Fields singletonInstanceField(classModel, serviceType, descriptorType); - serviceTypeFields(classModel, serviceType, descriptorType); + + serviceTypeMethod(classModel, service); + descriptorTypeMethod(classModel, service); + contractsMethod(classModel, service); // public fields are last, so they do not intersect with private fields (it is not as nice to read) // they cannot be first, as they require some of the private fields - dependencyFields(classModel, typeInfo, genericTypes, params); + dependencyFields(classModel, service); // dependencies require IP IDs, so they really must be last - dependenciesField(classModel, params); + dependenciesMethod(classModel, service); // add protected constructor classModel.addConstructor(constructor -> constructor.description("Constructor with no side effects") .accessModifier(AccessModifier.PROTECTED)); // methods (some methods define fields as well) - serviceTypeMethod(classModel); - descriptorTypeMethod(classModel); - contractsMethod(classModel, contracts); - dependenciesMethod(classModel, params, superType); - isAbstractMethod(classModel, superType, isAbstractClass); - instantiateMethod(classModel, serviceType, params, isAbstractClass); - postConstructMethod(typeInfo, classModel, serviceType); - preDestroyMethod(typeInfo, classModel, serviceType); - weightMethod(typeInfo, classModel, superType); + isAbstractMethod(classModel, service); + instantiateMethod(classModel, service); + postConstructMethod(classModel, service); + preDestroyMethod(classModel, service); + weightMethod(classModel, service); // service type is an implicit contract - Set allContracts = new HashSet<>(contracts); - allContracts.add(serviceType); - - ctx.addDescriptor("core", - serviceType, - descriptorType, - classModel, - weight(typeInfo).orElse(Weighted.DEFAULT_WEIGHT), - allContracts, - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); - - return classModel; - } - - private SuperType superType(TypeInfo typeInfo, Collection services) { - // find super type if it is also a service (or has a service descriptor) - - // check if the super type is part of current annotation processing - Optional superTypeInfoOptional = typeInfo.superTypeInfo(); - if (superTypeInfoOptional.isEmpty()) { - return SuperType.noSuperType(); - } - TypeInfo superType = superTypeInfoOptional.get(); - TypeName expectedSuperDescriptor = ctx.descriptorType(superType.typeName()); - TypeName superTypeToExtend = TypeName.builder(expectedSuperDescriptor) - .addTypeArgument(TypeName.create("T")) - .build(); - boolean isCore = superType.hasAnnotation(SERVICE_ANNOTATION_PROVIDER); - if (!isCore) { - throw new CodegenException("Service annotated with @Service.Provider extends invalid supertype," - + " the super type must also be a @Service.Provider. Type: " - + typeInfo.typeName().fqName() + ", super type: " - + superType.typeName().fqName()); - } - for (TypeInfo service : services) { - if (service.typeName().equals(superType.typeName())) { - return new SuperType(true, superTypeToExtend, service, true); - } - } - // if not found in current list, try checking existing types - return ctx.typeInfo(expectedSuperDescriptor) - .map(it -> new SuperType(true, superTypeToExtend, superType, true)) - .orElseGet(SuperType::noSuperType); - } - - // there must be none, or one non-private constructor (actually there may be more, we just use the first) - private TypedElementInfo constructor(TypeInfo typeInfo) { - return typeInfo.elementInfo() - .stream() - .filter(it -> it.kind() == ElementKind.CONSTRUCTOR) - .filter(not(ElementInfoPredicates::isPrivate)) - .findFirst() - // or default constructor - .orElse(DEFAULT_CONSTRUCTOR); - } - - private List params( - TypeInfo service, - TypedElementInfo constructor) { - AtomicInteger paramCounter = new AtomicInteger(); - - return constructor.parameterArguments() - .stream() - .map(param -> { - String constantName = "PARAM_" + paramCounter.getAndIncrement(); - RegistryCodegenContext.Assignment assignment = translateParameter(param.typeName(), constantName); - return new ParamDefinition(constructor, - null, - param, - constantName, - param.typeName(), - assignment.usedType(), - assignment.codeGenerator(), - ElementKind.CONSTRUCTOR, - constructor.elementName(), - param.elementName(), - param.elementName(), - false, - param.annotations(), - Set.of(), - contract(service.typeName() - .fqName() + " Constructor parameter: " + param.elementName(), - assignment.usedType()), - constructor.accessModifier(), - ""); - }) - .toList(); - } - - private TypeName contract(String description, TypeName typeName) { - /* - get the contract expected for this dependency - IP may be: - - Optional - - List - - ServiceProvider - - Supplier - - Optional - - Optional - - List - - List - */ - - if (typeName.isOptional()) { - if (typeName.typeArguments().isEmpty()) { - throw new IllegalArgumentException("Dependency with Optional type must have a declared type argument: " - + description); - } - return contract(description, typeName.typeArguments().getFirst()); - } - if (typeName.isList()) { - if (typeName.typeArguments().isEmpty()) { - throw new IllegalArgumentException("Dependency with List type must have a declared type argument: " - + description); - } - return contract(description, typeName.typeArguments().getFirst()); - } - if (typeName.isSupplier()) { - if (typeName.typeArguments().isEmpty()) { - throw new IllegalArgumentException("Dependency with Supplier type must have a declared type argument: " - + description); - } - return contract(description, typeName.typeArguments().getFirst()); + Set serviceContracts = new HashSet<>(service.contracts()); + Set factoryContracts = new HashSet<>(service.factoryContracts()); + if (factoryContracts.isEmpty()) { + serviceContracts.add(ResolvedType.create(serviceType)); + } else { + factoryContracts.add(ResolvedType.create(serviceType)); } - return typeName; - } + roundCtx.addDescriptor("core", + serviceType, + descriptorType, + classModel, + weight(typeInfo).orElse(Weighted.DEFAULT_WEIGHT), + serviceContracts, + factoryContracts, + typeInfo.originatingElementValue()); - private Map genericTypes(ClassModel.Builder classModel, - List params) { - // we must use map by string (as type name is equal if the same class, not full generic declaration) - Map result = new LinkedHashMap<>(); - AtomicInteger counter = new AtomicInteger(); - - for (ParamDefinition param : params) { - result.computeIfAbsent(param.translatedType().resolvedName(), - type -> { - var response = - new GenericTypeDeclaration("TYPE_" + counter.getAndIncrement(), - param.declaredType()); - addTypeConstant(classModel, param.translatedType(), response); - return response; - }); - result.computeIfAbsent(param.contract().fqName(), - type -> { - var response = - new GenericTypeDeclaration("TYPE_" + counter.getAndIncrement(), - param.declaredType()); - addTypeConstant(classModel, param.contract(), response); - return response; - }); - } - - return result; + return classModel; } private void addTypeConstant(ClassModel.Builder classModel, TypeName typeName, - GenericTypeDeclaration generic) { + String constantName) { String stringType = typeName.resolvedName(); // constants for dependency parameter types (used by next section) classModel.addField(field -> field @@ -365,7 +207,7 @@ private void addTypeConstant(ClassModel.Builder classModel, .isStatic(true) .isFinal(true) .type(TypeNames.TYPE_NAME) - .name(generic.constantName()) + .name(constantName) .update(it -> { if (stringType.indexOf('.') < 0) { // there is no package, we must use class (if this is a generic type, we have a problem) @@ -377,12 +219,17 @@ private void addTypeConstant(ClassModel.Builder classModel, it.addContentCreate(typeName); } })); + } + + private void addGenericTypeConstant(ClassModel.Builder classModel, + TypeName typeName, + String constantName) { classModel.addField(field -> field .accessModifier(AccessModifier.PRIVATE) .isStatic(true) .isFinal(true) .type(ANY_GENERIC_TYPE) - .name("G" + generic.constantName()) + .name(constantName) .update(it -> { if (typeName.primitive()) { it.addContent(TypeNames.GENERIC_TYPE) @@ -419,7 +266,7 @@ private void contracts(TypeInfo typeInfo, if (typeName.isSupplier()) { // this may be the interface itself, and then it does not have a type argument if (!typeName.typeArguments().isEmpty()) { - // provider must have a type argument (and the type argument is an automatic contract + // factory must have a type argument (and the type argument is an automatic contract TypeName providedType = typeName.typeArguments().getFirst(); // and we support Supplier> as well if (!providedType.generic()) { @@ -461,8 +308,10 @@ private void contracts(TypeInfo typeInfo, } // add contracts from interfaces and types annotated as @Contract - typeInfo.findAnnotation(ServiceCodegenTypes.SERVICE_ANNOTATION_CONTRACT) - .ifPresent(it -> collectedContracts.add(typeInfo.typeName())); + if (Annotations.findFirst(ServiceCodegenTypes.SERVICE_ANNOTATION_CONTRACT, + TypeHierarchy.hierarchyAnnotations(ctx, typeInfo)).isPresent()) { + collectedContracts.add(typeInfo.typeName()); + } // add contracts from @ExternalContracts typeInfo.findAnnotation(ServiceCodegenTypes.SERVICE_ANNOTATION_EXTERNAL_CONTRACTS) @@ -508,112 +357,59 @@ private void singletonInstanceField(ClassModel.Builder classModel, TypeName serv .defaultValueContent("new " + descriptorType.className() + "<>()")); } - private void serviceTypeFields(ClassModel.Builder classModel, TypeName serviceType, TypeName descriptorType) { - classModel.addField(field -> field - .isStatic(true) - .isFinal(true) - .accessModifier(AccessModifier.PRIVATE) - .type(TypeNames.TYPE_NAME) - .name("SERVICE_TYPE") - .addContentCreate(serviceType.genericTypeName())); + private void dependencyFields(ClassModel.Builder classModel, CoreService service) { - classModel.addField(field -> field - .isStatic(true) - .isFinal(true) - .accessModifier(AccessModifier.PRIVATE) - .type(TypeNames.TYPE_NAME) - .name("DESCRIPTOR_TYPE") - .addContentCreate(descriptorType.genericTypeName())); - } + // first add all types + for (var genericConstant : service.constants().genericConstants()) { + addGenericTypeConstant(classModel, genericConstant.type(), genericConstant.constantName()); + } + for (var typeNameConstant : service.constants().typeNameConstants()) { + addTypeConstant(classModel, typeNameConstant.type(), typeNameConstant.constantName()); + } - private void dependencyFields(ClassModel.Builder classModel, - TypeInfo service, - Map genericTypes, - List params) { - // constant for dependency - for (ParamDefinition param : params) { + // and then add all dependencies (these use the types created above) + for (CoreDependency dependency : service.dependencies()) { classModel.addField(field -> field + // must be public, used in generated Injection__Binding to bind services .accessModifier(AccessModifier.PUBLIC) .isStatic(true) .isFinal(true) .type(ServiceCodegenTypes.SERVICE_DEPENDENCY) - .name(param.constantName()) - .description(dependencyDescription(service, param)) - .update(it -> { - it.addContent(ServiceCodegenTypes.SERVICE_DEPENDENCY) - .addContentLine(".builder()") - .increaseContentPadding() - .increaseContentPadding() - .addContent(".typeName(") - .addContent(genericTypes.get(param.translatedType().resolvedName()).constantName()) - .addContentLine(")") - .update(maybeElementKind -> { - if (param.kind() != ElementKind.CONSTRUCTOR) { - // constructor is default and does not need to be defined - maybeElementKind.addContent(".elementKind(") - .addContent(TypeNames.ELEMENT_KIND) - .addContent(".") - .addContent(param.kind().name()) - .addContentLine(")"); - } - }) - .update(maybeMethod -> { - if (param.kind() == ElementKind.METHOD) { - maybeMethod.addContent(".method(") - .addContent(param.methodConstantName()) - .addContentLine(")"); - } - }) - .addContent(".name(\"") - .addContent(param.fieldId()) - .addContentLine("\")") - .addContentLine(".service(SERVICE_TYPE)") - .addContentLine(".descriptor(DESCRIPTOR_TYPE)") - .addContent(".descriptorConstant(\"") - .addContent(param.constantName()) - .addContentLine("\")") - .addContent(".contract(") - .addContent(genericTypes.get(param.contract().fqName()).constantName()) - .addContentLine(")") - .addContent(".contractType(G") - .addContent(genericTypes.get(param.contract().fqName()).constantName()) - .addContentLine(")"); - if (param.access() != AccessModifier.PACKAGE_PRIVATE) { - it.addContent(".access(") - .addContent(TypeNames.ACCESS_MODIFIER) - .addContent(".") - .addContent(param.access().name()) - .addContentLine(")"); - } - - if (param.isStatic()) { - it.addContentLine(".isStatic(true)"); - } - - if (!param.qualifiers().isEmpty()) { - for (Annotation qualifier : param.qualifiers()) { - it.addContent(".addQualifier(qualifier -> qualifier.typeName(") - .addContentCreate(qualifier.typeName().genericTypeName()) - .addContent(")"); - qualifier.value().ifPresent(q -> it.addContent(".value(\"") - .addContent(q) - .addContent("\")")); - it.addContentLine(")"); - } - } - - it.addContent(".build()") - .decreaseContentPadding() - .decreaseContentPadding(); - })); + .name(dependency.dependencyConstant()) + .description(dependencyDescription(service, dependency)) + .addContent(ServiceCodegenTypes.SERVICE_DEPENDENCY) + .addContentLine(".builder()") + .increaseContentPadding() + .increaseContentPadding() + .addContent(".typeName(") + .addContent(dependency.typeNameConstant()) + .addContentLine(")") + .addContent(".name(\"") + .addContent(dependency.name()) + .addContentLine("\")") + .addContentLine(".service(SERVICE_TYPE)") + .addContentLine(".descriptor(DESCRIPTOR_TYPE)") + .addContent(".descriptorConstant(\"") + .addContent(dependency.dependencyConstant()) + .addContentLine("\")") + .addContent(".contract(") + .addContent(dependency.contractTypeConstant()) + .addContentLine(")") + .addContent(".contractType(") + .addContent(dependency.genericTypeConstant()) + .addContentLine(")") + .addContent(".build()") + .decreaseContentPadding() + .decreaseContentPadding()); } } - private String dependencyDescription(TypeInfo service, ParamDefinition param) { - TypeName serviceType = service.typeName(); + private String dependencyDescription(CoreService service, CoreDependency dependency) { + TypedElementInfo constructor = dependency.constructor(); + TypeName serviceType = service.serviceType(); StringBuilder result = new StringBuilder("Dependency for "); - boolean servicePublic = service.accessModifier() == AccessModifier.PUBLIC; - boolean elementPublic = param.owningElement().accessModifier() == AccessModifier.PUBLIC; + boolean servicePublic = typeInfo.accessModifier() == AccessModifier.PUBLIC; + boolean elementPublic = constructor.accessModifier() == AccessModifier.PUBLIC; if (servicePublic) { result.append("{@link ") @@ -631,19 +427,19 @@ private String dependencyDescription(TypeInfo service, ParamDefinition param) { .append("#") .append(serviceType.className()) .append("(") - .append(toDescriptionSignature(param.owningElement(), true)) + .append(toDescriptionSignature(constructor, true)) .append(")") .append("}"); } else { // just text result.append("(") - .append(toDescriptionSignature(param.owningElement(), false)) + .append(toDescriptionSignature(constructor, false)) .append(")"); } result .append(", parameter ") - .append(param.elementInfo().elementName()) + .append(dependency.name()) .append("."); return result.toString(); } @@ -662,7 +458,7 @@ private String toDescriptionSignature(TypedElementInfo method, boolean javadoc) } } - private void dependenciesField(ClassModel.Builder classModel, List params) { + private void dependenciesMethod(ClassModel.Builder classModel, CoreService service) { classModel.addField(dependencies -> dependencies .isStatic(true) .isFinal(true) @@ -671,18 +467,41 @@ private void dependenciesField(ClassModel.Builder classModel, List { - Iterator iterator = params.iterator(); + var iterator = service.dependencies().iterator(); while (iterator.hasNext()) { - it.addContent(iterator.next().constantName()); + it.addContent(iterator.next().dependencyConstant()); if (iterator.hasNext()) { it.addContent(", "); } } }) .addContent(")")); + + // List dependencies() + boolean hasSuperType = service.superType().present(); + if (hasSuperType || !service.dependencies().isEmpty()) { + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .returnType(LIST_OF_DEPENDENCIES) + .name("dependencies") + .update(it -> { + if (hasSuperType) { + it.addContentLine("return combineDependencies(DEPENDENCIES, super.dependencies());"); + } else { + it.addContentLine("return DEPENDENCIES;"); + } + })); + } } - private void serviceTypeMethod(ClassModel.Builder classModel) { + private void serviceTypeMethod(ClassModel.Builder classModel, CoreService service) { + classModel.addField(field -> field + .isStatic(true) + .isFinal(true) + .accessModifier(AccessModifier.PRIVATE) + .type(TypeNames.TYPE_NAME) + .name("SERVICE_TYPE") + .addContentCreate(service.serviceType().genericTypeName())); + // TypeName serviceType() classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) .returnType(TypeNames.TYPE_NAME) @@ -690,7 +509,15 @@ private void serviceTypeMethod(ClassModel.Builder classModel) { .addContentLine("return SERVICE_TYPE;")); } - private void descriptorTypeMethod(ClassModel.Builder classModel) { + private void descriptorTypeMethod(ClassModel.Builder classModel, CoreService service) { + classModel.addField(field -> field + .isStatic(true) + .isFinal(true) + .accessModifier(AccessModifier.PRIVATE) + .type(TypeNames.TYPE_NAME) + .name("DESCRIPTOR_TYPE") + .addContentCreate(service.descriptorType().genericTypeName())); + // TypeName descriptorType() classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) .returnType(TypeNames.TYPE_NAME) @@ -698,86 +525,99 @@ private void descriptorTypeMethod(ClassModel.Builder classModel) { .addContentLine("return DESCRIPTOR_TYPE;")); } - private void contractsMethod(ClassModel.Builder classModel, Set contracts) { - if (contracts.isEmpty()) { - return; - } - classModel.addField(contractsField -> contractsField - .isStatic(true) - .isFinal(true) - .name("CONTRACTS") - .type(SET_OF_TYPES) - .addContent(Set.class) - .addContent(".of(") - .update(it -> { - Iterator iterator = contracts.iterator(); - while (iterator.hasNext()) { - it.addContentCreate(iterator.next().genericTypeName()); - if (iterator.hasNext()) { - it.addContent(", "); + private void contractsMethod(ClassModel.Builder classModel, CoreService service) { + var contracts = service.contracts(); + var factoryContracts = service.factoryContracts(); + var superType = service.superType(); + + if (!contracts.isEmpty() || superType.present()) { + // we must declare the contracts method + classModel.addField(contractsField -> contractsField + .isStatic(true) + .isFinal(true) + .name("CONTRACTS") + .type(SET_OF_RESOLVED_TYPES) + .addContent(Set.class) + .addContent(".of(") + .update(it -> { + Iterator iterator = contracts.iterator(); + while (iterator.hasNext()) { + it.addContentCreate(iterator.next()); + if (iterator.hasNext()) { + it.addContent(", "); + } } - } - }) - .addContent(")")); + }) + .addContent(")")); - // Set> contracts() - classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) - .name("contracts") - .returnType(SET_OF_TYPES) - .addContentLine("return CONTRACTS;")); - } + // Set> contracts() + classModel.addMethod(method -> method + .addAnnotation(Annotations.OVERRIDE) + .name("contracts") + .returnType(SET_OF_RESOLVED_TYPES) + .addContentLine("return CONTRACTS;")); + } - private void dependenciesMethod(ClassModel.Builder classModel, List params, SuperType superType) { - // List dependencies() - boolean hasSuperType = superType.hasSupertype(); - if (hasSuperType || !params.isEmpty()) { - classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) - .returnType(LIST_OF_DEPENDENCIES) - .name("dependencies") + if (!factoryContracts.isEmpty() || superType.present()) { + // we must declare the contracts method + classModel.addField(contractsField -> contractsField + .isStatic(true) + .isFinal(true) + .name("FACTORY_CONTRACTS") + .type(SET_OF_RESOLVED_TYPES) + .addContent(Set.class) + .addContent(".of(") .update(it -> { - if (hasSuperType) { - it.addContentLine("return combineDependencies(DEPENDENCIES, super.dependencies());"); - } else { - it.addContentLine("return DEPENDENCIES;"); + Iterator iterator = factoryContracts.iterator(); + while (iterator.hasNext()) { + it.addContentCreate(iterator.next()); + if (iterator.hasNext()) { + it.addContent(", "); + } } - })); + }) + .addContent(")")); + + // Set> factoryContracts() + classModel.addMethod(method -> method + .addAnnotation(Annotations.OVERRIDE) + .name("factoryContracts") + .returnType(SET_OF_RESOLVED_TYPES) + .addContentLine("return FACTORY_CONTRACTS;")); } } - private void instantiateMethod(ClassModel.Builder classModel, - TypeName serviceType, - List params, - boolean isAbstractClass) { - if (isAbstractClass) { + private void instantiateMethod(ClassModel.Builder classModel, CoreService service) { + if (service.isAbstract()) { return; } // T instantiate(DependencyContext ctx__helidonRegistry) classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) - .returnType(serviceType) + .returnType(service.serviceType()) .name("instantiate") .addParameter(ctxParam -> ctxParam.type(ServiceCodegenTypes.SERVICE_DEPENDENCY_CONTEXT) .name("ctx__helidonRegistry")) - .update(it -> createInstantiateBody(serviceType, it, params))); + .update(it -> createInstantiateBody(service.serviceType(), it, service.dependencies()))); } - private void postConstructMethod(TypeInfo typeInfo, ClassModel.Builder classModel, TypeName serviceType) { + private void postConstructMethod(ClassModel.Builder classModel, CoreService service) { // postConstruct() lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_POST_CONSTRUCT).ifPresent(method -> { classModel.addMethod(postConstruct -> postConstruct.name("postConstruct") .addAnnotation(Annotations.OVERRIDE) - .addParameter(instance -> instance.type(serviceType) + .addParameter(instance -> instance.type(service.serviceType()) .name("instance")) .addContentLine("instance." + method.elementName() + "();")); }); } - private void preDestroyMethod(TypeInfo typeInfo, ClassModel.Builder classModel, TypeName serviceType) { + private void preDestroyMethod(ClassModel.Builder classModel, CoreService service) { // preDestroy lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_PRE_DESTROY).ifPresent(method -> { classModel.addMethod(preDestroy -> preDestroy.name("preDestroy") .addAnnotation(Annotations.OVERRIDE) - .addParameter(instance -> instance.type(serviceType) + .addParameter(instance -> instance.type(service.serviceType()) .name("instance")) .addContentLine("instance." + method.elementName() + "();")); }); @@ -798,31 +638,31 @@ private Optional lifecycleMethod(TypeInfo typeInfo, TypeName a TypedElementInfo method = list.getFirst(); if (method.accessModifier() == AccessModifier.PRIVATE) { throw new CodegenException("Method annotated with " + annotationType.fqName() - + ", is private, which is not supported: " + typeInfo.typeName().fqName() - + "#" + method.elementName(), - method.originatingElement().orElseGet(method::elementName)); + + ", is private, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName(), + method.originatingElementValue()); } if (!method.parameterArguments().isEmpty()) { throw new CodegenException("Method annotated with " + annotationType.fqName() - + ", has parameters, which is not supported: " + typeInfo.typeName().fqName() - + "#" + method.elementName(), - method.originatingElement().orElseGet(method::elementName)); + + ", has parameters, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName(), + method.originatingElementValue()); } if (!method.typeName().equals(TypeNames.PRIMITIVE_VOID)) { throw new CodegenException("Method annotated with " + annotationType.fqName() - + ", is not void, which is not supported: " + typeInfo.typeName().fqName() - + "#" + method.elementName(), - method.originatingElement().orElseGet(method::elementName)); + + ", is not void, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName(), + method.originatingElementValue()); } return Optional.of(method); } private void createInstantiateBody(TypeName serviceType, Method.Builder method, - List params) { - List constructorParams = declareCtrParamsAndGetThem(method, params); - String paramsDeclaration = constructorParams.stream() - .map(ParamDefinition::ipParamName) + List params) { + declareConstructorParams(method, params); + String paramsDeclaration = params.stream() + .map(CoreDependency::name) .collect(Collectors.joining(", ")); // return new MyImpl(parameter, parameter2) @@ -833,8 +673,8 @@ private void createInstantiateBody(TypeName serviceType, .addContentLine(");"); } - private void isAbstractMethod(ClassModel.Builder classModel, SuperType superType, boolean isAbstractClass) { - if (!isAbstractClass && !superType.hasSupertype()) { + private void isAbstractMethod(ClassModel.Builder classModel, CoreService service) { + if (!service.isAbstract() && service.superType().empty()) { return; } // only override for abstract types (and subtypes, where we do not want to check if super is abstract), default is false @@ -842,11 +682,11 @@ private void isAbstractMethod(ClassModel.Builder classModel, SuperType superType .name("isAbstract") .returnType(TypeNames.PRIMITIVE_BOOLEAN) .addAnnotation(Annotations.OVERRIDE) - .addContentLine("return " + isAbstractClass + ";")); + .addContentLine("return " + service.isAbstract() + ";")); } - private void weightMethod(TypeInfo typeInfo, ClassModel.Builder classModel, SuperType superType) { - boolean hasSuperType = superType.hasSupertype(); + private void weightMethod(ClassModel.Builder classModel, CoreService service) { + boolean hasSuperType = service.superType().present(); // double weight() Optional weight = weight(typeInfo); @@ -869,17 +709,9 @@ private Optional weight(TypeInfo typeInfo) { .flatMap(Annotation::doubleValue); } - private RegistryCodegenContext.Assignment translateParameter(TypeName typeName, String constantName) { - return ctx.assignment(typeName, "ctx__helidonRegistry.dependency(" + constantName + ")"); - } - private TypeName descriptorInstanceType(TypeName serviceType, TypeName descriptorType) { return TypeName.builder(descriptorType) .addTypeArgument(serviceType) .build(); } - - private record GenericTypeDeclaration(String constantName, - TypeName typeName) { - } } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/HelidonMetaInfServices.java b/service/codegen/src/main/java/io/helidon/service/codegen/HelidonMetaInfServices.java index 4108c516d39..f965e8d2547 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/HelidonMetaInfServices.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/HelidonMetaInfServices.java @@ -44,9 +44,10 @@ * ({@link io.helidon.service.codegen.ServiceCodegenTypes#SERVICE_ANNOTATION_PROVIDER}) * will have a service descriptor generated at build time. *

    - * The service descriptor is then discoverable at runtime through our own resource in {@value #SERVICES_RESOURCE}. + * The service descriptor is then discoverable at runtime through our own resource in + * {@value Descriptors#SERVICE_REGISTRY_LOCATION}. */ -class HelidonMetaInfServices { +public class HelidonMetaInfServices { private final FilerResource services; private final String moduleName; private final Set descriptors; @@ -64,7 +65,7 @@ private HelidonMetaInfServices(FilerResource services, String moduleName, Set services) { + public void addAll(Collection services) { services.forEach(this::add); } @@ -96,7 +97,7 @@ void addAll(Collection services) { * * @param service service descriptor metadata to add */ - void add(DescriptorMetadata service) { + public void add(DescriptorMetadata service) { // if it is the same descriptor class, remove it descriptors.removeIf(it -> it.descriptorType().equals(service.descriptorType())); @@ -107,7 +108,7 @@ void add(DescriptorMetadata service) { /** * Write the file to output. */ - void write() { + public void write() { var root = Hson.structBuilder() .set("module", moduleName); List servicesHson = new ArrayList<>(); diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContext.java b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContext.java index 6066e8f90a6..82d0367e5b9 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContext.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContext.java @@ -16,21 +16,13 @@ package io.helidon.service.codegen; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; - -import io.helidon.codegen.ClassCode; import io.helidon.codegen.CodegenContext; -import io.helidon.codegen.classmodel.ClassModel; -import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.common.types.TypeName; /** * Codegen context adding methods suitable for Helidon Service Registry code generation. */ -interface RegistryCodegenContext extends CodegenContext { +public interface RegistryCodegenContext extends CodegenContext { /** * Create a new instance from an existing code generation context. * @@ -41,53 +33,6 @@ static RegistryCodegenContext create(CodegenContext context) { return new RegistryCodegenContextImpl(context); } - /** - * Service descriptor of a type that is already created. This allows extensions with lower weight to update - * the code generated descriptor after it was generated. - * - * @param serviceType type of the service (the implementation class we generate descriptor for) - * @return the builder of class model, if the service has a descriptor - */ - Optional descriptor(TypeName serviceType); - - /** - * Add a new service descriptor. - * - * @param registryType service registry this descriptor is designed for (core is the "top" level) - * @param serviceType type of the service (the implementation class we generate descriptor for) - * @param descriptorType type of the service descriptor - * @param descriptor descriptor class model - * @param weight weight of this service descriptor - * @param contracts contracts of this service descriptor - * @param originatingElements possible originating elements (such as Element in APT, or ClassInfo in classpath scanning) - * @throws java.lang.IllegalStateException if an attempt is done to register a new descriptor for the same type - */ - void addDescriptor(String registryType, - TypeName serviceType, - TypeName descriptorType, - ClassModel.Builder descriptor, - double weight, - Set contracts, - Object... originatingElements); - - /** - * Add a new class to be code generated. - * - * @param type type of the new class - * @param newClass builder of the new class - * @param mainTrigger a type that caused this, may be the processor itself, if not bound to any type - * @param originatingElements possible originating elements (such as Element in APT, or ClassInfo in classpath scanning) - */ - void addType(TypeName type, ClassModel.Builder newClass, TypeName mainTrigger, Object... originatingElements); - - /** - * Class for a type. - * - * @param type type of the generated type - * @return class model of the new type if any - */ - Optional type(TypeName type); - /** * Create a descriptor type for a service. * @@ -95,39 +40,4 @@ void addDescriptor(String registryType, * @return type of the service descriptor to be generated */ TypeName descriptorType(TypeName serviceType); - - /** - * All newly generated types. - * - * @return list of types and their source class model - */ - List types(); - - /** - * All newly generated descriptors. - * - * @return list of descriptors and their source class model - */ - List descriptors(); - - /** - * This provides support for replacements of types. - * - * @param typeName type name as required by the dependency ("injection point") - * @param valueSource code with the source of the parameter as Helidon provides it (such as Supplier of type) - * @return assignment to use for this instance, what type to use in Helidon registry, and code generator to transform to - * desired type - */ - Assignment assignment(TypeName typeName, String valueSource); - - /** - * Assignment for code generation. The original intended purpose is to support {@code Provider} from javax and jakarta - * without a dependency (or need to understand it) in the generator code. - * - * @param usedType type to use as the dependency type using only Helidon supported types - * (i.e. {@link java.util.function.Supplier} instead of jakarta {@code Provider} - * @param codeGenerator code generator that creates appropriate type required by the target - */ - record Assignment(TypeName usedType, Consumer> codeGenerator) { - } } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContextImpl.java b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContextImpl.java index ad7cfda1a0e..e8fb061da1a 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContextImpl.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenContextImpl.java @@ -16,81 +16,17 @@ package io.helidon.service.codegen; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import io.helidon.codegen.ClassCode; import io.helidon.codegen.CodegenContext; import io.helidon.codegen.CodegenContextDelegate; -import io.helidon.codegen.classmodel.ClassModel; import io.helidon.common.types.TypeName; class RegistryCodegenContextImpl extends CodegenContextDelegate implements RegistryCodegenContext { - private final List descriptors = new ArrayList<>(); - private final List nonDescriptors = new ArrayList<>(); - RegistryCodegenContextImpl(CodegenContext context) { super(context); } - @Override - public Optional descriptor(TypeName serviceType) { - Objects.requireNonNull(serviceType); - - for (DescriptorClassCode descriptor : descriptors) { - ClassCode classCode = descriptor.classCode(); - if (classCode.mainTrigger().equals(serviceType)) { - return Optional.of(classCode.classModel()); - } - } - return Optional.empty(); - } - - @Override - public void addDescriptor(String registryType, - TypeName serviceType, - TypeName descriptorType, - ClassModel.Builder descriptor, - double weight, - Set contracts, - Object... originatingElements) { - Objects.requireNonNull(registryType); - Objects.requireNonNull(serviceType); - Objects.requireNonNull(descriptorType); - Objects.requireNonNull(descriptor); - Objects.requireNonNull(contracts); - Objects.requireNonNull(originatingElements); - - descriptors.add(new DescriptorClassCodeImpl(new ClassCode(descriptorType, descriptor, serviceType, originatingElements), - registryType, - weight, - contracts)); - } - - @Override - public void addType(TypeName type, ClassModel.Builder newClass, TypeName mainTrigger, Object... originatingElements) { - nonDescriptors.add(new ClassCode(type, newClass, mainTrigger, originatingElements)); - } - - @Override - public Optional type(TypeName type) { - for (ClassCode classCode : nonDescriptors) { - if (classCode.newType().equals(type)) { - return Optional.of(classCode.classModel()); - } - } - for (DescriptorClassCode descriptor : descriptors) { - ClassCode classCode = descriptor.classCode(); - if (classCode.newType().equals(type)) { - return Optional.of(classCode.classModel()); - } - } - return Optional.empty(); - } - @Override public TypeName descriptorType(TypeName serviceType) { // type is generated in the same package with a name suffix @@ -101,21 +37,6 @@ public TypeName descriptorType(TypeName serviceType) { .build(); } - @Override - public List types() { - return nonDescriptors; - } - - @Override - public List descriptors() { - return descriptors; - } - - @Override - public Assignment assignment(TypeName typeName, String valueSource) { - return new Assignment(typeName, it -> it.addContent(valueSource)); - } - private static String descriptorClassName(TypeName typeName) { // for MyType.MyService -> MyType_MyService__ServiceDescriptor diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryRoundContext.java b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryRoundContext.java index b7d50143182..d9c60c095dc 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryRoundContext.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryRoundContext.java @@ -16,46 +16,48 @@ package io.helidon.service.codegen; -import java.util.Collection; +import java.util.Set; +import io.helidon.codegen.RoundContext; +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; /** * Context of a single round of code generation. * For example the first round may generate types, that require additional code generation. */ -interface RegistryRoundContext { +public interface RegistryRoundContext extends RoundContext { /** - * Available annotations for this provider. + * Add a new service descriptor. * - * @return annotation types + * @param registryType service registry this descriptor is designed for (core is the "top" level) + * @param serviceType type of the service (the implementation class we generate descriptor for) + * @param descriptorType type of the service descriptor + * @param descriptor descriptor class model + * @param weight weight of this service descriptor + * @param contracts contracts of this service descriptor + * @param factoryContracts contracts of the service class if it is a factory + * @param originatingElements possible originating elements (such as Element in APT, or ClassInfo in classpath scanning) + * @throws java.lang.IllegalStateException if an attempt is done to register a new descriptor for the same type */ - Collection availableAnnotations(); + // all parameters are needed, no sense in creating a builder + @SuppressWarnings("checkstyle:ParameterNumber") + void addDescriptor(String registryType, + TypeName serviceType, + TypeName descriptorType, + ClassModel.Builder descriptor, + double weight, + Set contracts, + Set factoryContracts, + Object... originatingElements); /** - * All types for processing in this round. + * Create service contracts for the provided service that honor configuration of contract discovery. * - * @return all type infos + * @param serviceInfo type info of the analyzed service + * @return service contracts */ - - Collection types(); - - /** - * All types annotated with a specific annotation. - * - * @param annotationType annotation type - * @return type infos annotated with the provided annotation - */ - - Collection annotatedTypes(TypeName annotationType); - - /** - * All elements annotated with a specific annotation. - * - * @param annotationType annotation type - * @return elements annotated with the provided annotation - */ - Collection annotatedElements(TypeName annotationType); + ServiceContracts serviceContracts(TypeInfo serviceInfo); } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/RoundContextImpl.java b/service/codegen/src/main/java/io/helidon/service/codegen/RoundContextImpl.java index 7e1c6bf8a7d..b147542385f 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/RoundContextImpl.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/RoundContextImpl.java @@ -20,23 +20,42 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import io.helidon.codegen.ClassCode; +import io.helidon.codegen.RoundContext; +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; class RoundContextImpl implements RegistryRoundContext { + private final RegistryCodegenContext ctx; + private final RoundContext delegate; private final Map> annotationToTypes; + private final Map> metaAnnotated; private final List types; + private final Consumer addDescriptorConsumer; private final Collection annotations; - RoundContextImpl(Set annotations, + RoundContextImpl(RegistryCodegenContext ctx, + RoundContext delegate, + Consumer addDescriptorConsumer, + Set annotations, Map> annotationToTypes, + Map> metaAnnotated, List types) { + this.ctx = ctx; + this.delegate = delegate; + this.addDescriptorConsumer = addDescriptorConsumer; this.annotations = annotations; this.annotationToTypes = annotationToTypes; + this.metaAnnotated = metaAnnotated; this.types = types; } @@ -86,4 +105,67 @@ public Collection annotatedTypes(TypeName annotationType) { return result; } + + @Override + public Collection annotatedAnnotations(TypeName metaAnnotation) { + return Optional.ofNullable(metaAnnotated.get(metaAnnotation)).orElseGet(Set::of); + } + + @Override + public Optional typeInfo(TypeName typeName) { + return delegate.typeInfo(typeName); + } + + /** + * Add a non-service descriptor generated type. + * + * @param type type of the new class + * @param newClass builder of the new class + * @param mainTrigger a type that caused this, may be the processor itself, if not bound to any type + * @param originatingElements possible originating elements (such as Element in APT, or ClassInfo in classpath scanning) + */ + @Override + public void addGeneratedType(TypeName type, + ClassModel.Builder newClass, + TypeName mainTrigger, + Object... originatingElements) { + delegate.addGeneratedType(type, newClass, mainTrigger, originatingElements); + } + + @Override + public void addDescriptor(String registryType, + TypeName serviceType, + TypeName descriptorType, + ClassModel.Builder descriptor, + double weight, + Set contracts, + Set factoryContracts, + Object... originatingElements) { + Objects.requireNonNull(registryType); + Objects.requireNonNull(serviceType); + Objects.requireNonNull(descriptorType); + Objects.requireNonNull(descriptor); + Objects.requireNonNull(contracts); + Objects.requireNonNull(originatingElements); + + addDescriptorConsumer + .accept(new DescriptorClassCodeImpl(new ClassCode(descriptorType, descriptor, serviceType, originatingElements), + registryType, + weight, + contracts, + factoryContracts)); + delegate.addGeneratedType(descriptorType, descriptor, serviceType, originatingElements); + } + + @Override + public ServiceContracts serviceContracts(TypeInfo serviceInfo) { + return ServiceContracts.create(ctx.options(), + this::typeInfo, + serviceInfo); + } + + @Override + public Optional generatedType(TypeName type) { + return delegate.generatedType(type); + } } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java index 317ac1222b7..0ed9c08ff5b 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java @@ -16,6 +16,7 @@ package io.helidon.service.codegen; +import io.helidon.common.Generated; import io.helidon.common.types.TypeName; /** @@ -51,9 +52,10 @@ public final class ServiceCodegenTypes { public static final TypeName SERVICE_ANNOTATION_DESCRIPTOR = TypeName.create("io.helidon.service.registry.Service.Descriptor"); /** - * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.GeneratedService.Descriptor}. + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.ServiceDescriptor}. */ - public static final TypeName SERVICE_DESCRIPTOR = TypeName.create("io.helidon.service.registry.GeneratedService.Descriptor"); + public static final TypeName SERVICE_DESCRIPTOR = + TypeName.create("io.helidon.service.registry.ServiceDescriptor"); /** * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Dependency}. */ @@ -62,6 +64,15 @@ public final class ServiceCodegenTypes { * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.DependencyContext}. */ public static final TypeName SERVICE_DEPENDENCY_CONTEXT = TypeName.create("io.helidon.service.registry.DependencyContext"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.builder.api.Prototype.Blueprint}. + */ + public static final TypeName BUILDER_BLUEPRINT = TypeName.create("io.helidon.builder.api.Prototype.Blueprint"); + + /** + * {@link io.helidon.common.types.TypeName} for {@link io.helidon.common.Generated}. + */ + public static final TypeName GENERATED_ANNOTATION = TypeName.create(Generated.class); private ServiceCodegenTypes() { } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceContracts.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceContracts.java new file mode 100644 index 00000000000..f4045fa2e88 --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceContracts.java @@ -0,0 +1,425 @@ +/* + * 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.codegen; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenOptions; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +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.common.types.TypeNames; + +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_CONTRACT; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_EXTERNAL_CONTRACTS; + +/** + * Handling of eligible contracts. + *

    + * Contract is eligible if it is annotated with {@code Service.Contract}, or referenced + * from service type or its super types via {@code Service.ExternalContracts}. + *

    + * In case the option {@link io.helidon.service.codegen.ServiceOptions#AUTO_ADD_NON_CONTRACT_INTERFACES} is set to + * true, all types are eligible (including classes and not contract annotated types). + */ +public class ServiceContracts { + private static final TypeInfo OBJECT_INFO = TypeInfo.builder() + .typeName(TypeNames.OBJECT) + .kind(ElementKind.CLASS) + .accessModifier(AccessModifier.PUBLIC) + .build(); + + private final TypeInfo serviceInfo; + private final Function isEligibleInfo; + private final Function isEligibleType; + private final Function> typeInfoFactory; + + private ServiceContracts(Function> typeInfoFactory, + TypeInfo serviceInfo, + Function isEligibleInfo, + Function isEligibleType) { + this.typeInfoFactory = typeInfoFactory; + this.serviceInfo = serviceInfo; + this.isEligibleInfo = isEligibleInfo; + this.isEligibleType = isEligibleType; + } + + /** + * Create new eligible contracts. + * + * @param options codegen options + * @param typeInfoFactory function to obtain a type info (if possible) based on type name + * @param serviceInfo service info to analyze + * @return a new instance to check if an implemented interface or a super type is a contract or not + */ + public static ServiceContracts create(CodegenOptions options, + Function> typeInfoFactory, + TypeInfo serviceInfo) { + Set eligibleExternalContracts = new HashSet<>(); + // gather eligible external contracts + eligibleContracts(eligibleExternalContracts, + new HashSet<>(), + serviceInfo); + + boolean onlyAnnotatedContracts = !ServiceOptions.AUTO_ADD_NON_CONTRACT_INTERFACES.value(options); + var nonContractTypes = ServiceOptions.NON_CONTRACT_TYPES.value(options); + + Function isEligibleInfo = typeInfo -> isEligible( + nonContractTypes, + onlyAnnotatedContracts, + eligibleExternalContracts, + typeInfo); + + Function isEligibleType = typeName -> isEligible( + typeInfoFactory, + nonContractTypes, + onlyAnnotatedContracts, + eligibleExternalContracts, + typeName); + + return new ServiceContracts(typeInfoFactory, serviceInfo, isEligibleInfo, isEligibleType); + } + + /** + * Get the desired type parameter of the type info provided. + * + * @param typeInfo type info to analyze (such as an implemented interface {@code Supplier} + * @param index index of the type arguments (such as {@code 0}) + * @return the type argument at the requested index, such as {@code java.lang.String} + * @throws io.helidon.codegen.CodegenException in case the type argument is not available + */ + public static TypeName requiredTypeArgument(TypeInfo typeInfo, int index) { + Objects.requireNonNull(typeInfo); + + TypeName withGenerics = typeInfo.typeName(); + List typeArguments = withGenerics.typeArguments(); + if (typeArguments.isEmpty()) { + throw new CodegenException("Type arguments cannot be empty for implemented interface " + withGenerics, + typeInfo.originatingElementValue()); + } + if (typeArguments.size() < (index + 1)) { + throw new CodegenException("There must be at least " + (index + 1) + " type arguments for implemented interface " + + withGenerics.resolvedName(), + typeInfo.originatingElementValue()); + } + + // factory must have a type argument (and the type argument is an automatic contract + TypeName contract = typeArguments.get(index); + if (contract.generic()) { + // probably just T (such as Supplier) + throw new CodegenException("Type argument must be a concrete type for implemented interface " + withGenerics, + typeInfo.originatingElementValue()); + } + return contract; + } + + /** + * Check if a type info is eligible to be a contract. + * + * @param contractInfo candidate type info + * @return whether the candidate is a contract or not + */ + public boolean isEligible(TypeInfo contractInfo) { + return this.isEligibleInfo.apply(contractInfo); + } + + /** + * Check if a type is eligible to be a contract. + * + * @param contractType candidate type + * @return whether the candidate is a contract or not + */ + public boolean isEligible(TypeName contractType) { + return this.isEligibleType.apply(contractType); + } + + /** + * Analyse the service info if it is in fact a factory of the expected type. + * + * @param factoryInterface the provider we check, the provided contract must be the first type argument + * @return result of the analysis + */ + public FactoryAnalysis analyseFactory(TypeName factoryInterface) { + Optional implementedFactory = serviceInfo.interfaceTypeInfo() + .stream() + .filter(it -> it.typeName().equals(factoryInterface)) + .findFirst(); + + if (implementedFactory.isEmpty()) { + // the factory interface is not implemented by the service + return FactoryAnalysis.create(); + } + + // it is implemented + TypeInfo typeInfo = implementedFactory.get(); + TypeName contract = resolveOptional(typeInfo, requiredTypeArgument(typeInfo), factoryInterface); + Set contracts = new HashSet<>(); + contracts.add(ResolvedType.create(contract)); + + TypeInfo contractInfo = contractInfo(typeInfoFactory, serviceInfo, contract); + + addContracts(contracts, + new HashSet<>(), + contractInfo); + return FactoryAnalysis.create(typeInfo.typeName(), + contract, + contractInfo, + contracts); + } + + /** + * Add contracts from the type (from its implemented interfaces and super types). + * + * @param contractSet set of contracts to amend with the contracts of the provided type info + * @param processed set of processed contracts, to avoid infinite loop + * @param typeInfo type info to analyze for contracts + */ + public void addContracts(Set contractSet, + HashSet processed, + TypeInfo typeInfo) { + TypeName withGenerics = typeInfo.typeName(); + ResolvedType resolvedType = ResolvedType.create(withGenerics); + + if (!processed.add(resolvedType)) { + // this type was already fully processed + return; + } + + if (!resolvedType.type().typeArguments().isEmpty()) { + // we also need to add a contract for the type it implements + // i.e. if this is Circle, we may want to add Circle as well + typeInfoFactory.apply(withGenerics.genericTypeName()) + .ifPresent(declaration -> { + TypeName tn = declaration.typeName(); + for (int i = 0; i < withGenerics.typeArguments().size(); i++) { + TypeName declared = tn.typeArguments().get(i); + if (declared.generic()) { + // this is not ideal (this could be T extends Circle) + var asString = declared.toString(); + int index = asString.indexOf(" extends "); + if (index != -1) { + TypeName extendedType = TypeName.create(asString.substring(index + 9)); + if (isEligible(extendedType)) { + contractSet.add(ResolvedType.create( + TypeName.builder() + .from(withGenerics) + .typeArguments(List.of(extendedType)) + .build())); + } + } + } else { + contractSet.add(ResolvedType.create(declared)); + } + } + }); + } + + // add this type if eligible + if (isEligible(typeInfo)) { + contractSet.add(ResolvedType.create(withGenerics)); + } + + // super type + typeInfo.superTypeInfo() + .ifPresent(it -> addContracts( + contractSet, + processed, + it + )); + + // interfaces + typeInfo.interfaceTypeInfo() + .forEach(it -> addContracts( + contractSet, + processed, + it + )); + } + + private static TypeInfo contractInfo(Function> typeInfoFactory, + TypeInfo serviceTypeInfo, + TypeName contract) { + if (TypeNames.OBJECT.equals(contract)) { + return OBJECT_INFO; + } + var typeInfo = typeInfoFactory.apply(contract); + if (typeInfo.isPresent()) { + return typeInfo.get(); + } + throw new CodegenException("Failed to discover type info for " + contract.fqName(), + serviceTypeInfo.originatingElementValue()); + } + + private static TypeName requiredTypeArgument(TypeInfo typeInfo) { + return requiredTypeArgument(typeInfo, 0); + } + + private static boolean isEligible(Function> typeInfoFactory, + Set nonContractTypes, + boolean onlyAnnotatedAreEligible, + Set eligibleExternalContracts, + TypeName toCheck) { + if (eligibleExternalContracts.contains(toCheck)) { + return true; + } + if (nonContractTypes.contains(toCheck)) { + return false; + } + // if the type info does not exist on classpath, and is not an external contract, return false + return typeInfoFactory.apply(toCheck) + .map(it -> isEligible(nonContractTypes, onlyAnnotatedAreEligible, eligibleExternalContracts, it)) + .orElse(false); + } + + private static boolean isEligible(Set nonContractTypes, + boolean onlyAnnotatedAreEligible, + Set eligibleExternalContracts, + TypeInfo toCheck) { + if (eligibleExternalContracts.contains(toCheck.typeName())) { + return true; + } + if (nonContractTypes.contains(toCheck.typeName())) { + return false; + } + if (toCheck.hasAnnotation(SERVICE_ANNOTATION_CONTRACT)) { + return true; + } + if (onlyAnnotatedAreEligible) { + return false; + } + return toCheck.kind() == ElementKind.INTERFACE; + } + + /** + * Gather eligible external contracts from this type and super types, + * all other contracts are validated when encountered (and either onlyAnnotatedAreEligible is set to false, + * or they have to be annotated with Contract). + */ + private static void eligibleContracts(Set eligibleContracts, + Set processedFullyQualified, + TypeInfo typeInfo) { + + if (!processedFullyQualified.add(typeInfo.typeName().resolvedName())) { + // this type was already fully processed + return; + } + + addExternalContracts(eligibleContracts, typeInfo); + + // super type + typeInfo.superTypeInfo() + .ifPresent(it -> eligibleContracts(eligibleContracts, processedFullyQualified, it)); + + // interfaces + typeInfo.interfaceTypeInfo() + .forEach(it -> eligibleContracts(eligibleContracts, processedFullyQualified, it)); + } + + private static void addExternalContracts(Set eligibleContracts, TypeInfo typeInfo) { + typeInfo.findAnnotation(SERVICE_ANNOTATION_EXTERNAL_CONTRACTS) + .flatMap(Annotation::typeValues) + .ifPresent(eligibleContracts::addAll); + } + + private TypeName resolveOptional(TypeInfo typeInfo, TypeName typeName, TypeName factoryInterface) { + // for suppliers, we support optional, all other factory types can return optional by design + if (factoryInterface.equals(TypeNames.SUPPLIER) && typeName.isOptional()) { + // Supplier of optionals + if (typeName.typeArguments().isEmpty()) { + throw new CodegenException("Invalid declaration of Supplier, Optional is missing type argument", + typeInfo.originatingElementValue()); + } + return typeName.typeArguments().getFirst(); + } + return typeName; + } + + /** + * Result of analysis of provided contracts. + */ + public interface FactoryAnalysis { + + /** + * Create a new result for cases where the service does not implement the factory interface. + * + * @return a new factory analysis that is not {@link #valid()} + */ + static FactoryAnalysis create() { + return new FactoryAnalysisImpl(); + } + + /** + * The requested factory interface is implemented and provides one or more contracts. + * + * @param factoryType type of the factory implementation (such as {@code Supplier}) + * @param providedType the type provided (always a contract) + * @param providedTypeInfo type info of the provided type + * @param providedContracts transitive contracts (includes the provided type as well) + * @return a new analysis result for a valid factory implementation + */ + static FactoryAnalysis create(TypeName factoryType, + TypeName providedType, + TypeInfo providedTypeInfo, + Set providedContracts) { + return new FactoryAnalysisImpl(factoryType, providedType, providedTypeInfo, providedContracts); + } + + /** + * whether the factory interface is implemented. + * + * @return if the factory interface is implemented by the service + */ + boolean valid(); + + /** + * Type of the factory interface with type arguments (such as {@code Supplier×String>}, guard access by {@link #valid()}). + * + * @return factory type name + */ + TypeName factoryType(); + + /** + * The contract provided (guard access by {@link #valid()}). + * + * @return provided type name + */ + TypeName providedType(); + + /** + * Type info of the provided type. + * + * @return type info of the {@link #providedType()} + */ + TypeInfo providedTypeInfo(); + + /** + * All contracts transitively inherited from the provided type (guard access by {@link #valid()}). + * + * @return provided contracts + */ + Set providedContracts(); + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtension.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtension.java index 7328c4112aa..0b8d8888277 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtension.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtension.java @@ -20,6 +20,7 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; +import io.helidon.service.codegen.spi.RegistryCodegenExtension; class ServiceExtension implements RegistryCodegenExtension { private static final TypeName GENERATOR = TypeName.create(ServiceExtension.class); @@ -35,12 +36,18 @@ public void process(RegistryRoundContext roundContext) { Collection descriptorsRequired = roundContext.types(); for (TypeInfo typeInfo : descriptorsRequired) { - generateDescriptor(descriptorsRequired, typeInfo); + generateDescriptor(roundContext, descriptorsRequired, typeInfo); } } - private void generateDescriptor(Collection services, + private void generateDescriptor(RegistryRoundContext roundContext, + Collection descriptorsRequired, TypeInfo typeInfo) { - GenerateServiceDescriptor.generate(GENERATOR, ctx, services, typeInfo); + + GenerateServiceDescriptor.generate(GENERATOR, + ctx, + roundContext, + descriptorsRequired, + typeInfo); } } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtensionProvider.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtensionProvider.java new file mode 100644 index 00000000000..a1aeff58861 --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceExtensionProvider.java @@ -0,0 +1,55 @@ +/* + * 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.codegen; + +import java.util.Set; + +import io.helidon.codegen.Option; +import io.helidon.common.types.TypeName; +import io.helidon.service.codegen.spi.RegistryCodegenExtension; +import io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider; + +/** + * A {@link java.util.ServiceLoader} provider implementation that adds code generation for Helidon Service Registry. + * This extension creates service descriptors. + */ +public class ServiceExtensionProvider implements RegistryCodegenExtensionProvider { + /** + * Required default constructor for {@link java.util.ServiceLoader}. + * + * @deprecated only for {@link java.util.ServiceLoader} + */ + @Deprecated + public ServiceExtensionProvider() { + super(); + } + + @Override + public Set> supportedOptions() { + return Set.of(ServiceOptions.AUTO_ADD_NON_CONTRACT_INTERFACES); + } + + @Override + public Set supportedAnnotations() { + return Set.of(ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER); + } + + @Override + public RegistryCodegenExtension create(RegistryCodegenContext codegenContext) { + return new ServiceExtension(codegenContext); + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceOptions.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceOptions.java index dae3314baf4..8967fa1f1ce 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceOptions.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceOptions.java @@ -16,19 +16,35 @@ package io.helidon.service.codegen; +import java.io.Serializable; +import java.util.Set; + import io.helidon.codegen.Option; +import io.helidon.common.GenericType; +import io.helidon.common.types.TypeName; /** * Supported options specific to Helidon Service Registry. */ -final class ServiceOptions { +public final class ServiceOptions { /** * Treat all super types as a contract for a given service type being added. */ public static final Option AUTO_ADD_NON_CONTRACT_INTERFACES = Option.create("helidon.registry.autoAddNonContractInterfaces", - "Treat all super types as a contract for a given service type being added.", - false); + "Treat all super types and implemented types as a contract for a given service type " + + "being added. Defaults to true.", + true); + /** + * A set of interface/class types that should not be considered contracts, even if implemented by a service. + */ + public static final Option> NON_CONTRACT_TYPES = + Option.createSet("helidon.registry.nonContractTypes", + "Types that should not be considered contracts. " + + "By default we exclude Serializable.", + Set.of(TypeName.create(Serializable.class)), + TypeName::create, + new GenericType>() { }); private ServiceOptions() { } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenExtension.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenExtension.java index be5b29c9d98..bc105d8b6bf 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenExtension.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenExtension.java @@ -20,26 +20,28 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import io.helidon.codegen.ClassCode; import io.helidon.codegen.CodegenContext; -import io.helidon.codegen.CodegenFiler; import io.helidon.codegen.CodegenOptions; import io.helidon.codegen.ModuleInfo; -import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.RoundContext; +import io.helidon.codegen.TypeHierarchy; import io.helidon.codegen.spi.CodegenExtension; import io.helidon.common.Weighted; import io.helidon.common.types.Annotation; +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.common.types.TypedElementInfo; +import io.helidon.service.codegen.spi.RegistryCodegenExtension; +import io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider; import io.helidon.service.metadata.DescriptorMetadata; import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_DESCRIPTOR; @@ -48,31 +50,42 @@ * Handles processing of all extensions, creates context and writes types. */ class ServiceRegistryCodegenExtension implements CodegenExtension { - private final Map> typeToExtensions = new HashMap<>(); - private final Map> extensionPredicates = new IdentityHashMap<>(); private final Set generatedServiceDescriptors = new HashSet<>(); + private final List extensions; private final RegistryCodegenContext ctx; - private final List extensions; private final String module; - private ServiceRegistryCodegenExtension(CodegenContext ctx, TypeName generator) { + private ServiceRegistryCodegenExtension(CodegenContext ctx, + List extensions) { this.ctx = RegistryCodegenContext.create(ctx); this.module = ctx.moduleName().orElse(null); - - ServiceExtension serviceExtension = new ServiceExtension(this.ctx); - this.extensions = List.of(serviceExtension); - this.typeToExtensions.put(ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER, List.of(serviceExtension)); + this.extensions = extensions.stream() + .map(it -> { + RegistryCodegenExtension extension = it.create(this.ctx); + return new ExtensionInfo(extension, + discoveryPredicate(it.supportedAnnotations(), + it.supportedAnnotationPackages()), + it.supportedMetaAnnotations()); + }) + .toList(); } - static ServiceRegistryCodegenExtension create(CodegenContext ctx, TypeName generator) { - return new ServiceRegistryCodegenExtension(ctx, generator); + static ServiceRegistryCodegenExtension create(CodegenContext ctx, + List extensions) { + return new ServiceRegistryCodegenExtension(ctx, extensions); } @Override public void process(io.helidon.codegen.RoundContext roundContext) { + List descriptors = new ArrayList<>(); Collection allTypes = roundContext.types(); if (allTypes.isEmpty()) { - extensions.forEach(it -> it.process(createRoundContext(List.of(), it))); + extensions.forEach(it -> it.extension() + .process(createRoundContext( + roundContext, + List.of(), + it, + descriptors))); return; } @@ -83,20 +96,22 @@ public void process(io.helidon.codegen.RoundContext roundContext) { // and create a new round context for each extension // for each extension, create a RoundContext with just the stuff it wants - for (RegistryCodegenExtension extension : extensions) { - extension.process(createRoundContext(annotatedTypes, extension)); + for (var extension : extensions) { + extension.extension().process(createRoundContext(roundContext, annotatedTypes, extension, descriptors)); } - writeNewTypes(); + writeNewTypes(descriptors); for (TypeInfo typeInfo : roundContext.annotatedTypes(SERVICE_ANNOTATION_DESCRIPTOR)) { // add each declared descriptor in source code Annotation descriptorAnnot = typeInfo.annotation(SERVICE_ANNOTATION_DESCRIPTOR); double weight = descriptorAnnot.doubleValue("weight").orElse(Weighted.DEFAULT_WEIGHT); - Set contracts = descriptorAnnot.typeValues("contracts") - .map(Set::copyOf) - .orElseGet(Set::of); + Set contracts = descriptorAnnot.typeValues("contracts") + .stream() + .flatMap(List::stream) + .map(ResolvedType::create) + .collect(Collectors.toUnmodifiableSet()); String registryType = descriptorAnnot.stringValue("registryType").orElse("core"); @@ -104,7 +119,8 @@ public void process(io.helidon.codegen.RoundContext roundContext) { generatedServiceDescriptors.add(DescriptorMetadata.create(registryType, typeInfo.typeName(), weight, - contracts)); + contracts, + Set.of())); } if (roundContext.availableAnnotations().size() == 1 && roundContext.availableAnnotations() @@ -121,10 +137,10 @@ public void process(io.helidon.codegen.RoundContext roundContext) { @Override public void processingOver(io.helidon.codegen.RoundContext roundContext) { // do processing over in each extension - extensions.forEach(RegistryCodegenExtension::processingOver); - - // if there was any type generated, write it out (will not trigger next round) - writeNewTypes(); + extensions + .stream() + .map(ExtensionInfo::extension) + .forEach(RegistryCodegenExtension::processingOver); if (!generatedServiceDescriptors.isEmpty()) { // re-check, maybe we run from a tool that does not generate anything except for the module component, @@ -135,6 +151,24 @@ public void processingOver(io.helidon.codegen.RoundContext roundContext) { } } + private static Predicate discoveryPredicate(Set typeNames, Collection packages) { + List prefixes = packages.stream() + .map(it -> it.endsWith(".*") ? it.substring(0, it.length() - 2) : it) + .toList(); + return typeName -> { + if (typeNames.contains(typeName)) { + return true; + } + String packageName = typeName.packageName(); + for (String prefix : prefixes) { + if (packageName.startsWith(prefix)) { + return true; + } + } + return false; + }; + } + private void addDescriptorsToServiceMeta() { // and write the module component Optional currentModule = ctx.module(); @@ -154,107 +188,79 @@ private void addDescriptorsToServiceMeta() { services.write(); } - private void writeNewTypes() { - // after each round, write all generated types - CodegenFiler filer = ctx.filer(); - + private void writeNewTypes(List descriptors) { + /* + This is no longer going to write the types, as it is now (correctly) delegated to + codegen round context + */ // generate all code - var descriptors = ctx.descriptors(); for (var descriptor : descriptors) { ClassCode classCode = descriptor.classCode(); - ClassModel classModel = classCode.classModel().build(); generatedServiceDescriptors.add(DescriptorMetadata.create(descriptor.registryType(), classCode.newType(), descriptor.weight(), - descriptor.contracts())); - filer.writeSourceFile(classModel, classCode.originatingElements()); - } - descriptors.clear(); - - var otherTypes = ctx.types(); - for (var classCode : otherTypes) { - ClassModel classModel = classCode.classModel().build(); - filer.writeSourceFile(classModel, classCode.originatingElements()); + descriptor.contracts(), + descriptor.factoryContracts())); } - otherTypes.clear(); } private List annotatedTypes(Collection allTypes) { List result = new ArrayList<>(); for (TypeInfo typeInfo : allTypes) { - result.add(new TypeInfoAndAnnotations(typeInfo, annotations(typeInfo))); + result.add(new TypeInfoAndAnnotations(typeInfo, TypeHierarchy.nestedAnnotations(ctx, typeInfo))); } return result; } - private RegistryRoundContext createRoundContext(List annotatedTypes, - RegistryCodegenExtension extension) { - Set extAnnots = new HashSet<>(); - Map> extAnnotToType = new HashMap<>(); - Map extTypes = new HashMap<>(); + private RegistryRoundContext createRoundContext(RoundContext roundContext, + List annotatedTypes, + ExtensionInfo extension, + List newDescriptors) { + + Set availableAnnotations = new HashSet<>(); + Map> annotationToTypes = new HashMap<>(); + Map processedTypes = new HashMap<>(); for (TypeInfoAndAnnotations annotatedType : annotatedTypes) { - for (TypeName typeName : annotatedType.annotations()) { - boolean added = false; - List validExts = this.typeToExtensions.get(typeName); - if (validExts != null) { - for (RegistryCodegenExtension validExt : validExts) { - if (validExt == extension) { - extAnnots.add(typeName); - extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(annotatedType.typeInfo()); - extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); - added = true; - } - } - } - if (!added) { - Predicate predicate = this.extensionPredicates.get(extension); - if (predicate != null && predicate.test(typeName)) { - extAnnots.add(typeName); - extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) - .add(annotatedType.typeInfo()); - extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); - } + for (TypeName annotationType : annotatedType.annotations()) { + // first check if directly supported + if (extension.supportedAnnotationsPredicate.test(annotationType) + || isMetaAnnotated(roundContext, extension, annotationType)) { + + availableAnnotations.add(annotationType); + processedTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo()); + annotationToTypes.computeIfAbsent(annotationType, k -> new ArrayList<>()) + .add(annotatedType.typeInfo()); + // annotation is meta-annotated with a supported meta-annotation, + // or we support the annotation type, or it is prefixed by the package prefix } } } + Map> metaAnnotated = new HashMap<>(); + for (TypeName typeName : extension.supportedMetaAnnotations()) { + metaAnnotated.put(typeName, Set.copyOf(roundContext.annotatedAnnotations(typeName))); + } + return new RoundContextImpl( - Set.copyOf(extAnnots), - Map.copyOf(extAnnotToType), - List.copyOf(extTypes.values())); + ctx, + roundContext, + newDescriptors::add, + Set.copyOf(availableAnnotations), + Map.copyOf(annotationToTypes), + Map.copyOf(metaAnnotated), + List.copyOf(processedTypes.values())); } - private Set annotations(TypeInfo theTypeInfo) { - Set result = new HashSet<>(); - - // on type - theTypeInfo.annotations() - .stream() - .map(Annotation::typeName) - .forEach(result::add); - - // on fields, methods etc. - theTypeInfo.elementInfo() - .stream() - .map(TypedElementInfo::annotations) - .flatMap(List::stream) - .map(Annotation::typeName) - .forEach(result::add); - - // on parameters - theTypeInfo.elementInfo() - .stream() - .map(TypedElementInfo::parameterArguments) - .flatMap(List::stream) - .map(TypedElementInfo::annotations) - .flatMap(List::stream) - .map(Annotation::typeName) - .forEach(result::add); - - return result; + private boolean isMetaAnnotated(RoundContext roundContext, ExtensionInfo extension, TypeName annotationType) { + for (TypeName typeName : extension.supportedMetaAnnotations()) { + if (roundContext.annotatedAnnotations(typeName) + .contains(annotationType)) { + return true; + } + } + return false; } private String topLevelPackage(Set typeNames) { @@ -272,4 +278,9 @@ private String topLevelPackage(Set typeNames) { private record TypeInfoAndAnnotations(TypeInfo typeInfo, Set annotations) { } + + private record ExtensionInfo(RegistryCodegenExtension extension, + Predicate supportedAnnotationsPredicate, + Set supportedMetaAnnotations) { + } } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenProvider.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenProvider.java index de39791ea3d..bcb00edda3e 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenProvider.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceRegistryCodegenProvider.java @@ -16,30 +16,60 @@ package io.helidon.service.codegen; +import java.util.List; +import java.util.ServiceLoader; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import io.helidon.codegen.CodegenContext; import io.helidon.codegen.Option; import io.helidon.codegen.spi.CodegenExtension; import io.helidon.codegen.spi.CodegenExtensionProvider; +import io.helidon.codegen.spi.CodegenProvider; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider; /** * A {@link java.util.ServiceLoader} provider implementation for {@link io.helidon.codegen.spi.CodegenExtensionProvider} * that handles Helidon Service Registry code generation. */ +@Weight(Weighted.DEFAULT_WEIGHT - 10) // we want builders to be processed first public class ServiceRegistryCodegenProvider implements CodegenExtensionProvider { - private static final Set> SUPPORTED_OPTIONS = Set.of( - ServiceOptions.AUTO_ADD_NON_CONTRACT_INTERFACES - ); + private static final List EXTENSIONS = + HelidonServiceLoader.create(ServiceLoader.load(RegistryCodegenExtensionProvider.class, + ServiceRegistryCodegenProvider.class.getClassLoader())) + .asList(); - private static final Set SUPPORTED_ANNOTATIONS = Set.of( - TypeNames.GENERATED, - ServiceCodegenTypes.SERVICE_ANNOTATION_DESCRIPTOR, - ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER - ); - private static final Set SUPPORTED_ANNOTATION_PACKAGES = Set.of(); + private static final Set> SUPPORTED_OPTIONS = + EXTENSIONS.stream() + .map(CodegenProvider::supportedOptions) + .flatMap(Set::stream) + .collect(Collectors.toUnmodifiableSet()); + + private static final Set SUPPORTED_ANNOTATIONS = + Stream.concat(EXTENSIONS.stream() + .map(RegistryCodegenExtensionProvider::supportedAnnotations) + .flatMap(Set::stream), + Stream.of(TypeNames.GENERATED, + ServiceCodegenTypes.SERVICE_ANNOTATION_DESCRIPTOR)) + .collect(Collectors.toUnmodifiableSet()); + + private static final Set SUPPORTED_ANNOTATION_PACKAGES = + EXTENSIONS.stream() + .map(RegistryCodegenExtensionProvider::supportedAnnotationPackages) + .flatMap(Set::stream) + .collect(Collectors.toUnmodifiableSet()); + + private static final Set SUPPORTED_META_ANNOTATIONS = + EXTENSIONS.stream() + .map(RegistryCodegenExtensionProvider::supportedMetaAnnotations) + .flatMap(Set::stream) + .collect(Collectors.toUnmodifiableSet()); /** * Required default constructor. @@ -65,8 +95,13 @@ public Set supportedAnnotationPackages() { return SUPPORTED_ANNOTATION_PACKAGES; } + @Override + public Set supportedMetaAnnotations() { + return SUPPORTED_META_ANNOTATIONS; + } + @Override public CodegenExtension create(CodegenContext ctx, TypeName generatorType) { - return ServiceRegistryCodegenExtension.create(ctx, generatorType); + return ServiceRegistryCodegenExtension.create(ctx, EXTENSIONS); } } diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceSuperType.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceSuperType.java new file mode 100644 index 00000000000..a875526d1f1 --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceSuperType.java @@ -0,0 +1,119 @@ +/* + * 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.codegen; + +import java.util.Objects; + +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; + +/** + * Definition of a super service type (if any). + * Only classes can have super types, and only if they directly extend a class that has a service descriptor generated. + */ +public final class ServiceSuperType { + private final TypeInfo typeInfo; + private final String descriptorType; + private final TypeName descriptorTypeName; + + private ServiceSuperType(TypeInfo typeInfo, String descriptorType, TypeName descriptoryTypeName) { + this.descriptorType = descriptorType; + this.descriptorTypeName = descriptoryTypeName; + this.typeInfo = typeInfo; + } + + /** + * Create a registry based super type. + * + * @param typeInfo type info of the super type + * @param descriptorType descriptor type (core, inject etc.) of the descriptor + * @param descriptorTypeName type name of the service descriptor of the extended type + * @return a new super type for a real super type + */ + public static ServiceSuperType create(TypeInfo typeInfo, String descriptorType, TypeName descriptorTypeName) { + Objects.requireNonNull(typeInfo); + Objects.requireNonNull(descriptorType); + Objects.requireNonNull(descriptorTypeName); + + return new ServiceSuperType(typeInfo, descriptorType, descriptorTypeName); + } + + /** + * Create a super type that represents "no supertype" (i.e. the only supertype is {@link java.lang.Object}). + * + * @return super type that is not present + */ + public static ServiceSuperType create() { + return new ServiceSuperType(null, null, null); + } + + /** + * Whether there is a super service type. + * + * @return if this service has a valid service super type + */ + public boolean present() { + return typeInfo != null; + } + + /** + * Whether there is NOT a super service type. + * + * @return if the service does not have a valid super type + */ + public boolean empty() { + return typeInfo == null; + } + + /** + * Type of the service descriptor, either {@code core} for core service registry, or other depending on supported + * types (such as {@code inject}). + * + * @return type of the service + */ + public String serviceType() { + return descriptorType == null ? "core" : descriptorType; + } + + /** + * Type information of the super service type of this service. + * + * @return type info of the super service type + * @throws java.lang.IllegalStateException if this is not a valid super type, guard with {@link #present()} + */ + public TypeInfo typeInfo() { + if (typeInfo == null) { + throw new IllegalStateException("TypeInfo is only available if a service has a valid super type, please guard" + + " with SuperServiceType#present()."); + } + return typeInfo; + } + + /** + * Type name of the service descriptor of the super service type. + * + * @return type name of the service descriptor for the super service type + * @throws java.lang.IllegalStateException if this is not a valid super type, guard with {@link #present()} + */ + public TypeName descriptorType() { + if (typeInfo == null) { + throw new IllegalStateException("Descriptor TypeName is only available if a service has a valid super type," + + " please guard with SuperServiceType#present()."); + } + return descriptorTypeName; + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/TypedElements.java b/service/codegen/src/main/java/io/helidon/service/codegen/TypedElements.java deleted file mode 100644 index f05eebab293..00000000000 --- a/service/codegen/src/main/java/io/helidon/service/codegen/TypedElements.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.codegen; - -import java.util.ArrayList; -import java.util.List; - -import io.helidon.codegen.ElementInfoPredicates; -import io.helidon.common.types.AccessModifier; -import io.helidon.common.types.ElementKind; -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 java.util.function.Predicate.not; - -final class TypedElements { - static final ElementMeta DEFAULT_CONSTRUCTOR = new ElementMeta(TypedElementInfo.builder() - .typeName(TypeNames.OBJECT) - .accessModifier(AccessModifier.PUBLIC) - .kind(ElementKind.CONSTRUCTOR) - .build()); - - private TypedElements() { - } - - static List gatherElements(TypeInfo typeInfo) { - List result = new ArrayList<>(); - - List declaredElements = typeInfo.elementInfo() - .stream() - .toList(); - - for (TypedElementInfo declaredMethod : declaredElements) { - List interfaceMethods = new ArrayList<>(); - - if (declaredMethod.kind() == ElementKind.METHOD) { - // now find the same method on any interface (if declared there) - for (TypeInfo info : typeInfo.interfaceTypeInfo()) { - info.elementInfo() - .stream() - .filter(ElementInfoPredicates::isMethod) - .filter(not(ElementInfoPredicates::isStatic)) - .filter(not(ElementInfoPredicates::isPrivate)) - .filter(it -> signatureMatches(declaredMethod, it)) - .findFirst() - .ifPresent(it -> interfaceMethods.add(new TypedElements.DeclaredElement(info, it))); - } - } - result.add(new TypedElements.ElementMeta(declaredMethod, interfaceMethods)); - } - - return result; - } - - private static boolean signatureMatches(TypedElementInfo method, TypedElementInfo interfaceMethod) { - // if the method has the same name and same parameter types, it is our candidate (return type MUST be the same, - // as otherwise this could not be compiled - if (!ElementInfoPredicates.elementName(method.elementName()).test(interfaceMethod)) { - return false; - } - List expectedParams = method.parameterArguments() - .stream() - .map(TypedElementInfo::typeName) - .toList(); - - return ElementInfoPredicates.hasParams(expectedParams).test(interfaceMethod); - } - - record ElementMeta(TypedElementInfo element, - List interfaceMethods) { - ElementMeta(TypedElementInfo element) { - this(element, List.of()); - } - } - - record DeclaredElement(TypeInfo iface, - TypedElementInfo element) { - } -} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java b/service/codegen/src/main/java/io/helidon/service/codegen/spi/RegistryCodegenExtension.java similarity index 87% rename from service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java rename to service/codegen/src/main/java/io/helidon/service/codegen/spi/RegistryCodegenExtension.java index c093f37add4..f088c1f218e 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/spi/RegistryCodegenExtension.java @@ -14,12 +14,14 @@ * limitations under the License. */ -package io.helidon.service.codegen; +package io.helidon.service.codegen.spi; + +import io.helidon.service.codegen.RegistryRoundContext; /** * Code generation extension for Helidon Service Registry. */ -interface RegistryCodegenExtension { +public interface RegistryCodegenExtension { /** * Process a single round. * diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/spi/RegistryCodegenExtensionProvider.java b/service/codegen/src/main/java/io/helidon/service/codegen/spi/RegistryCodegenExtensionProvider.java new file mode 100644 index 00000000000..d0378aec64e --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/spi/RegistryCodegenExtensionProvider.java @@ -0,0 +1,35 @@ +/* + * 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.codegen.spi; + +import io.helidon.codegen.spi.CodegenProvider; +import io.helidon.service.codegen.RegistryCodegenContext; + +/** + * A {@link java.util.ServiceLoader} provider interface for extensions of code generators for Helidon Inject. + * The difference between this extension and a general {@link io.helidon.codegen.spi.CodegenExtensionProvider} is that + * this provider has access to {@link io.helidon.service.codegen.RegistryCodegenContext}. + */ +public interface RegistryCodegenExtensionProvider extends CodegenProvider { + /** + * Create a new extension based on the context. + * + * @param codegenContext injection code generation context + * @return a new extension + */ + RegistryCodegenExtension create(RegistryCodegenContext codegenContext); +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/spi/package-info.java b/service/codegen/src/main/java/io/helidon/service/codegen/spi/package-info.java new file mode 100644 index 00000000000..251312c17a1 --- /dev/null +++ b/service/codegen/src/main/java/io/helidon/service/codegen/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * SPI for extending code generation capabilities of Helidon Inject. + */ +package io.helidon.service.codegen.spi; diff --git a/service/codegen/src/main/java/module-info.java b/service/codegen/src/main/java/module-info.java index a7ecbf9bd08..b2b67e9496a 100644 --- a/service/codegen/src/main/java/module-info.java +++ b/service/codegen/src/main/java/module-info.java @@ -14,8 +14,6 @@ * limitations under the License. */ -import io.helidon.service.codegen.ServiceRegistryCodegenProvider; - /** * Code generation for Helidon Service Registry. */ @@ -28,7 +26,12 @@ requires io.helidon.service.metadata; exports io.helidon.service.codegen; + exports io.helidon.service.codegen.spi; + + uses io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider; provides io.helidon.codegen.spi.CodegenExtensionProvider - with ServiceRegistryCodegenProvider; + with io.helidon.service.codegen.ServiceRegistryCodegenProvider; + provides io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider + with io.helidon.service.codegen.ServiceExtensionProvider; } \ No newline at end of file diff --git a/service/inject/README.md b/service/inject/README.md new file mode 100644 index 00000000000..7a8f87f72a3 --- /dev/null +++ b/service/inject/README.md @@ -0,0 +1,429 @@ +Inject Service Registry +---- + +An extension to the core service registry. + +All features are implemented in a way that can use no reflection, mostly through code generating required handling classes. + +Helidon Inject includes: + +- [Dependency Injection](#dependency-injection) +- [Lifecycle Support](#service-lifecycle) +- [Factories and Services](#factories-and-services) +- [Aspect Oriented Programming (interceptors)](#interceptors) +- [Events](events) +- [Programmatic Lookup](#programmatic-lookup) +- [Other](#other) +- [Glossary](#glossary) + +# Dependency Injection + +The basic building stone for inversion of control, dependency injection provides a mechanism to obtain an instance of a service +at runtime, from the service registry, rather than constructing service instances through a constructor or a factory method. + +When using dependency injection, we can separate the concerns of "how to create a service instance" from +"how to use the contract". The consumer of the contract is not burdened with the details of how to obtain a valid instance, +and the provider of the service is not burdened with providing an API to build/setup a service instance. +In some cases such interaction would be quite cumbersome, as we would need to carry a shared instance through constructors to +reach the correct place where we want to create a service instance. + +One of the advantages of such an approach is the capability to exchange the service that implements a contract without the need +to modify the consumers of such a contract. + +## Injection points + +In Helidon, dependency injection can be done in the following ways: + +- Through a constructor annotated with `@Injection.Inject` - each parameter is considered an injection point; this is the + recommended way of injecting dependencies (as it can be unit tested easily, and fields can be declared `private final`) +- Through field(s) annotated with `@Injection.Inject` - each field is considered an injection point; this is not recommended, as + the fields must be accessible (at least package local), and cannot be declared as `final` + +An injection point is satisfied by a service with the highest weight implementing the requested contract. + +## Services + +Services are: + +1. Java classes annotated with one of the `Injection.Scope` annotations, such as + - `@Injection.Singleton` - up to one instance exists in the service registry + - `@Injection.PerLookup` - an instance is created each time a lookup is done (injecting into an injection point is considered + a lookup as well) + - `@Injection.PerRequest` - up to one instance exists in the service registry per request (what is a request is not defined in + the injection framework itself, but it matches concepts such as HTTP request/response interaction, or consuming of a + messaging message) +2. Any class with `@Injection.Inject` annotation that does not have a scope annotation. In such a case, the + service will be `@Injection.PerLookup`. +3. Any `core` service defined for Helidon Service Registry (using annotation `Service.Provider`), the scope is `PerLookup` if the + service implements a `Supplier`, and `@Singleton` otherwise; all dependencies are considered injection points + +Only services can have Injection points. + +## Qualifiers + +Any annotation "meta-annotated" with `@Injection.Qualifier` is considered a qualifier. +Qualifier annotations can be used to "qualify" injection points and services. + +If an injection points is qualified (it has one or more qualifiers), it will only be satisfied with services that match all +the specified qualifiers. + +### Named + +One qualifier is provided out-of-the-box - the `@Injection.Named` (and `@Injection.NamedByType` which does the same thing, +only the name is the fully qualified class name of the provided class). + +Named instances are used by some feature of Helidon Inject itself. + +# Service Lifecycle + +The service registry manages lifecycle of services. + +To manage lifecycle, you can use the following annotations: + +- `@Injection.PostConstruct` - a method annotated with this annotation will be invoked after the instance is constructed and fully + injected +- `@Injection.PreDestroy` - a method annotated with this annotation will be invoked after the service is no longer used by the + registry + +The behavior depends on the scope of the bean as follows: + +- `@Injection.PerLookup` - only "post construct" lifecycle method is invoked, as we do not control the instance after is is + injected +- Any other scope - the "pre destroy" lifecycle method is invoked when the scope is deactivated (Singletons on registry shutdown + or JVM shutdown) + +# Factories and Services + +Let's consider we have a contract named `MyContract`. + +The simple case is that we have a class that implements the contract, and that is a service, such as: + +```java + +@Injection.Singleton +class MyImpl implements MyContract { +} +``` + +This means the service instance itself is an implementation of the contract, and when this service is used to satisfy an injection +point, we will get an instance of `MyImpl`. + +But such an approach is only feasible if the contract is an interface, and we are fine with doing a full implementation. +There may be cases, where this is not sufficient: + +- we need to provide an instance created by somebody else +- the provided contract is not an interface +- the provided instance may not be created at all (i.e. it is optional) + +This can be done by implementing one of the factory interfaces Helidon Inject supports: + +- `java.util.function.Supplier` - a factory that supplies a single instance (can also be `Supplier>`) +- `io.helidon.service.inject.api.Injection.ServicesFactory` - a factory that creates zero or more contract implementations +- `io.helidon.service.inject.api.Injection.InjectionPointFactory` - a factory that provides zero or more instances for each + injection point +- `io.helidon.service.inject.api.Injection.QualifiedFactory` - a factory that provides zero or more instances for a specific + qualifier and contract + +The factory interfaces above should provide enough tooling to implement any injection use case. + +# Interceptors + +Interception provides capability to intercept call to a constructor or a method (even to fields when used as injection points). + +Interception is (by default) only enabled for elements annotated with an annotation that is a `Interception.Intercepted`. +Annotation processor configuration allows for creating interception "plumbing" for any annotation, or to disable it altogether. + +Interception works "around" the invocation, so it can: + +- do something before actual invocation +- modify invocation parameters +- do something after actual invocation +- modify response +- handle exceptions + +Annotation type: `io.helidon.service.inject.api.Interception` + +Annotations: + +| Annotation class | Description | +|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Intercepted` | Marker for annotations that should trigger interception | +| `Delegate` | Marks a class as supporting interception delegation. Classes are not good candidates for delegation, as you need to create an instance that delegates to another instance, opening space for side-effects. To use a class, it must have an accessible no-arg constructor, and it should be designed not to have side-effects from construction | +| `ExternalDelegate` | Add this to a service provider that provides a class that requires delegation, if the class is not part of your current project (i.e. you cannot annotate it with `Delegate` | + +Interfaces: + +| Interface class | Description | +|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Interceptor` | A service implementing this interface, and named with the annotation type (maybe using `NamedByType`) will be used as interceptor of methods annotated with that annotation. Interceptor must call `proceed` method to handle the interception chain | + +# Events + +Events allow in-application communication between services, by providing a mechanism to emit an event, and +to create a consumer/consumers of events. + +One event can be delivered to (0..n) consumers. + +Basic terminology: + +- `Event Producer` - a service that calls an emitter (origin of an event) +- `Event Emitter` - a service that emits an event to the event system +- `Event Object` - an arbitrary object that is sent around as an event +- `Event Observer` - a service that receives events, and has a method annotated with + `io.helidon.service.inject.api.Event.Observer` +- `Qualified Event` - event published by an emitter providing a qualifier (annotation annotated with `Injection.Qualifier`) + +## Emitting Events + +Event Emitters are code generated by Helidon. To create an Event Producer, simply inject the emitter. +Event producers can be in any scope, the generated event emitter is always in `Injection.Singleton` scope. + +A simple singleton service that injects an event emitter for event object of type `MyEventObject`. + +```java + +@Injection.Singleton +class MyService { + private final Event.Emitter emitter; + + @Injection.Inject + MyService(Event.Emitter emitter) { + this.emitter = emitter; + } +} +``` + +To emit an event, you simply call `emitter.emit(myEventObjectInstance)`. +The method will return once all event observers were notified (unless they are asynchronous - see below). +In case any of the observers throws an exception, an `EventDispatchException` will be thrown with all exceptions caught added as +suppressed (i.e. we will invoke all observers, even after we catch an exception). + +Event emitters are code generated for each Event Producer, so we may end up with more than one in the system. As all of them +provide the exact same function, this is not an issue. + +_Explanation of the above statement: we cannot code generate classes into packages that do not belong to the current module, so we +always code generate the emitter to the same package as the service that needs the emitter. Even though this may duplicate code, +it is the only safe way we can do during annotation processing (where we do not have access to the classpath of the application)_ + +## Consuming Events + +An event can be consumed by declaring an observer method. +Event consumers can only be in `Injection.Singleton` or `Injection.PerLookup` scopes. The lookup is done exactly once, and all +events are delivered to the same instance for the lifetime of the service registry. + +Helidon code generates an `EventObserverRegistration` service, which is used by the event manager to gather all observers for +event handling. + +To create an event observer: + +- create an observer method, with a single parameter of the event type you want to observe +- annotate the method with `Event.Observer` + +Example: + +```java + +@Event.Observer +void event(MyEventObject eventObject) { + // do something with the event +} +``` + +## Asynchronous Events + +Events can be emitted asynchronously, and event observer can be asynchronous. +Executor service for asynchronous events can be provided via service registry, as a service that implements contract +`java.util.concurrent.ExecutorService`, and is named `io.helidon.service.inject.api.EventManager`. +If none is provided, the service will use a thread per task executor with Virtual threads, thread names will be prefixed with +`inject-event-manager-`. + +### Asynchronous Event Producer + +Rules of asynchronous event producing: + +1. Method `Event.Emitter.emitAsync(..)` returns a `CompletionStage` +2. All *synchronous* Event Consumer are submitted to an executor service, and the returned completion stage will provide either + success (the event object itself), or will provide an exception, which will have `EventDispatchException` as a cause +3. The method returns once all the event observers are submitted to the executor service (there is no guarantee that anything has + been delivered - we may have delivered 0 to n events (where n is number of synchronous observers)) +4. All *asynchronous* Event Observer are invoked outside of the returned completion stage + +### Asynchronous Observer + +Asynchronous observer methods are invoked from separate threads (through the executor service mentioned above), and their results +are ignored by the Event Emitter; if there is an exception thrown from the observer method, it is logged with `WARNING` log level +into logger named `io.helidon.service.inject.api.EventManager`. + +To declare an asynchronous observer use annotation `Event.AsyncObserver` instead of `Event.Observer`. + +Example: + +```java + +@Event.AsyncObserver +void event(MyEventObject eventObject) { + // handle event +} +``` + +## Qualified Events + +A Qualified Event is only delivered to Event Consumers that use the same qualifier. + +### Qualified Event Producer + +A qualified event can be produced with two options: + +1. The injection point of `Event.Emitter` (the constructor parameter, or field) is annotated with a qualifier annotation +2. The `Event.Emitter.emit(..)` method is called with explicit qualifier(s), note that if combined, the qualifier specified by the + injection point will always be present! + +Example (combination of both): + +```java +import io.helidon.service.inject.api.Qualifier; + +// class declaration +private static final Qualifier BLUE = Qualifier.create(Blue.class); + + @Injection.Inject + EventEmitter(@Black Event.Emitter event) { + // the event producer will implicitly have Black qualifier added + this.event = event; + } + + void emit(MyEventObject eventObject) { + // the event will be emitted with both Blue and Black qualifiers + this.event.emit(eventObject, BLUE); + } +``` + +### Qualified Event Observers + +To consume a qualified event, observer method must be annotated with the correct qualifier(s). + +Example: + +```java + +@Injection.Singleton +class EventObserver { + @Event.Observer + @Black + void event(MyEventObject eventObject) { + // handle event that is qualified with Black (and none other) + } +} +``` + +# Programmatic Lookup + +As usual with Helidon, what can be done via automation (dependency injection in this case) can also be done programmatically. + +The service registry can be used and handled "from outside" - you can create a registry instance, lookup services, call methods on +them. + +It can also be used "from inside" - you can inject an `InjectRegistry` into your services. In case this approach is done, we +cannot work around lookup costs as we can when only dependency injection is used. + +To create a registry instance: + +```java +// create an instance of a registry manager - can be configured and shut down +var registryManager = InjectRegistryManager.create(); +// get the associated service registry +var registry = registryManager.registry(); +``` + +Note that all instances are created lazily, so the registry will do "nothing" by default. If a service does something during +construction or post construction, you must lookup an instance from the registry first. + +Special registry operations: + +- `List lookupServices(Lookup lookup)` - get all service descriptors that match the lookup +- `Optional get(ServiceInfo)` - get an instance for the provided service descriptor + +The common registry operations are grouped by method name. Acceptable parameters are described below. + +Registry methods: + +- `T get(...)` - immediately get an instance of a contract from the registry; throws if implementation not available +- `Optional first(...)` - immediately get an instance of a contract from the registry; there may not be an implementation + available +- `List all(...)` - immediately get all instances of a contract from the registry; result may be empty +- `Supplier supply(...)` - get a supplier of an instance; the service may be instantiated only when `get` is called +- `Supplier> supplyFirst(...)` - get a supplier of an optional instance +- `Supplier> supplyAll(...)` - get a supplier of all instances + +Lookup parameter options: + +- `Class` - the contract we are looking for +- `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments) +- `Lookup` - a full search criteria for a registry lookup + +# Other + +## API types quick reference + +Annotation type: `io.helidon.service.inject.api.Injection` + +Annotations: + +| Annotation class | Description | +|------------------|---------------------------------------------------------------------------------------------------------------------------| +| `Inject` | Marks element as an injection point; although we prefer constructor injection, field and method injection works as well | +| `Qualifier` | Marker for annotations that are qualifiers | +| `Named` | A qualifier that provides a name | +| `NamedByType` | An equivalent of `Named`, that uses the fully qualified class name of the configured class as name | +| `Scope` | Marker for annotations that are scopes | +| `PerLookup` | Service instance is created per lookup (either for injection point, or via registry lookup) | +| `Singleton` | Singleton scope - a service registry will create zero or one instances of this service (instantiation is lazy) | +| `PerRequest` | Request scope - a service registry will create zero or one instance of this service per request scope instance | +| `RunLevel` | A "layer" in which this service should be instantiated; not executed by injection, will be used when starting application | +| `PerInstance` | Create a service instance for each instance of the configured contract available in registry (usually for named) | +| `InstanceName` | Parameter or field that will be injected with the name this service instance is created for (see `PerInstance`) | +| `Describe` | Create a descriptor for a type that is not a service itself, but an instance would be provided at scope creation time | + +Interfaces: + +| Interface class | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------| +| `ServicesFactory` | A service factory that creates zero or more qualified service instances at runtime | +| `InjectionPointFactory` | A service factory that creates values for specific injection points | +| `QualifiedFactory` | A service factory to resolve qualified injection points of any type (used for example by config value injection | +| `QualifiedInstance` | Used as a return type of some of the interfaces above, not to be implemented by users | +| `ScopeHandler` | Extension point to support additional scopes | + +## Injection point options + +An injection point may have the following forms (`Contract` stands for a contract interface, or class): + +Instance based: + +1. `Contract` - injects an instance of the contract with the highest weight from the registry +2. `Optional` - same as previous, the contract may not have an implementation available in registry +3. `List` - a list of all available instances in the registry + +Supplier based (to break cyclic dependency, and to create instances as late as possible): + +1. `Supplier` +2. `Supplier>` +3. `Supplier>` + +Service instance based (to obtain registry metadata in addition to the instance): + +1. `ServiceInstance` +2. `Optional>` +3. `List>` + +# Glossary + +| Term | Description | +|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Core Service | A class annotated with `@Service.Provider` | +| Contract | A class extended by a service, or an interface implemented by a service, can be used to lookup instances | +| Dependency | A "Core Service" constructor parameter (type must be another service or a "Contract") | +| Service | A class annotated with one of the scope annotations, or a core service | +| Factory | A "Core Service" or "Service" that implements one of the factory interfaces; Core service is a factory only if it implements a `Supplier | +| Injection Point | Field annotated with `@Injection.Inject`, or a constructor parameter of a constructor used for injection (either the only accessible constructor, or the only constructor annotated with `@Injection.Inject`) | + diff --git a/service/inject/api/pom.xml b/service/inject/api/pom.xml new file mode 100644 index 00000000000..b2e7dc72deb --- /dev/null +++ b/service/inject/api/pom.xml @@ -0,0 +1,114 @@ + + + + + + io.helidon.service.inject + helidon-service-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-inject-api + Helidon Service Inject API + + API to declare services that use injection. + + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-config + + + io.helidon.common + helidon-common-types + + + io.helidon.builder + helidon-builder-api + + + io.helidon.service + helidon-service-registry + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + + + + diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/ActivationRequestBlueprint.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/ActivationRequestBlueprint.java new file mode 100644 index 00000000000..7469ef6515e --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/ActivationRequestBlueprint.java @@ -0,0 +1,53 @@ +/* + * 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.api; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.service.inject.api.Activator.Phase; + +/** + * Request to activate a service. + */ +@Prototype.Blueprint +interface ActivationRequestBlueprint { + /** + * The phase to start activation. Typically, this should be left as the default (i.e., PENDING). + * + * @return phase to start + */ + Optional startingPhase(); + + /** + * Ultimate target phase for activation. + *

    + * Defaults to {@link Activator.Phase#ACTIVE}, unless configured otherwise (in the registry). + * + * @return phase to target + */ + Phase targetPhase(); + + /** + * Whether to throw an exception on failure to activate, or return an error activation result on activation. + * + * @return whether to throw on failure + */ + @Option.DefaultBoolean(true) + boolean throwIfError(); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/ActivationResultBlueprint.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/ActivationResultBlueprint.java new file mode 100644 index 00000000000..7f58f1bdd1f --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/ActivationResultBlueprint.java @@ -0,0 +1,78 @@ +/* + * 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.api; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.service.inject.api.Activator.Phase; + +/** + * Represents the result of a service activation or deactivation. + * + * @see Activator + **/ +@Prototype.Blueprint +interface ActivationResultBlueprint { + + /** + * The activation phase that was found at onset of the phase transition. + * + * @return the starting phase + */ + @Option.Default("INIT") + Phase startingActivationPhase(); + + /** + * The activation phase that was requested at the onset of the phase transition. + * + * @return the target, desired, ultimate phase requested + */ + @Option.Default("INIT") + Phase targetActivationPhase(); + + /** + * The activation phase we finished successfully on, or are otherwise currently in if not yet finished. + * + * @return the finishing phase + */ + Phase finishingActivationPhase(); + + /** + * Any throwable/exceptions that were observed during activation. + * + * @return any captured error + */ + Optional error(); + + /** + * Returns true if this result was successful. + * + * @return true if successful + */ + boolean success(); + + /** + * Returns true if this result was unsuccessful. + * + * @return true if unsuccessful + */ + default boolean failure() { + return !success(); + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Activator.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Activator.java new file mode 100644 index 00000000000..446ceefa489 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Activator.java @@ -0,0 +1,139 @@ +/* + * 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.api; + +import java.util.List; +import java.util.Optional; + +/** + * Activator is responsible for lifecycle management of a service instance within a scope. + * + * @param type of the instance + */ +public interface Activator { + /** + * Service descriptor of this activator. + * + * @return service descriptor + */ + InjectServiceDescriptor descriptor(); + + /** + * Get instances from this managed service. + * This method is called when we already know that this service matches the lookup, and we can safely instantiate everything. + * + * @param lookup lookup to help with narrowing down the instances + * @return empty optional if an instance is not available, supplier of qualified instances otherwise + */ + Optional>> instances(Lookup lookup); + + /** + * Activate a managed service/factory. + * + * @param activationRequest activation request + * @return the result of activation + */ + ActivationResult activate(ActivationRequest activationRequest); + + /** + * Deactivate a managed service. This will trigger any {@link io.helidon.service.registry.Service.PreDestroy} + * method on the underlying service instance. The service will reach terminal + * {@link Activator.Phase#DESTROYED} phase, regardless of result of this call. + * + * @return the result of deactivation + */ + ActivationResult deactivate(); + + /** + * Current activation phase. + * + * @return phase of this activator + */ + Phase phase(); + + /** + * Description of this activator, including the current phase. + * + * @return description of this activator + */ + String description(); + + /** + * Progression of activation of a managed service instance (each service may have more than one managed instance, depending on + * its scope). + */ + enum Phase { + /** + * Starting state before anything happens activation-wise. Service registry is aware. + * Initialization may be done here. + */ + INIT(false), + /** + * Starting to be activated. + */ + ACTIVATION_STARTING(true), + /** + * Constructing. + */ + CONSTRUCTING(true), + + /** + * Injecting (fields then methods). + */ + INJECTING(true), + + /** + * Calling any post construct method. + */ + POST_CONSTRUCTING(true), + /** + * Finishing post construct method. + */ + ACTIVATION_FINISHING(true), + /** + * Service is active. + */ + ACTIVE(true), + /** + * About to call pre-destroy. + */ + PRE_DESTROYING(true), + /** + * Destroyed (after calling any pre-destroy). + * This is a final state. + */ + DESTROYED(false); + /** + * True if this phase is eligible for deactivation/shutdown. + */ + private final boolean eligibleForDeactivation; + + Phase(boolean eligibleForDeactivation) { + this.eligibleForDeactivation = eligibleForDeactivation; + } + + /** + * Determines whether this phase passes the gate for whether deactivation (PreDestroy) can be called. + * + * @return true if this phase is eligible to be included in shutdown processing + * @see io.helidon.service.registry.ServiceRegistryManager#shutdown() + */ + public boolean eligibleForDeactivation() { + return eligibleForDeactivation; + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Configuration.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Configuration.java new file mode 100644 index 00000000000..13612d63d9c --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Configuration.java @@ -0,0 +1,99 @@ +/* + * 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.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Function; + +import io.helidon.common.config.Config; +import io.helidon.common.config.ConfigException; +import io.helidon.common.types.TypeName; + +/** + * Configuration related types used with Helidon injection. + */ +public final class Configuration { + private Configuration() { + } + + /** + * Configuration property, can be used when using Helidon declarative. + */ + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.CLASS) + @Documented + @Injection.Qualifier + public @interface Value { + /** + * Configuration key (from the config root) to find the value. + *

    + * Default configuration key is {@code .} for field injection (if supported), + * and {@code .} for constructor injection. The {@code } + * is the fully qualified class name of the type that uses this annotation. + *

    + * Note that parameter names are not retained for runtime, so the parameter name when not using + * build time processing would be generated by the JVM. + *

    + * A default value can be specified as part of the key definition, separated by {@code :} (colon) from + * the key, such as: + *

    +         *     public MyType(@Config.Value("app.greeting:Ciao") String greeting) {
    +         *     }
    +         * 
    + * This would look in configuration tree for key {@code app.greeting}, and if not found, would use + * {@code Ciao} as the default value. If default value is not defined, and the property does not exist, + * an exception would be thrown at runtime. + *

    + * Default values will have string converters applied if the type is not {@link String}. + * To provide a more complex default, such as when mapping a configuration tree to an object, use + * {@link #defaultProvider()} instead. + * + * @return configuration value key, with possible default value + */ + String value() default ""; + + /** + * A class that provides the default value + * of the type expected by the injected field/parameter, it must be accessible through service registry. + * + * @return default provider class + */ + Class> defaultProvider() default NoProvider.class; + + /** + * Default value of {@link Value#defaultProvider()}. + */ + final class NoProvider implements Function { + /** + * Type name of this class. + */ + public static final TypeName TYPE = TypeName.create(NoProvider.class); + + private NoProvider() { + } + + @Override + public Object apply(Config config) { + throw new ConfigException("This default value provider should not be used"); + } + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Event.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Event.java new file mode 100644 index 00000000000..194319e7af2 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Event.java @@ -0,0 +1,97 @@ +/* + * 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.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.concurrent.CompletionStage; + +import io.helidon.service.registry.Service; + +/** + * Injection event types. + *

    + * To publish an event, inject an instance of {@link io.helidon.service.inject.api.Event.Emitter}. + *

    + * To receive events, implement a method (at least package private) with the event object as a parameter, annotated with + * {@link io.helidon.service.inject.api.Event.Observer}. The method can have any name, must be {@code void}, + * and have a single parameter that defines the event type. + */ +public final class Event { + private Event() { + } + + /** + * A service method that is an event observer. The method MUST have a parameter that is the type of the event. + */ + @Target(ElementType.METHOD) + public @interface Observer { + } + + /** + * A service method that is an event observer. The method MUST have a parameter that is the type of the event. + * Async observers are invoked on a separate thread and will never feed information back to the + * {@link io.helidon.service.inject.api.Event.Emitter} + * (even if {@link io.helidon.service.inject.api.Event.Emitter#emitAsync(Object, Qualifier...)} is used). + */ + @Target({ElementType.METHOD, ElementType.TYPE}) + public @interface AsyncObserver { + } + + /** + * To publish an event, simply inject an instance of this type (correctly typed with your event object) into your service, + * and call {@link #emit(Object, Qualifier...)} on it when needed. + *

    + * A single service can inject more than one instance, if it wants to publish events of different types. + * The type of the event is determining which events are published (i.e. qualifiers or any other annotation are not relevant, + * only the type of the object). + * + * @param type of the event object + */ + @Service.Contract + public interface Emitter { + /** + * Emit an event. + * The method blocks until all observers are processed. + *

    + * Only observers with the same qualifiers as specified will be notified. + * + * @param eventObject event object to deliver to the observers + * @param qualifiers qualifiers (zero or more) that qualify this event instance + * @throws io.helidon.service.inject.api.EventDispatchException if any exception is encountered, the first one is the + * cause, and all others are added as a + * suppressed exception to the thrown exception + */ + void emit(T eventObject, Qualifier... qualifiers); + + /** + * Emit an event. + * The method returns immediately with a completion stage that will get completed + * when all observers are notified. If any observer throws an exception, an + * {@link io.helidon.service.inject.api.EventDispatchException} is created, all exceptions are added as suppressed to it, + * and it is thrown, so {@link java.util.concurrent.CompletionStage#exceptionally(java.util.function.Function)} + * is invoked on the returned stage. + *

    + * Only observers with the same qualifiers as specified will be notified. + * + * @param eventObject event object to deliver to the listeners + * @param qualifiers qualifiers (zero or more) that qualify this event instance + * @return completion stage to observe completion events + */ + CompletionStage emitAsync(T eventObject, Qualifier... qualifiers); + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/EventDispatchException.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/EventDispatchException.java new file mode 100644 index 00000000000..897c39c25f8 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/EventDispatchException.java @@ -0,0 +1,36 @@ +/* + * 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.api; + +import java.util.Objects; + +/** + * This exception is thrown when event dispatching fails. + */ +public class EventDispatchException extends RuntimeException { + /** + * Create an exception with the first encountered exception as the cause. + * Additional exceptions (if encountered) are added as {@link #getSuppressed()}. + * + * @param message descriptive message + * @param cause cause of the failure + */ + public EventDispatchException(String message, Throwable cause) { + super(Objects.requireNonNull(message), + Objects.requireNonNull(cause)); + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/EventManager.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/EventManager.java new file mode 100644 index 00000000000..c777121d09f --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/EventManager.java @@ -0,0 +1,76 @@ +/* + * 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.api; + +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; + +import io.helidon.common.types.ResolvedType; +import io.helidon.service.registry.Service; + +/** + * Event manager is used by generated code to manage events and listeners. + */ +@Service.Contract +public interface EventManager { + /** + * Register an event consumer. + * + * @param eventType type of event + * @param eventConsumer consumer accepting the event + * @param qualifiers qualifiers the consumer is interested in + * @param type of the event object + */ + void register(ResolvedType eventType, Consumer eventConsumer, Set qualifiers); + + /** + * Register an asynchronous event consumer. + * + * @param eventType type of event + * @param eventConsumer consumer accepting the event + * @param qualifiers qualifiers the consumer is interested in + * @param type of the event object + */ + void registerAsync(ResolvedType eventType, Consumer eventConsumer, Set qualifiers); + + /** + * Emit an event. + * + * @param eventObjectType type of event + * @param eventObject event object instance + * @param qualifiers qualifiers of the producer of the event + */ + void emit(ResolvedType eventObjectType, + Object eventObject, + Set qualifiers); + + /** + * Emit an asynchronous event. + * + * @param eventObjectType type of event + * @param eventObject event object instance + * @param qualifiers qualifiers of the producer of the event + * @param type of the event object + * @return completion stage that completes when all synchronous event observers are notified; it may end exceptionally, + * if any of these throws an exception - in such a case all exceptions are available through the cause, or suppressed + * exceptions of {@link io.helidon.service.inject.api.EventDispatchException} + */ + CompletionStage emitAsync(ResolvedType eventObjectType, + T eventObject, + Set qualifiers); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/FactoryType.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/FactoryType.java new file mode 100644 index 00000000000..b6a6c705603 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/FactoryType.java @@ -0,0 +1,53 @@ +/* + * 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.api; + +/** + * Described service factory type. + *

    + * Core services (services defined for core service registry) can be only {@link #SERVICE} or {@link #SUPPLIER}. + */ +public enum FactoryType { + /** + * This service descriptor cannot provide an instance (such as service descriptors generated from interfaces, + * where we provide instances as part of creating a scope). + */ + NONE, + /** + * Direct implementation of a service. + *

    + * This is the case when service does not implement any of the service factory interfaces, but it does + * implement at least one contract. + */ + SERVICE, + /** + * The service implements a {@link java.util.function.Supplier} of a contract. + */ + SUPPLIER, + /** + * The service implements a {@link io.helidon.service.inject.api.Injection.ServicesFactory}. + */ + SERVICES, + /** + * The service implements an {@link io.helidon.service.inject.api.Injection.InjectionPointFactory}. + */ + INJECTION_POINT, + /** + * The service implements a {@link io.helidon.service.inject.api.Injection.QualifiedFactory}. + */ + QUALIFIED +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/GeneratedInjectService.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/GeneratedInjectService.java new file mode 100644 index 00000000000..0e70bdd7a44 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/GeneratedInjectService.java @@ -0,0 +1,280 @@ +/* + * 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.api; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.common.GenericType; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeName; +import io.helidon.service.registry.Service; + +/** + * All types in this class are used from generated code for services. + */ +public final class GeneratedInjectService { + private GeneratedInjectService() { + } + + /** + * Each descriptor for s service that is implements {@link io.helidon.service.inject.api.Injection.QualifiedFactory} + * implements this interface to provide information about the qualifier it supports. + */ + public interface QualifiedFactoryDescriptor { + /** + * Type of qualifier a {@link io.helidon.service.inject.api.Injection.QualifiedFactory} provides. + * + * @return type name of the qualifier this qualified factory can provide instances for + */ + TypeName qualifierType(); + } + + /** + * Each descriptor for s service that is annotated with {@link io.helidon.service.inject.api.Injection.PerInstance} + * implements this interface to provide information about the type that drives it. + */ + public interface PerInstanceDescriptor { + /** + * Service instances may be created for instances of another service. + * If a type is created for another type, it inherits ALL qualifiers of the type that it is based on. + * + * @return create for service type + */ + TypeName createFor(); + } + + /** + * Each descriptor for a service that implements {@link io.helidon.service.inject.api.Injection.ScopeHandler} + * implements this interface to provide information about the scope it handles. + */ + public interface ScopeHandlerDescriptor { + /** + * Scope handled by the scope handler service. + * + * @return type of the scope handled (annotation) + */ + TypeName handledScope(); + } + + /** + * For event observers an observer registration is generated, so it can be picked-up by the + * {@link io.helidon.service.inject.api.EventManager} implementation at runtime. + */ + @Service.Contract + public interface EventObserverRegistration { + /** + * Register with the event manager. + * + * @param manager event manager + */ + void register(EventManager manager); + } + + /** + * Utility type to provide method to combine injection point information for inheritance support. + */ + public static final class IpSupport { + private IpSupport() { + } + + /** + * Combine dependencies from this type with dependencies from supertype. + * This is a utility for code generated types. + * + * @param myType this type's dependencies + * @param superType super type's dependencies + * @return a new list without constructor dependencies from super type + */ + public static List combineIps(List myType, List superType) { + List result = new ArrayList<>(myType); + + // always inject all fields + result.addAll(superType.stream() + .filter(it -> it.elementKind() == ElementKind.FIELD) + .toList()); + // ignore constructors, as we only need to inject constructor on the instantiated type + + // and only add methods that are not already injected on existing type + Set injectedMethods = myType.stream() + .filter(it -> it.elementKind() == ElementKind.METHOD) + .map(Ip::method) + .flatMap(Optional::stream) + .collect(Collectors.toSet()); + + result.addAll(superType.stream() + .filter(it -> it.elementKind() == ElementKind.METHOD) + .filter(it -> it.method().isPresent()) + .filter(it -> injectedMethods.add(it.method().get())) // we check presence above + .toList()); + + return List.copyOf(result); + } + } + + /** + * Intercepted wrapper for generated interception delegates. + * + * @param type of the provided contratc + */ + abstract static class InterceptionWrapper { + /** + * Wrap a qualified instance so the actual instance is correctly intercepted. + * + * @param qualifiedInstance qualified instance created by appropriate factory + * @return qualified instance with wrapped instance + */ + protected Injection.QualifiedInstance wrapQualifiedInstance(Injection.QualifiedInstance qualifiedInstance) { + return Injection.QualifiedInstance.create(wrap(qualifiedInstance.get()), qualifiedInstance.qualifiers()); + } + + /** + * Wrap the instance for interception. + * This method is code generated. + * + * @param originalInstance instance to wrap + * @return wrapped instance + */ + protected abstract T wrap(T originalInstance); + } + + /** + * Wrapper for generated Service factories that implement a {@link java.util.function.Supplier} of a service. + * + * @param type of the provided contract + */ + public abstract static class SupplierFactoryInterceptionWrapper extends InterceptionWrapper + implements Supplier { + private final Supplier delegate; + + /** + * Creates a new instance delegating service instantiation to the provided supplier. + * + * @param delegate used to obtain service instance that will be {@link #wrap(Object) wrapped} for interception + */ + protected SupplierFactoryInterceptionWrapper(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public T get() { + return wrap(delegate.get()); + } + } + + /** + * Wrapper for generated Service factories that implement a + * {@link io.helidon.service.inject.api.Injection.ServicesFactory}. + * + * @param type of the provided contract + */ + public abstract static class ServicesFactoryInterceptionWrapper extends InterceptionWrapper + implements Injection.ServicesFactory { + private final Injection.ServicesFactory delegate; + + /** + * Creates a new instance delegating service instantiation to the provided services factory. + * + * @param delegate used to obtain service instances that will be {@link #wrap(Object) wrapped} for interception + */ + protected ServicesFactoryInterceptionWrapper(Injection.ServicesFactory delegate) { + this.delegate = delegate; + } + + @Override + public List> services() { + return delegate.services() + .stream() + .map(this::wrapQualifiedInstance) + .collect(Collectors.toUnmodifiableList()); + } + } + + /** + * Wrapper for generated Service factories that implement a + * {@link io.helidon.service.inject.api.Injection.InjectionPointFactory}. + * + * @param type of the provided contract + */ + public abstract static class IpFactoryInterceptionWrapper extends InterceptionWrapper + implements Injection.InjectionPointFactory { + private final Injection.InjectionPointFactory delegate; + + /** + * Creates a new instance delegating service instantiation to the provided injection point factory. + * + * @param delegate used to obtain service instances that will be {@link #wrap(Object) wrapped} for interception + */ + protected IpFactoryInterceptionWrapper(Injection.InjectionPointFactory delegate) { + this.delegate = delegate; + } + + @Override + public List> list(Lookup lookup) { + return delegate.first(lookup) + .stream() + .map(this::wrapQualifiedInstance) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public Optional> first(Lookup lookup) { + return delegate.first(lookup) + .map(this::wrapQualifiedInstance); + } + } + + /** + * Wrapper for generated Service factories that implement a + * {@link io.helidon.service.inject.api.Injection.QualifiedFactory}. + * + * @param type of the provided contract + * @param type of the qualifier annotation + */ + public abstract static class QualifiedFactoryInterceptionWrapper extends InterceptionWrapper + implements Injection.QualifiedFactory { + private final Injection.QualifiedFactory delegate; + + /** + * Creates a new instance delegating service instantiation to the provided qualified factory. + * + * @param delegate used to obtain service instances that will be {@link #wrap(Object) wrapped} for interception + */ + protected QualifiedFactoryInterceptionWrapper(Injection.QualifiedFactory delegate) { + this.delegate = delegate; + } + + @Override + public List> list(Qualifier qualifier, Lookup lookup, GenericType type) { + return delegate.list(qualifier, lookup, type) + .stream() + .map(this::wrapQualifiedInstance) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public Optional> first(Qualifier qualifier, Lookup lookup, GenericType type) { + return delegate.first(qualifier, lookup, type) + .map(this::wrapQualifiedInstance); + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectRegistry.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectRegistry.java new file mode 100644 index 00000000000..4da62e8fc78 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectRegistry.java @@ -0,0 +1,135 @@ +/* + * 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.api; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.types.TypeName; +import io.helidon.service.registry.Service; + +/** + * Entry point to services with injection support in Helidon. + *

    + * The service registry has knowledge about all the services within your application. + *

    + * This is the full service registry with injection support. + */ +@Service.Contract +@Injection.Describe +public interface InjectRegistry extends io.helidon.service.registry.ServiceRegistry { + /** + * {@link io.helidon.service.metadata.DescriptorMetadata#registryType()} for inject services. + */ + String REGISTRY_TYPE_INJECT = "inject"; + + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(InjectRegistry.class); + + /** + * Get the first service instance matching the lookup with the expectation that there is a match available. + * + * @param lookup lookup criteria to find matching services + * @param type of the service, if you use any other than {@link java.lang.Object}, make sure + * you have configured appropriate contracts in the lookup, as we cannot infer this + * @return the best service instance matching the lookup, cast to the expected type; please use a {@code Object} as the type + * if the result may contain an unknown instance + * @throws io.helidon.service.registry.ServiceRegistryException if there is no service that could satisfy the lookup, or the + * resolution to instance failed + */ + T get(Lookup lookup); + + /** + * Get the first service instance matching the contract with the expectation that there may not be a match available. + * + * @param lookup lookup criteria to find matching services + * @param type of the service, if you use any other than {@link java.lang.Object}, make sure + * you have configured appropriate contracts in the lookup, as we cannot infer this + * @return the best service instance matching the lookup, cast to the expected type; please use a {@code Object} as the type + * if the result may contain an unknown instance + */ + Optional first(Lookup lookup); + + /** + * Get all service instances matching the lookup with the expectation that there may not be a match available. + * + * @param lookup lookup criteria to find matching services + * @param type of the service, if you use any other than {@link java.lang.Object}, make sure + * you have configured appropriate contracts in the lookup, as we cannot infer this + * @return list of services matching the criteria, may be empty if none matched, or no instances were provided + */ + List all(Lookup lookup); + + /** + * Get the first service supplier matching the lookup with the expectation that there is a match available. + * The provided {@link java.util.function.Supplier#get()} may throw an + * {@link io.helidon.service.registry.ServiceRegistryException} in case the matching service cannot provide a value (either + * because + * of scope mismatch, or because there is no available instance, and we use a runtime resolution through + * {@link io.helidon.service.inject.api.Injection.ServicesFactory}, + * {@link io.helidon.service.inject.api.Injection.InjectionPointFactory}, or similar). + * + * @param lookup lookup criteria to find matching services + * @param type of the service, if you use any other than {@link java.lang.Object}, make sure + * you have configured appropriate contracts in the lookup, as we cannot infer this + * @return the best service supplier matching the lookup, cast to the expected type; please use a {@code Object} as the type + * if the result may contain an unknown instance + * @throws io.helidon.service.registry.ServiceRegistryException if there is no service that could satisfy the lookup + */ + Supplier supply(Lookup lookup); + + /** + * Find the first service matching the lookup with the expectation that there may not be a match available. + * + * @param lookup lookup criteria to find matching services + * @param type of the service, if you use any other than {@link java.lang.Object}, make sure + * you have configured appropriate contracts in the lookup, as we cannot infer this + * @return the best service matching the lookup, cast to the expected type; please use a {@code Object} as the type + * if the result may contain an unknown instance + */ + Supplier> supplyFirst(Lookup lookup); + + /** + * Lookup a supplier of all services matching the lookup with the expectation that there may not be a match available. + * + * @param lookup lookup criteria to find matching services + * @param type of the service, if you use any other than {@link java.lang.Object}, make sure + * you have configured appropriate contracts in the lookup, as we cannot infer this + * @return supplier of list of services ordered, may be empty if there is no match + */ + Supplier> supplyAll(Lookup lookup); + + /** + * A lookup method operating on the service descriptors, rather than service instances. + * This is useful for tools that need to analyze the structure of the registry, + * for testing etc. + * The returned instances are either the actual instances registered with the registry, or an inject + * based wrapper if the service is from core registry. Use {@link InjectServiceInfo#coreInfo()} to get the actual instance + * if instance equality is required. + *

    + * The registry is optimized for look-ups based on service type and service contracts, all other + * lookups trigger a full registry scan. + * + * @param lookup lookup criteria to find matching services + * @return a list of service descriptors that match the lookup criteria + */ + List lookupServices(Lookup lookup); + +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectServiceDescriptor.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectServiceDescriptor.java new file mode 100644 index 00000000000..67efdc89e2f --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectServiceDescriptor.java @@ -0,0 +1,58 @@ +/* + * 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.api; + +import java.util.Set; + +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.ServiceDescriptor; + +/** + * A descriptor of a service. In addition to providing service metadata, this also allows instantiation + * and injection to the service instance. The descriptor is usually code generated, though it can be + * handcrafted (it must be annotated with {@link io.helidon.service.registry.Service.Descriptor} in such a case). + * + * @param type of the service this descriptor describes + */ +public interface InjectServiceDescriptor extends ServiceDescriptor, InjectServiceInfo { + /** + * Create a new service instance. + * + * @param ctx injection context with all injection points data + * @param interceptionMetadata interception metadata to use when the constructor should be intercepted + * @return a new instance, must be of the type T or a subclass + */ + // we cannot return T, as it does not allow us to correctly handle inheritance + default Object instantiate(DependencyContext ctx, InterceptionMetadata interceptionMetadata) { + throw new IllegalStateException("Cannot instantiate type " + serviceType().fqName() + ", as it is either abstract," + + " or an interface."); + } + + /** + * Inject fields and methods. + * + * @param ctx injection context + * @param interceptionMetadata interception metadata to support interception of field injection + * @param injected mutable set of already injected methods from subtypes + * @param instance instance to update + */ + default void inject(DependencyContext ctx, + InterceptionMetadata interceptionMetadata, + Set injected, + T instance) { + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectServiceInfo.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectServiceInfo.java new file mode 100644 index 00000000000..f89144d2c03 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InjectServiceInfo.java @@ -0,0 +1,86 @@ +/* + * 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.api; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Service metadata. + */ +public interface InjectServiceInfo extends io.helidon.service.registry.ServiceInfo { + /** + * List of injection points required by this service (and possibly by its supertypes). + * Each dependency is a point of injection of one instance into + * constructor, method parameter, or a field. + * + * @return required dependencies + */ + @Override + default List dependencies() { + return List.of(); + } + + /** + * Service qualifiers. + * + * @return qualifiers + */ + default Set qualifiers() { + return Set.of(); + } + + /** + * Run level of this service. + * + * @return run level + */ + default Optional runLevel() { + return Optional.empty(); + } + + /** + * Scope of this service. + * + * @return scope of the service + */ + TypeName scope(); + + /** + * What factory type is the described service. + * Inject services can be any of the types in the {@link FactoryType enum}. + * + * @return factory type + */ + default FactoryType factoryType() { + return FactoryType.SERVICE; + } + + /** + * Returns the instance of the core service descriptor. + * As we use identity, this is a required method that MUST return the singleton instance of the service descriptor. + * + * @return singleton instance of the underlying service descriptor + */ + default io.helidon.service.registry.ServiceInfo coreInfo() { + // for all injection based service descriptors this is enough + return this; + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java new file mode 100644 index 00000000000..d26291c3f97 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Injection.java @@ -0,0 +1,527 @@ +/* + * 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.api; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import io.helidon.common.GenericType; +import io.helidon.common.types.TypeName; +import io.helidon.service.registry.Service; + +/** + * Injection annotations. These annotations can extend support provided through + * {@link io.helidon.service.registry.Service} annotations for injection. + *

    + * This is the entry point for any annotation related to service definition with injection support in Helidon Service Registry. + *

    + * Explore annotations in this type to find out how to enhance your service behavior. + *

    + * Note that to utilize Helidon Inject and its service registry, you need to configure annotation processor to generate + * required source files. + */ +public final class Injection { + private Injection() { + } + + /** + * Method, constructor, or field marked with this annotation is considered as injectable, and its injection points + * will be satisfied with services from the service registry. An injection point is a field, or a single parameter. + *

    + * An injection point may expect instance of a service, or a {@link java.util.function.Supplier} of the same. + *

    + * Annotating an inaccessible component will always be marked as an error at compilation time + * (private fields, methods, constructors). + *

    + * Annotating a final field will always be marked as an error at compilation time. + *

    + * We recommend to use constructor injection, as field injection makes testing harder. + */ + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) + @Documented + public @interface Inject { + } + + /** + * Marks annotations that act as qualifiers. + *

    + * A qualifier annotation restricts the eligible service instances that can be injected into an injection point to those + * qualified by the same qualifier. + */ + @Target(ElementType.ANNOTATION_TYPE) + @Retention(RetentionPolicy.CLASS) + @Documented + public @interface Qualifier { + } + + /** + * A qualifier that can restrict injection to specifically named instances, or that qualifies services with that name. + */ + @Qualifier + @Retention(RetentionPolicy.CLASS) + @Documented + @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE}) + public @interface Named { + /** + * Type name of this annotation. + */ + TypeName TYPE = TypeName.create(Named.class); + /** + * Represents a wildcard name (i.e., matches anything). + */ + String WILDCARD_NAME = "*"; + /** + * Default name to identify a default instance. + */ + String DEFAULT_NAME = "@default"; + + /** + * The name. + * + * @return name this injection point requires, or this service provides, or a factory provides + */ + String value(); + } + + /** + * Scope annotation. + * A scope defines the cardinality of instances. This is a meta-annotation used to define that an annotation is a scope. + * Note that a single service can only have one scope annotation, and that scopes are not inheritable. + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.ANNOTATION_TYPE) + @Inherited + public @interface Scope { + } + + /** + * A partial scope that creates a new instance for each injection point/lookup. + * The "partial scope" means that the service instances are not managed. If this + * service gets injected, a new instance is created for each injection. The service is instantiated, + * post construct method (if any) is called, and then it is ignored (i.e. it never gets a pre destroy + * method invocation). + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) + @Scope + public @interface PerLookup { + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(PerLookup.class); + } + + /** + * A singleton service. + * The service registry will only contain a single instance of this service, and all injection points will be satisfied by + * the same instance. + *

    + * A singleton instance is guaranteed to have its constructor, post-construct, and pre-destroy methods invoked once within + * the lifecycle of the service registry. + *

    + * Alternative to this annotation is {@link io.helidon.service.inject.api.Injection.PerLookup} (or no annotation on a type + * that has {@link Injection.Inject} on its elements). Such a service would be injected + * every time its factory is invoked (each injection point, or on call to {@link java.util.function.Supplier#get()} if + * supplier is injected), and {@link io.helidon.service.inject.api.Injection.PerRequest} for request bound instances. + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Scope + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + public @interface Singleton { + /** + * Type name of this annotation. + */ + TypeName TYPE = TypeName.create(Singleton.class); + } + + /** + * A service with an instance per request. + * Injections to different scopes are supported, but must be through a {@link java.util.function.Supplier}, + * as we do not provide a proxy mechanism for instances. + *

    + * Request scope is not started by default. If you want to use request scope, you can add the following + * library to your classpath to add support for it: + *

    + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Scope + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD}) + public @interface PerRequest { + /** + * This interface type. + */ + TypeName TYPE = TypeName.create(PerRequest.class); + } + + /** + * This annotation is effectively the same as {@link Injection.Named} + * where the {@link Injection.Named#value()} is a {@link Class} + * name instead of a {@link String}. The name that would be used is the fully qualified name of the type. + */ + @Qualifier + @Documented + @Retention(RetentionPolicy.CLASS) + public @interface NamedByType { + /** + * Type name of this interface. + * {@link io.helidon.common.types.TypeName} is used in Helidon Inject APIs. + */ + TypeName TYPE = TypeName.create(NamedByType.class); + + /** + * The class used will function as the name. + * + * @return the class + */ + Class value(); + } + + /** + * Indicates the desired startup sequence for a service class. This is not used internally by Injection, but is available as a + * convenience to the caller in support for a specific startup sequence for service activations. + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface RunLevel { + + /** + * Represents an eager singleton that should be started at "startup". Note, however, that callers control the actual + * activation for these services, not the injection framework itself, as shown below: *
    +         * {@code
    +         * registry.all(Lookup.builder()
    +         *     .runLevel(Injection.RunLevel.STARTUP)
    +         *     .build());
    +         * }
    +         * 
    + */ + double STARTUP = 10D; + + /** + * Represents services that have the concept of "serving" something, such as webserver. + */ + double SERVER = 50D; + + /** + * Anything > 0 is left to the underlying provider implementation's discretion for meaning; this is just a default for + * something that is deemed "other than startup". + */ + double NORMAL = 100D; + + /** + * The service ranking applied when not declared explicitly. + * + * @return the startup int value, defaulting to {@link #NORMAL} + */ + double value() default NORMAL; + } + + /** + * A service that has instances created for each named instance of the service it is driven by. + * The instance created will have the same {@link Injection.Named} qualifier as the + * driving instance (in addition to all qualifiers defined on this service). + *

    + * There are a few restrictions on this type of services: + *

      + *
    • The service MUST NOT implement {@link java.util.function.Supplier}
    • + *
    • The service MUST NOT implement {@link io.helidon.service.inject.api.Injection.InjectionPointFactory}
    • + *
    • The service MUST NOT implement {@link io.helidon.service.inject.api.Injection.ServicesFactory}
    • + *
    • All types that inherit from this service will also inherit the driven by
    • + *
    • There MAY be an injection point of the type defined in {@link #value()}, without any qualifiers - + * this injection point will be satisfied by the driving instance
    • + *
    • There MAY be a {@link String} injection point qualified with + * {@link io.helidon.service.inject.api.Injection.InstanceName} - this injection point will be satisfied by the + * name of the driving instance
    • + *
    + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface PerInstance { + /** + * The service type driving this service. If the service provides more than one instance, + * the instances MUST be {@link Injection.Named}. + * + * @return type of the service driving instances of this service + */ + Class value(); + } + + /** + * For types that are {@link io.helidon.service.inject.api.Injection.PerInstance}, an injection point (field, parameter) can + * be annotated with this annotation to receive the name qualifier associated with this instance. + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.PARAMETER, ElementType.FIELD}) + @Qualifier + public @interface InstanceName { + /** + * Type name of this interface. + * {@link io.helidon.common.types.TypeName} is used in Helidon Inject APIs. + */ + TypeName TYPE = TypeName.create(InstanceName.class); + } + + /** + * Describe the annotated type. This will generate a service descriptor that cannot create an instance. + * This is useful for scoped instances that are provided when the scope is activated. + *

    + * This annotation will ignore type hierarchy (the descriptor will never have a super type). + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface Describe { + /** + * Customize the scope to use, defaults to {@link io.helidon.service.inject.api.Injection.Singleton}. + * + * @return scope to use for the generated service descriptor + */ + Class value() default Singleton.class; + } + + /** + * 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. + * + * @param type of the provided services + */ + public interface ServicesFactory { + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(ServicesFactory.class); + + /** + * List of service instances. + * Each instance may have a different set of qualifiers. + *

    + * The following is inherited from this factory: + *

      + *
    • Set of contracts, except for {@link io.helidon.service.inject.api.Injection.ServicesFactory}
    • + *
    • Scope
    • + *
    • Run level
    • + *
    • Weight
    • + *
    + * + * @return qualified suppliers of service instances + */ + List> services(); + } + + /** + * Provides ability to contextualize the injected service by the target receiver of the injection point dynamically + * at runtime. This API will provide service instances of type {@code T}. + *

    + * The ordering of services, and the preferred service itself, is determined by the service registry implementation. + *

    + * The service registry does not make any assumptions about qualifiers of the instances being created, though they should + * be either the same as the injection point factory itself, or a subset of it, so the service can be discovered through + * one of the lookup methods (i.e. the injection point factory may be annotated with a + * {@link Named} with {@link Named#WILDCARD_NAME} + * value, and each instance provided may use a more specific name qualifier). + * + * @param the type that the factory produces + */ + public interface InjectionPointFactory { + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(InjectionPointFactory.class); + + /** + * Get (or create) an instance of this service type for the given injection point context. This is logically the same + * as using the first element of the result from calling {@link #list(io.helidon.service.inject.api.Lookup)}. + * + * @param lookup the service query + * @return the best service instance matching the criteria, if any matched, with qualifiers (if any) + */ + Optional> first(Lookup lookup); + + /** + * Get (or create) a list of instances matching the criteria for the given injection point context. + * + * @param lookup the service query + * @return the service instances matching criteria for the lookup in order of weight, or empty if none matching + */ + default List> list(Lookup lookup) { + return first(lookup).map(List::of).orElseGet(List::of); + } + } + + /** + * A factory to resolve qualified injection points of any type. + *

    + * As compared to {@link io.helidon.service.inject.api.Injection.InjectionPointFactory}, this type is capable of resolving ANY injection + * point as long as it is annotated by the qualifier. The contract of the injection point depends on how the implementation + * service declares the type parameters of this interface. If you use any type other than {@link java.lang.Object}, that will + * be the only supported contract, otherwise any type is expected to be supported. + *

    + * A good practice is to create an accompanying codegen extension that validates injection points at build time. + * + * @param type of the provided instance, the special case is {@link java.lang.Object} - if used, we consider this + * factory to be capable of handling ANY type, and will allow injection points with any type as long as it is + * qualified by the qualifier + * @param type of qualifier supported by this factory + */ + public interface QualifiedFactory { + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(QualifiedFactory.class); + + /** + * Get the first instance (if any) matching the qualifier and type. + * + * @param qualifier the qualifier this type supports (same type as the {@code A} type this type implements) + * @param lookup full lookup used to obtain the value, may contain the actual injection point + * @param type type to be injected (or type requested) + * @return the qualified instance matching the request, or an empty optional if none match + */ + Optional> first(io.helidon.service.inject.api.Qualifier qualifier, + Lookup lookup, + GenericType type); + + /** + * Get all instances matching the qualifier and type. + * + * @param qualifier the qualifier this type supports (same type as the {@code A} type this type implements) + * @param lookup full lookup used to obtain the value, may contain the actual injection point + * @param type type to be injected (or type requested) + * @return the qualified instance matching the request, or an empty optional if none match + */ + default List> list(io.helidon.service.inject.api.Qualifier qualifier, + Lookup lookup, + GenericType type) { + return first(qualifier, lookup, type) + .map(List::of) + .orElseGet(List::of); + } + } + + /** + * An instance with its qualifiers. + * Some services are allowed to create more than one instance, and there may be a need + * to use different qualifiers than the factory service uses. + * + * @param type of instance, as provided by the service + * @see io.helidon.service.inject.api.Injection.ServicesFactory + */ + public interface QualifiedInstance extends Supplier { + /** + * Create a new qualified instance. + * + * @param instance the instance + * @param qualifiers qualifiers to use + * @param type of the instance + * @return a new qualified instance + */ + static QualifiedInstance create(T instance, io.helidon.service.inject.api.Qualifier... qualifiers) { + return new QualifiedInstanceImpl<>(instance, Set.of(qualifiers)); + } + + /** + * Create a new qualified instance. + * + * @param instance the instance + * @param qualifiers qualifiers to use + * @param type of the instance + * @return a new qualified instance + */ + static QualifiedInstance create(T instance, Set qualifiers) { + return new QualifiedInstanceImpl<>(instance, qualifiers); + } + + /** + * Get the instance that the registry manages (or an instance that is unmanaged, if the provider is in + * {@link io.helidon.service.inject.api.Injection.PerLookup}, or if the instance is created by a factory). + * The instance must be guaranteed to be constructed and if managed by the registry, and activation scope is not limited, + * then injected as well. + * + * @return instance + */ + @Override + T get(); + + /** + * Qualifiers of the instance. + * + * @return qualifiers of the service instance + */ + Set qualifiers(); + } + + /** + * Extension point for the service registry to support new scopes. + *

    + * Implementation must be qualified with the fully qualified name of the corresponding scope annotation class. + * + * @see io.helidon.service.inject.api.Injection.Named + * @see io.helidon.service.inject.api.Injection.NamedByType + */ + @Service.Contract + public interface ScopeHandler { + /** + * Type name of this interface. + * Service registry uses {@link io.helidon.common.types.TypeName} in its APIs. + */ + TypeName TYPE = TypeName.create(ScopeHandler.class); + + /** + * Get the current scope if available. + * + * @return current scope instance, or empty if the scope is not active + */ + Optional currentScope(); + + + /** + * Activate the given scope. + * + * @param scope scope to activate + */ + default void activate(io.helidon.service.inject.api.Scope scope) { + scope.registry().activate(); + } + + /** + * De-activate the given scope. + * + * @param scope scope to de-activate + */ + default void deactivate(io.helidon.service.inject.api.Scope scope) { + scope.registry().deactivate(); + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InstanceName__ServiceDescriptor.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InstanceName__ServiceDescriptor.java new file mode 100644 index 00000000000..12e6d8a0844 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InstanceName__ServiceDescriptor.java @@ -0,0 +1,69 @@ +/* + * 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.api; + +import java.util.Set; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +/** + * Service descriptor to enable injection of String name of a {@link io.helidon.service.inject.api.Injection.PerInstance} + * service (using qualifier {@link io.helidon.service.inject.api.Injection.InstanceName}). + *

    + * Not intended for direct use by users, implementation detail of the service registry, must be public, + * as it may be used in generated binding. + */ +@SuppressWarnings({"checkstyle:TypeName"}) // matches pattern of generated descriptors +public class InstanceName__ServiceDescriptor implements InjectServiceDescriptor { + /** + * Singleton instance to be referenced when building bindings. + */ + public static final InstanceName__ServiceDescriptor INSTANCE = new InstanceName__ServiceDescriptor(); + + private static final TypeName INFO_TYPE = TypeName.create(InstanceName__ServiceDescriptor.class); + private static final Set CONTRACTS = Set.of(ResolvedType.create(TypeNames.STRING)); + + private InstanceName__ServiceDescriptor() { + } + + @Override + public TypeName serviceType() { + return INFO_TYPE; + } + + @Override + public TypeName descriptorType() { + return INFO_TYPE; + } + + @Override + public Set contracts() { + return CONTRACTS; + } + + @Override + public TypeName scope() { + return Injection.Singleton.TYPE; + } + + @Override + public FactoryType factoryType() { + return FactoryType.NONE; + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Interception.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Interception.java new file mode 100644 index 00000000000..6a524b83285 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Interception.java @@ -0,0 +1,135 @@ +/* + * 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.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.registry.Service; + +/** + * Interception annotations and types. + * This is the entry point for any annotation and type related to interception in Helidon Inject. + */ +public final class Interception { + private Interception() { + } + + /** + * Meta-annotation for an annotation that will trigger services annotated with it to become intercepted. + * This will intercept any method in an annotated type, or an annotated method. + * + * @see Interception.Interceptor + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.ANNOTATION_TYPE) + @Inherited + public @interface Intercepted { + } + + /** + * Use this annotation to mark a class ready for interception delegation. + * The delegates are code generated automatically if a service factory (such as a {@link java.util.function.Supplier}) + * provides an instance of a class (or provides an interface implementation) that has methods + * annotated with interception trigger(s). + *

    + * Classes are by default not good candidates for interception, so they MUST be annotated either with + * this annotation, or referenced via {@link io.helidon.service.inject.api.Interception.ExternalDelegate}. + *

    + * Implementing a delegate for a class introduces several problems, the biggest one being + * construction side-effects. + *

    + * If you want delegation for classes to support interception: + *

    + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface Delegate { + } + + /** + * Use this annotation to mark an external class ready for interception delegation. + * This annotations must be added to the service factory (such as a {@link java.util.function.Supplier}) + * that provides an instance of a class. + *

    + * If the factory provides an interface, this annotation is not needed, as interfaces are safe to delegate. + * + * @see io.helidon.service.inject.api.Interception.Delegate + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface ExternalDelegate { + /** + * Type provided by this service factory that is a class and should support interception. + * + * @return type that should be intercepted, see {@link io.helidon.service.inject.api.Interception.Delegate} + */ + Class value(); + } + + /** + * Implementors of this contract must be {@link io.helidon.service.inject.api.Injection.Named} + * according to the {@link io.helidon.service.inject.api.Interception.Intercepted} annotation they support. + */ + @Service.Contract + public interface Interceptor { + + /** + * Called during interception of the target V. The implementation typically should finish with the call to + * {@link Interception.Interceptor.Chain#proceed}. + * + * @param ctx the invocation context + * @param chain the chain to call proceed on + * @param args the arguments to the call + * @param the return value type (or {@link Void} for void method elements) + * @return the return value to the caller + * @throws Exception if there are any checked exceptions thrown by the underlying method, or any runtime exception thrown + */ + V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception; + + /** + * Represents the next in line for interception, terminating with a call to the service instance. + * + * @param the return value + */ + interface Chain { + /** + * Call the next interceptor in line, or finish with the call to the service being intercepted. + * Note that that arguments are passed by reference to each interceptor ultimately leading up to the final + * call to the underlying intercepted target. Callers can mutate the arguments passed directly on the provided array + * instance. + * + * @param args the arguments passed + * @return the result of the call + * @throws Exception may throw any checked exceptions thrown by the underlying method, or any runtime exception + * thrown + */ + V proceed(Object[] args) throws Exception; + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionContextBlueprint.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionContextBlueprint.java new file mode 100644 index 00000000000..05a24115879 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionContextBlueprint.java @@ -0,0 +1,61 @@ +/* + * 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.api; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypedElementInfo; + +/** + * Invocation context provides metadata about the invoked element to an interceptor. + * Used by {@link io.helidon.service.inject.api.Interception.Interceptor}. + */ +@Prototype.Blueprint +interface InterceptionContextBlueprint { + /** + * The service instance being intercepted. + * This always returns the underlying instance. + * + * @return instance being intercepted, or empty optional if the intercepted method is not done on an instance + * (i.e. a constructor interception) + */ + Optional serviceInstance(); + + /** + * The service being intercepted. + * + * @return the service being intercepted + */ + InjectServiceInfo serviceInfo(); + + /** + * Annotations on the enclosing type. + * + * @return the annotations on the enclosing type + */ + List typeAnnotations(); + + /** + * The element info represents the method, field, or the constructor being invoked. + * + * @return the element info of element being intercepted + */ + TypedElementInfo elementInfo(); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionException.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionException.java new file mode 100644 index 00000000000..b16618b9b5d --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionException.java @@ -0,0 +1,68 @@ +/* + * 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.api; + +import io.helidon.service.registry.ServiceRegistryException; + +/** + * Wraps any checked exceptions that are thrown during the {@link io.helidon.service.inject.api.Interception.Interceptor} + * invocations. + */ +public class InterceptionException extends ServiceRegistryException { + + /** + * Tracks whether the target being intercepted was called once successfully - meaning that the target was called and it + * did not result in any exception being thrown. + */ + private final boolean targetWasCalled; + + /** + * Constructor. + * + * @param msg the message + * @param targetWasCalled set to true if the target of interception was ultimately called successfully + */ + public InterceptionException(String msg, + boolean targetWasCalled) { + super(msg); + this.targetWasCalled = targetWasCalled; + } + + /** + * Constructor. + * + * @param msg the message + * @param cause the root cause + * @param targetWasCalled set to true if the target of interception was ultimately called successfully + */ + public InterceptionException(String msg, + Throwable cause, + boolean targetWasCalled) { + super(msg, cause); + this.targetWasCalled = targetWasCalled; + } + + /** + * Returns true if the final target of interception was ultimately called. + * + * @return if the target being intercepted was ultimately called successfully + */ + public boolean targetWasCalled() { + return targetWasCalled; + } + +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionInvoker.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionInvoker.java new file mode 100644 index 00000000000..a829c9363ea --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionInvoker.java @@ -0,0 +1,35 @@ +/* + * 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.api; + +/** + * Invocation of an element that has parameters, and may throw checked exceptions. + * This type is used to handle intercepted methods, constructors, and field injections. + * + * @param type of the result of the invocation + */ +@FunctionalInterface +public interface InterceptionInvoker { + /** + * Invoke the element. + * + * @param parameters to pass to the element + * @return result of the invocation + * @throws Exception any exception that may be required by the invoked element + */ + T invoke(Object... parameters) throws Exception; +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionMetadata.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionMetadata.java new file mode 100644 index 00000000000..f5cc8bad771 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/InterceptionMetadata.java @@ -0,0 +1,82 @@ +/* + * 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.api; + +import java.util.List; +import java.util.Set; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypedElementInfo; + +/** + * Provides a service descriptor, or an intercepted instance with information + * whether to, and how to intercept elements. + *

    + * Used (mostly) by generated code (passed as a parameter to + * {@link + * InjectServiceDescriptor#inject(io.helidon.service.registry.DependencyContext, + * InterceptionMetadata, java.util.Set, Object)}, and + * {@link + * InjectServiceDescriptor#instantiate(io.helidon.service.registry.DependencyContext, + * InterceptionMetadata)}). + */ +@Injection.Describe +public interface InterceptionMetadata { + /** + * Create an invoker that handles interception if needed, for constructors. + * + * @param descriptor metadata of the service being intercepted + * @param typeQualifiers qualifiers on the type + * @param typeAnnotations annotations on the type + * @param element element being intercepted + * @param targetInvoker invoker of the element + * @param checkedExceptions expected checked exceptions that can be thrown by the invoker + * @param type of the result of the invoker + * @return an invoker that handles interception if enabled and if there are matching interceptors, any checkedException + * will + * be re-thrown, any runtime exception will be re-thrown + */ + InterceptionInvoker createInvoker(InjectServiceInfo descriptor, + Set typeQualifiers, + List typeAnnotations, + TypedElementInfo element, + InterceptionInvoker targetInvoker, + Set> checkedExceptions); + + /** + * Create an invoker that handles interception if needed. + * + * @param serviceInstance instance of the service that is being intercepted + * @param descriptor metadata of the service being intercepted + * @param typeQualifiers qualifiers on the type + * @param typeAnnotations annotations on the type + * @param element element being intercepted + * @param targetInvoker invoker of the element + * @param checkedExceptions expected checked exceptions that can be thrown by the invoker + * @param type of the result of the invoker + * @return an invoker that handles interception if enabled and if there are matching interceptors, any checkedException + * will + * be re-thrown, any runtime exception will be re-thrown + */ + InterceptionInvoker createInvoker(Object serviceInstance, + InjectServiceInfo descriptor, + Set typeQualifiers, + List typeAnnotations, + TypedElementInfo element, + InterceptionInvoker targetInvoker, + Set> checkedExceptions); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/IpBlueprint.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/IpBlueprint.java new file mode 100644 index 00000000000..6bbaadfc218 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/IpBlueprint.java @@ -0,0 +1,82 @@ +/* + * 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.api; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.service.registry.Dependency; + +/** + * Unique identification, and metadata of an injection point. + */ +@Prototype.Blueprint +@Prototype.CustomMethods(IpSupport.CustomMethods.class) +interface IpBlueprint extends Dependency { + /** + * Kind of element we inject into (constructor, field, method). + * + * @return element kind (for parameters, the containing element) + */ + @Option.Default("CONSTRUCTOR") + ElementKind elementKind(); + + /** + * The qualifier type annotations on this element. + * + * @return the qualifier type annotations on this element + */ + @Option.Singular + @Option.Redundant(stringValue = false) + // kind + service type + name is a unique identification already + Set qualifiers(); + + /** + * The access modifier on the injection point/receiver. + * Defaults to {@link io.helidon.common.types.AccessModifier#PACKAGE_PRIVATE}. + * + * @return the access + */ + @Option.Default("PACKAGE_PRIVATE") + @Option.Redundant + // kind + service type + name is a unique identification already + AccessModifier access(); + + /** + * The annotations on this element. + * + * @return the annotations on this element + */ + @Option.Singular + @Option.Redundant + // kind + service type + name is a unique identification already + Set annotations(); + + /** + * Top level method that declares this method. + * This is to provide information about overridden methods, as we should only inject such methods once. + * + * @return unique identification of a declaring method + */ + @Option.Redundant(stringValue = false) + Optional method(); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/IpSupport.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/IpSupport.java new file mode 100644 index 00000000000..5bc90079794 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/IpSupport.java @@ -0,0 +1,49 @@ +/* + * 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.api; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.types.ElementKind; +import io.helidon.service.registry.Dependency; + +final class IpSupport { + private IpSupport() { + } + + final class CustomMethods { + private CustomMethods() { + } + + /** + * Return the dependency if it is an instance of {@link io.helidon.service.inject.api.Ip}, + * or create an Ip that is equivalent to the dependency. + * + * @param dependency dependency to convert to injection point + * @return injection point + */ + @Prototype.FactoryMethod + static Ip create(Dependency dependency) { + if (dependency instanceof Ip ip) { + return ip; + } + return Ip.builder() + .from(dependency) + .elementKind(ElementKind.CONSTRUCTOR) + .build(); + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupBlueprint.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupBlueprint.java new file mode 100644 index 00000000000..925122ff909 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupBlueprint.java @@ -0,0 +1,263 @@ +/* + * 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.api; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; + +/** + * Lookup criteria to discover services, mostly used when interacting with a service registry. + */ +@Prototype.Blueprint(createEmptyPublic = false) +@Prototype.CustomMethods(LookupSupport.CustomMethods.class) +interface LookupBlueprint { + + /** + * The managed service implementation type name. + * + * @return the service type name + */ + Optional serviceType(); + + /** + * The managed service assigned Scope. If empty, any scope is matched. + * If more than one value, any service in one of these scopes is matched. + * + * @return the service scope type name + */ + @Option.Singular + Set scopes(); + + /** + * The managed service assigned Qualifier's. + * + * @return the service qualifiers + */ + @Option.Singular + Set qualifiers(); + + /** + * The managed services advertised types (i.e., typically its interfaces, can be through + * {@link io.helidon.service.registry.Service.ExternalContracts}). + * + * @return the service contracts implemented + */ + @Option.Singular + Set contracts(); + + /** + * A single {@link io.helidon.common.GenericType} can be defined if the lookup should also honor + * {@link io.helidon.service.inject.api.Injection.QualifiedFactory} services that can handle any type. + * This would be the target type to convert to. If not specified, Object will be used. + * + * @return generic type of the contract, if only one contract is desired + */ + @Option.Decorator(LookupSupport.GenericTypeDecorator.class) + Optional> contractType(); + + /** + * The optional {@link Injection.RunLevel} ascribed to the service. + * + * @return the service's run level + */ + Optional runLevel(); + + /** + * Weight that was declared on the type itself. + * + * @return the declared weight + */ + Optional weight(); + + /** + * Whether to include abstract type service descriptors. + * + * @return whether to include abstract classes and interfaces + */ + @Option.DefaultBoolean(false) + boolean includeAbstract(); + + /** + * Optionally, the injection point search applies to. + * There are some service factories (such as + * {@link io.helidon.service.inject.api.Injection.InjectionPointFactory}) that + * provide instances for a specific injection point. + * Such factories may require an injection point to be present, and may fail otherwise. + *

    + * Injection points of each service are generated as public constants on their respective service descriptors. + * + * @return the optional injection point context info + */ + @Option.Decorator(LookupSupport.IpDecorator.class) + Optional injectionPoint(); + + /** + * If configured, the lookup will return service factories of the + * chosen types. + * If no factory types are defined, service instances are returned. + *

    + * Otherwise only service factories of the chosen types are returned, as follows: + *

      + *
    • {@link FactoryType#SERVICE} - only services that directly implement the + * contract
    • + *
    • {@link FactoryType#SUPPLIER} - only that are {@link java.util.function.Supplier} + * of the contract
    • + *
    • {@link FactoryType#QUALIFIED} - services that are + * {@link io.helidon.service.inject.api.Injection.QualifiedFactory} of the contract
    • + *
    • {@link FactoryType#SERVICES} - services that are + * {@link io.helidon.service.inject.api.Injection.ServicesFactory} of the contract
    • + *
    • {@link FactoryType#INJECTION_POINT} - services that are + * {@link io.helidon.service.inject.api.Injection.InjectionPointFactory} of the contract
    • + *
    • {@link FactoryType#NONE} - this has no effect and will not modify the lookup
    • + *
    + * + * @return desired factory types + */ + @Option.Singular + Set factoryTypes(); + + /** + * Determines whether this lookup matches the criteria for injection. + * Matches is a looser form of equality check than {@code equals()}. If a service matches criteria + * it is generally assumed to be viable for assignability. + * + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + */ + default boolean matches(Lookup criteria) { + return matchesContracts(criteria) + && matchesAbstract(includeAbstract(), criteria.includeAbstract()) + && matchesTypes(scopes(), criteria.scopes()) + && Qualifiers.matchesQualifiers(qualifiers(), criteria.qualifiers()) + && matchesOptionals(runLevel(), criteria.runLevel()); + } + + /** + * Determines whether this service info criteria matches the service descriptor. + * Matches is a looser form of equality check than {@code equals()}. If a service matches criteria + * it is generally assumed to be viable for assignability. + * + * @param serviceInfo to compare with + * @return true if this criteria matches the service descriptor + */ + default boolean matches(InjectServiceInfo serviceInfo) { + if (this == LookupSupport.CustomMethods.EMPTY) { + return !serviceInfo.isAbstract(); + } + + boolean matches = matches(serviceInfo.serviceType(), this.serviceType()); + if (matches && this.serviceType().isEmpty()) { + matches = serviceInfo.contracts().containsAll(this.contracts()) + || this.contracts().contains(ResolvedType.create(serviceInfo.serviceType())); + } + return matches + && matchesProviderTypes(factoryTypes(), serviceInfo.factoryType()) + && matchesAbstract(includeAbstract(), serviceInfo.isAbstract()) + && (this.scopes().isEmpty() || this.scopes().contains(serviceInfo.scope())) + && Qualifiers.matchesQualifiers(serviceInfo.qualifiers(), this.qualifiers()) + && matchesWeight(serviceInfo, this) + && matchesOptionals(serviceInfo.runLevel(), this.runLevel()); + } + + /** + * Determines whether the provided criteria match just the contracts portion of the provided criteria. Note that + * it is expected any external contracts have been consolidated into the regular contract section. + * + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance from only the contracts point of view + */ + default boolean matchesContracts(Lookup criteria) { + if (criteria == LookupSupport.CustomMethods.EMPTY) { + return true; + } + + boolean matches = matchesOptionals(serviceType(), criteria.serviceType()); + if (matches && criteria.serviceType().isEmpty()) { + matches = contracts().containsAll(criteria.contracts()); + } + return matches; + } + + /** + * Determines whether the provided qualifiers are matched by this lookup criteria. + * + * @param qualifiers qualifiers of a service + * @return whether this lookup matches those qualifiers + */ + default boolean matchesQualifiers(Set qualifiers) { + return Qualifiers.matchesQualifiers(qualifiers, qualifiers()); + } + + private static boolean matchesWeight(InjectServiceInfo src, + LookupBlueprint criteria) { + if (criteria.weight().isEmpty()) { + return true; + } + + Double srcWeight = src.weight(); + return (srcWeight.compareTo(criteria.weight().get()) <= 0); + } + + private static boolean matches(Object src, + Optional criteria) { + if (criteria.isEmpty()) { + return true; + } + return Objects.equals(src, criteria.get()); + } + + private boolean matchesProviderTypes(Set providerTypes, FactoryType providerType) { + if (providerTypes.isEmpty() || (providerTypes.size() == 1 && providerTypes.contains(FactoryType.NONE))) { + return true; + } + return providerTypes.contains(providerType); + } + + private boolean matchesTypes(Set scopes, Set criteria) { + if (criteria.isEmpty()) { + return true; + } + for (TypeName scope : scopes) { + if (criteria.contains(scope)) { + return true; + } + } + return false; + } + + private boolean matchesOptionals(Optional src, Optional criteria) { + if (criteria.isEmpty()) { + return true; + } + return src.map(it -> Objects.equals(it, criteria.get())) + .orElse(false); + } + + private boolean matchesAbstract(boolean criteriaAbstract, boolean isAbstract) { + if (criteriaAbstract) { + return true; + } + return !isAbstract; + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java new file mode 100644 index 00000000000..6f49a8ade71 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/LookupSupport.java @@ -0,0 +1,173 @@ +/* + * 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.api; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.registry.Dependency; + +final class LookupSupport { + private LookupSupport() { + } + + static final class CustomMethods { + /** + * Empty lookup matches anything and everything except for abstract types. + */ + @Prototype.Constant + static final Lookup EMPTY = createEmpty(); + + private CustomMethods() { + } + + /** + * Create service lookup from this injection point information. + * + * @param injectionPoint injection point to create lookup for + * @return lookup to match injection point + */ + @Prototype.FactoryMethod + static Lookup create(Ip injectionPoint) { + return Lookup.builder() + .injectionPoint(injectionPoint) + .build(); + } + + /** + * Create lookup from a specific dependency. + * + * @param dependency dependency to create lookup for + * @return lookup for the dependency + */ + @Prototype.FactoryMethod + static Lookup create(Dependency dependency) { + if (dependency instanceof Ip ip) { + return create(ip); + } + return Lookup.builder() + .injectionPoint(Ip.builder() + .from(dependency) + .elementKind(ElementKind.CONSTRUCTOR) + .build()) + .build(); + } + + /** + * Create service lookup from a contract type. + * + * @param contract a single contract to base the lookup on + * @return lookup for matching services + */ + @Prototype.FactoryMethod + static Lookup create(Class contract) { + return Lookup.builder() + .addContract(contract) + .build(); + } + + /** + * Create service lookup from a contract type. + * + * @param contract a single contract to base the lookup on + * @return lookup for matching services + */ + @Prototype.FactoryMethod + static Lookup create(TypeName contract) { + return Lookup.builder() + .addContract(ResolvedType.create(contract)) + .build(); + } + + /** + * The managed services advertised types (i.e., typically its interfaces). + * + * @param builder builder instance + * @param contract the service contracts implemented + * @see Lookup#contracts() + */ + @Prototype.BuilderMethod + static void addContract(Lookup.BuilderBase builder, Class contract) { + builder.addContract(ResolvedType.create(contract)); + } + + /** + * The managed service implementation type. + * + * @param builder builder instance + * @param contract the service type + */ + @Prototype.BuilderMethod + static void serviceType(Lookup.BuilderBase builder, Class contract) { + builder.serviceType(TypeName.create(contract)); + } + + private static Lookup createEmpty() { + return Lookup.builder().build(); + } + } + + static final class IpDecorator implements Prototype.OptionDecorator, Optional> { + @Override + public void decorate(Lookup.BuilderBase builder, Optional injectionPoint) { + if (injectionPoint.isPresent()) { + Ip value = injectionPoint.get(); + builder.qualifiers(value.qualifiers()) + .addContract(ResolvedType.create(value.contract())); + + if (!GenericType.OBJECT.equals(value.contractType())) { + builder.contractType(value.contractType()); + } + } else { + builder.injectionPoint().ifPresent(existing -> { + // clear if contained only IP stuff + boolean shouldClear = true; + if (!builder.qualifiers().equals(existing.qualifiers())) { + shouldClear = false; + + } + if (!(builder.contracts().contains(ResolvedType.create(existing.contract())) + && builder.contracts().size() == 1)) { + shouldClear = false; + } + + if (shouldClear) { + builder.qualifiers(Set.of()); + builder.contracts(Set.of()); + builder.clearContractType(); + } + }); + } + + } + } + + static class GenericTypeDecorator implements Prototype.OptionDecorator, Optional>> { + @Override + public void decorate(Lookup.BuilderBase builder, Optional> optionValue) { + if (optionValue.isEmpty()) { + return; + } + builder.addContract(ResolvedType.create(optionValue.get())); + } + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceMetadata.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifiedInstanceImpl.java similarity index 71% rename from service/registry/src/main/java/io/helidon/service/registry/ServiceMetadata.java rename to service/inject/api/src/main/java/io/helidon/service/inject/api/QualifiedInstanceImpl.java index b77781742bf..4d37aeb76a2 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceMetadata.java +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifiedInstanceImpl.java @@ -14,17 +14,13 @@ * limitations under the License. */ -package io.helidon.service.registry; +package io.helidon.service.inject.api; import java.util.Set; -import io.helidon.common.types.TypeName; -import io.helidon.service.registry.GeneratedService.Descriptor; - -interface ServiceMetadata { - double weight(); - - Set contracts(); - - Descriptor descriptor(); +record QualifiedInstanceImpl(T instance, Set qualifiers) implements Injection.QualifiedInstance { + @Override + public T get() { + return instance; + } } diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifierBlueprint.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifierBlueprint.java new file mode 100644 index 00000000000..8d8a9fff5e3 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifierBlueprint.java @@ -0,0 +1,31 @@ +/* + * 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.api; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.types.Annotation; + +/** + * Represents a qualifier annotation (a specific case of annotations, annotated with + * {@link Injection.Qualifier}). + * + * @see Injection.Qualifier + */ +@Prototype.Blueprint +@Prototype.CustomMethods(QualifierSupport.CustomMethods.class) +interface QualifierBlueprint extends Annotation { +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifierSupport.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifierSupport.java new file mode 100644 index 00000000000..b60a22127f2 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/QualifierSupport.java @@ -0,0 +1,212 @@ +/* + * 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.api; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; + +/* + Support for QualifierBlueprint + */ +class QualifierSupport { + static final class CustomMethods { + /** + * Represents a wildcard {@link Injection.Named} qualifier. + */ + @Prototype.Constant + static final Qualifier WILDCARD_NAMED = createNamed(Injection.Named.WILDCARD_NAME); + + /** + * Represents an instance named with the default name: {@value Injection.Named#DEFAULT_NAME}. + */ + @Prototype.Constant + static final Qualifier DEFAULT_NAMED = createNamed(Injection.Named.DEFAULT_NAME); + + /** + * Represents a qualifier used for injecting name of {@link io.helidon.service.inject.api.Injection.PerInstance} + * instances. + */ + @Prototype.Constant + static final Qualifier CREATE_FOR_NAME = create(Injection.InstanceName.class); + + private CustomMethods() { + } + + /** + * Creates a qualifier from an annotation. + * + * @param qualifierType the qualifier type + * @return qualifier + */ + @Prototype.FactoryMethod + static Qualifier create(Class qualifierType) { + Objects.requireNonNull(qualifierType); + TypeName typeName = TypeName.create(qualifierType); + TypeName qualifierTypeName = maybeNamed(typeName); + return Qualifier.builder().typeName(qualifierTypeName).build(); + } + + /** + * Creates a qualifier with a value from an annotation. + * + * @param qualifierType the qualifier type + * @param value the value property + * @return qualifier + */ + @Prototype.FactoryMethod + static Qualifier create(Class qualifierType, String value) { + Objects.requireNonNull(qualifierType); + TypeName typeName = TypeName.create(qualifierType); + TypeName qualifierTypeName = maybeNamed(typeName); + return Qualifier.builder() + .typeName(qualifierTypeName) + .putValue("value", value) + .build(); + } + + /** + * Creates a qualifier from an annotation. + * + * @param qualifierType the qualifier type + * @return qualifier + */ + @Prototype.FactoryMethod + static Qualifier create(TypeName qualifierType) { + Objects.requireNonNull(qualifierType); + TypeName qualifierTypeName = maybeNamed(qualifierType); + return Qualifier.builder().typeName(qualifierTypeName).build(); + } + + /** + * Creates a qualifier with a value from an annotation. + * + * @param qualifierType the qualifier type + * @param value the value property + * @return qualifier + */ + @Prototype.FactoryMethod + static Qualifier create(TypeName qualifierType, String value) { + Objects.requireNonNull(qualifierType); + TypeName qualifierTypeName = maybeNamed(qualifierType); + return Qualifier.builder() + .typeName(qualifierTypeName) + .putValue("value", value) + .build(); + } + + /** + * Creates a qualifier from an annotation. + * + * @param annotation the qualifier annotation + * @return qualifier + */ + @Prototype.FactoryMethod + static Qualifier create(Annotation annotation) { + Objects.requireNonNull(annotation); + if (annotation instanceof Qualifier qualifier) { + return qualifier; + } + return Qualifier.builder() + .typeName(maybeNamed(annotation.typeName())) + .values(removeEmptyProperties(annotation.values())) + .build(); + } + + /** + * Creates a {@link Injection.Named} qualifier. + * + * @param name the name + * @return named qualifier + */ + @Prototype.FactoryMethod + static Qualifier createNamed(String name) { + Objects.requireNonNull(name); + return Qualifier.builder() + .typeName(Injection.Named.TYPE) + .value(name) + .build(); + } + + /** + * Creates a {@link Injection.Named} qualifier. + * + * @param name the name + * @return named qualifier + */ + @Prototype.FactoryMethod + static Qualifier createNamed(Injection.Named name) { + Objects.requireNonNull(name); + Qualifier.Builder builder = Qualifier.builder() + .typeName(Injection.Named.TYPE); + if (!name.value().isEmpty()) { + builder.value(name.value()); + } + return builder.build(); + } + + /** + * Creates a {@link Injection.Named} qualifier. + * + * @param name the name + * @return named qualifier + */ + @Prototype.FactoryMethod + static Qualifier createNamed(Injection.NamedByType name) { + Objects.requireNonNull(name); + return Qualifier.builder() + .typeName(Injection.Named.TYPE) + .value(name.value().getName()) + .build(); + } + + /** + * Creates a {@link Injection.Named} qualifier from a class name. + * + * @param className class whose name will be used + * @return named qualifier + */ + @Prototype.FactoryMethod + static Qualifier createNamed(Class className) { + Objects.requireNonNull(className); + return Qualifier.builder() + .typeName(Injection.Named.TYPE) + .value(className.getName()) + .build(); + } + + private static TypeName maybeNamed(TypeName qualifierType) { + if (Injection.NamedByType.TYPE.equals(qualifierType)) { + return Injection.Named.TYPE; + } + return qualifierType; + } + + private static Map removeEmptyProperties(Map values) { + HashMap result = new HashMap<>(values); + result.entrySet().removeIf(entry -> { + Object value = entry.getValue(); + return value instanceof String str && str.isBlank(); + }); + return result; + } + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Qualifiers.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Qualifiers.java new file mode 100644 index 00000000000..e242013621a --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Qualifiers.java @@ -0,0 +1,95 @@ +/* + * 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.api; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.Annotation; + +/** + * Utility methods for qualifiers. + */ +final class Qualifiers { + private Qualifiers() { + } + + /** + * Matches qualifier collections. + * + * @param src the target service info to evaluate + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + */ + static boolean matchesQualifiers(Collection src, + Collection criteria) { + if (criteria.isEmpty()) { + // the criteria do not care about qualifiers at all + return true; + } + + if (src.isEmpty()) { + // neither defines qualifiers + return false; + } + + // criteria has a qualifier while service does not + // only return true if criteria contains ONLY wildcard named qualifier + if (criteria.size() == 1 && criteria.contains(Qualifier.WILDCARD_NAMED)) { + return true; + } + + if (src.contains(Qualifier.WILDCARD_NAMED)) { + // if provider has any name, and criteria ONLY asks for named, we match + if (criteria.stream() + .allMatch(it -> it.typeName().equals(Injection.Named.TYPE))) { + return true; + } + } + + for (Qualifier criteriaQualifier : criteria) { + if (src.contains(criteriaQualifier)) { + // NOP; + } else if (criteriaQualifier.typeName().equals(Injection.Named.TYPE)) { + if (criteriaQualifier.equals(Qualifier.WILDCARD_NAMED) + || criteriaQualifier.value().isEmpty()) { + // any Named qualifier will match ... + boolean hasSameTypeAsCriteria = src.stream() + .anyMatch(q -> q.typeName().equals(criteriaQualifier.typeName())); + if (hasSameTypeAsCriteria) { + continue; + } + } else if (src.contains(Qualifier.WILDCARD_NAMED)) { + continue; + } + return false; + } else if (criteriaQualifier.value().isEmpty()) { + Set sameTypeAsCriteriaSet = src.stream() + .filter(q -> q.typeName().equals(criteriaQualifier.typeName())) + .collect(Collectors.toSet()); + if (sameTypeAsCriteriaSet.isEmpty()) { + return false; + } + } else { + return false; + } + } + + return true; + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Scope.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Scope.java new file mode 100644 index 00000000000..7aed6f43ab6 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Scope.java @@ -0,0 +1,35 @@ +/* + * 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.api; + +/** + * A scope, such as request scope. + */ +public interface Scope extends AutoCloseable { + /** + * Stop the scope, and destroy all service instances created within it. + */ + @Override + void close(); + + /** + * Service registry instance associated with this scope. + * + * @return services + */ + ScopedRegistry registry(); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/ScopeNotActiveException.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/ScopeNotActiveException.java new file mode 100644 index 00000000000..db28fce85ff --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/ScopeNotActiveException.java @@ -0,0 +1,54 @@ +/* + * 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.api; + +import java.util.Objects; + +import io.helidon.common.types.TypeName; +import io.helidon.service.registry.ServiceRegistryException; + +/** + * An attempt was done to get a service instance from a scope that is not active. + */ +public class ScopeNotActiveException extends ServiceRegistryException { + /** + * Scope that was expected to be active. + */ + private final TypeName scope; + + /** + * Create a new exception with a description and scope this exception is created for. + * + * @param msg descriptive message + * @param scope scope that failed to be found + */ + public ScopeNotActiveException(String msg, TypeName scope) { + super(msg); + + Objects.requireNonNull(scope); + this.scope = scope; + } + + /** + * Scope that was not active. + * + * @return scope + */ + public TypeName scope() { + return scope; + } +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/ScopedRegistry.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/ScopedRegistry.java new file mode 100644 index 00000000000..fd30eaee895 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/ScopedRegistry.java @@ -0,0 +1,49 @@ +/* + * 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.api; + +import java.util.function.Supplier; + +import io.helidon.service.registry.ServiceInfo; + +/** + * Service registry of a specific scope. + */ +public interface ScopedRegistry { + /** + * Activate this registry instance. This method will prepare this registry for use. + */ + void activate(); + + /** + * Deactivate this registry instance. This method will deactivate all active instances + * + * @throws io.helidon.service.registry.ServiceRegistryException in case one or more services failed to deactivate + */ + void deactivate(); + + /** + * Provides either an existing activator, if one is already available in this scope, or adds a new activator instance. + * + * @param descriptor service descriptor + * @param activatorSupplier supplier of new activators to manage service instances + * @param type of the instances supported by the descriptor + * @return activator for the service, either an existing one, or a new one created from the supplier + */ + Activator activator(ServiceInfo descriptor, + Supplier> activatorSupplier); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/Scopes.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/Scopes.java new file mode 100644 index 00000000000..ed45de92dbb --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/Scopes.java @@ -0,0 +1,45 @@ +/* + * 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.api; + +import java.util.Map; + +import io.helidon.common.types.TypeName; +import io.helidon.service.registry.Service; +import io.helidon.service.registry.ServiceDescriptor; + +/** + * Service that provides support for creating {@link io.helidon.service.inject.api.Scope} instances. + */ +@Service.Contract +@Injection.Describe +public interface Scopes { + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(Scopes.class); + + /** + * Create a registry managed scope. + * + * @param scope scope annotation type + * @param id id of the scope + * @param initialBindings initial bindings for the created scope + * @return a new scope instance + */ + Scope createScope(TypeName scope, String id, Map, Object> initialBindings); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/ServiceInstance.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/ServiceInstance.java new file mode 100644 index 00000000000..e14fae124c9 --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/ServiceInstance.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.inject.api; + +import java.util.Set; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; + +/** + * An instance managed by the service registry, with a subset of relevant metadata. + * This type is injectable in the same manner as a regular service instance. + * + * @param type of the instance + */ +public interface ServiceInstance extends Injection.QualifiedInstance { + /** + * Type name of this interface. {@link io.helidon.common.types.TypeName} is used in various APIs of service registry. + */ + TypeName TYPE = TypeName.create(ServiceInstance.class); + + /** + * Contracts of the service instance. + * + * @return contracts the service instance implements + */ + Set contracts(); + + /** + * Scope this instance was created in. Always the same as the scope of the associated service descriptor + * ({@link InjectServiceDescriptor#scope()}. + * This method may return {@link io.helidon.service.inject.api.Injection.PerLookup} in case no scope is + * defined ("Dependent" scope is not a real scope, as the instances cannot be managed, so each time an instance is injected, + * it is constructed, injected, post constructed, and then forgotten by the registry). + * + * @return scope of this service instance + */ + TypeName scope(); + + /** + * Weight of this instance, inherited from {@link io.helidon.service.registry.ServiceDescriptor#weight()}. + * + * @return weight + */ + double weight(); + + /** + * Service type responsible for creating this value, inherited from + * {@link io.helidon.service.registry.ServiceDescriptor#serviceType()}. + * + * @return service type + */ + TypeName serviceType(); +} diff --git a/service/inject/api/src/main/java/io/helidon/service/inject/api/package-info.java b/service/inject/api/src/main/java/io/helidon/service/inject/api/package-info.java new file mode 100644 index 00000000000..2e26bd5b36d --- /dev/null +++ b/service/inject/api/src/main/java/io/helidon/service/inject/api/package-info.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +/** + * API required to define services, and to compile the code generated sources for Helidon Inject, + * with a core service registry implementation (replacement for {@link java.util.ServiceLoader}). + *

    + * The following main entry points for declaring services are available: + *

      + *
    • {@link io.helidon.service.registry.Service} - for core registry
    • + *
    • {@link io.helidon.service.inject.api.Injection} - for injection support
    • + *
    • {@link io.helidon.service.inject.api.Interception} - for interception
    • + *
    + */ +package io.helidon.service.inject.api; diff --git a/service/inject/api/src/main/java/module-info.java b/service/inject/api/src/main/java/module-info.java new file mode 100644 index 00000000000..96636cbe113 --- /dev/null +++ b/service/inject/api/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Service registry with injection support API. + */ +module io.helidon.service.inject.api { + requires transitive io.helidon.common.config; + requires transitive io.helidon.service.registry; + requires transitive io.helidon.builder.api; + requires transitive io.helidon.common.types; + + exports io.helidon.service.inject.api; +} \ No newline at end of file diff --git a/service/inject/codegen/README.md b/service/inject/codegen/README.md new file mode 100644 index 00000000000..296d6414550 --- /dev/null +++ b/service/inject/codegen/README.md @@ -0,0 +1,21 @@ +Service Inject Codegen +--------------- + +# Supported annotations + +| Annotation | Description | +|------------------------------------|-------------------------------------------------------------------------------------| +| @Injection.Inject | Type with an injection point is a per lookup service unless it has scope annotation | +| @Injection.Describe | Generates a service descriptor for a contract without a service implementation | +| @Injection.PerInstance | A service instantiated based on another service | +| @Injection.Scope (meta-annotation) | Scoped service | + +# Supported options + +Options can be configured as annotation processor options, when running via annotation processor. + +| Option | Description | +|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `helidon.registry.autoAddNonContractInterfaces` | If set to `true`, all implemented interfaces and super types are considered a contract; by default, `@Service.Contract` or `@Service.ExternalContracts` must be in place | +| `helidon.inject.interceptionStrategy` | How to handle generation of interceptor invokers (NONE, EXPLICIT, ALL_RUNTIME, ALL_RETAINED) | +| `helidon.inject.scopeMetaAnnotations` | Set of annotations that mark scopes that are not Helidon scopes | diff --git a/service/inject/codegen/pom.xml b/service/inject/codegen/pom.xml new file mode 100644 index 00000000000..c743c516b47 --- /dev/null +++ b/service/inject/codegen/pom.xml @@ -0,0 +1,59 @@ + + + + + + io.helidon.service.inject + helidon-service-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-inject-codegen + Helidon Service Inject Codegen + + Code generation for Helidon Service Inject applications + + + + + io.helidon.common + helidon-common-types + + + io.helidon.common + helidon-common + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-class-model + + + io.helidon.service + helidon-service-codegen + + + diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Assignments.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Assignments.java new file mode 100644 index 00000000000..b09c7f8ca7f --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Assignments.java @@ -0,0 +1,60 @@ +/* + * 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.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.TypeName; +import io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.inject.codegen.spi.InjectAssignment; +import io.helidon.service.inject.codegen.spi.InjectAssignmentProvider; + +class Assignments { + private final List assingments; + + Assignments(RegistryCodegenContext ctx) { + this.assingments = HelidonServiceLoader.create( + ServiceLoader.load(InjectAssignmentProvider.class, + Assignments.class.getClassLoader())) + .stream() + .map(it -> it.create(ctx)) + .toList(); + } + + /** + * This provides support for replacements of types. + * + * @param typeName type name as required by the dependency ("injection point") + * @param valueSource code with the source of the parameter as Helidon provides it (such as Supplier of type) + * @return assignment to use for this instance, what type to use in Helidon registry, and code generator to transform to + * desired type + */ + public InjectAssignment.Assignment assignment(TypeName typeName, String valueSource) { + for (InjectAssignment assignmentProvider : assingments) { + Optional assignment = assignmentProvider.assignment(typeName, valueSource); + if (assignment.isPresent()) { + return assignment.get(); + } + } + + return InjectAssignment.Assignment.create(typeName, it -> it.addContent(valueSource)); + } + +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedElements.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedElements.java new file mode 100644 index 00000000000..185e9c28dc0 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedElements.java @@ -0,0 +1,157 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypedElementInfo; + +/** + * Each described service has elements for the service itself (both if provider and if direct implementation), + * and in case of provider it has elements for the provided types. + */ +class DescribedElements { + private final Set interceptedMethods; + private final Set plainMethods; + // all elements that are accessible + private final List allElements; + // all intercepted elements (subset of allElements) + private final List interceptedElements; + // all plain (not intercepted) elements (subset of allElements) + private final List plainElements; + private final boolean intercepted; + private final boolean methodIntercepted; + private final boolean constructorIntercepted; + + private DescribedElements(Set interceptedMethods, + Set plainMethods, + List allElements, + List interceptedElements, + List plainElements, + boolean isIntercepted, + boolean methodsIntercepted, + boolean constructorIntercepted) { + this.interceptedMethods = interceptedMethods; + this.plainMethods = plainMethods; + this.allElements = allElements; + this.interceptedElements = interceptedElements; + this.plainElements = plainElements; + this.intercepted = isIntercepted; + this.methodIntercepted = methodsIntercepted; + this.constructorIntercepted = constructorIntercepted; + } + + /** + * Create for a service. + * + * @param ctx to find {@link io.helidon.common.types.TypeInfo} for analysis + * @param interception interception support + * @param serviceTypeInfo type info of the processed service + * @param contracts eligible contracts of the service + * @return described elements for a service + */ + static DescribedElements create(CodegenContext ctx, + Interception interception, + Collection contracts, + TypeInfo serviceTypeInfo) { + var all = TypedElements.gatherElements(ctx, contracts, serviceTypeInfo) + .stream() + .collect(Collectors.toUnmodifiableList()); + var allMethods = all.stream() + .map(TypedElements.ElementMeta::element) + .filter(ElementInfoPredicates::isMethod) + .map(TypedElementInfo::signature) + .collect(Collectors.toUnmodifiableSet()); + + var intercepted = interception.maybeIntercepted(serviceTypeInfo, all); + var interceptedMethods = intercepted.stream() + .map(TypedElements.ElementMeta::element) + .filter(ElementInfoPredicates::isMethod) + .map(TypedElementInfo::signature) + .collect(Collectors.toUnmodifiableSet()); + + var plain = all.stream() + .filter(it -> !interceptedMethods.contains(it.element().signature())) + .collect(Collectors.toUnmodifiableList()); + var plainSignatures = allMethods.stream() + .filter(it -> !interceptedMethods.contains(it)) + .collect(Collectors.toUnmodifiableSet()); + + boolean methodsIntercepted = intercepted.stream() + .map(TypedElements.ElementMeta::element) + .anyMatch(ElementInfoPredicates::isMethod); + boolean constructorIntercepted = intercepted.stream() + .map(TypedElements.ElementMeta::element) + .anyMatch(ElementInfoPredicates::isConstructor); + boolean isIntercepted = !intercepted.isEmpty(); + + return new DescribedElements(interceptedMethods, + plainSignatures, + all, + intercepted, + plain, + isIntercepted, + methodsIntercepted, + constructorIntercepted); + } + + @Override + public String toString() { + return "intercepted (" + interceptedElements.size() + "), plain (" + plainElements.size() + ")"; + } + + public Set interceptedMethods() { + return interceptedMethods; + } + + public Set plainMethods() { + return plainMethods; + } + + public List allElements() { + return allElements; + } + + public List interceptedElements() { + return interceptedElements; + } + + public List plainElements() { + return plainElements; + } + + public boolean intercepted() { + return intercepted; + } + + public boolean methodsIntercepted() { + return methodIntercepted; + } + + public boolean constructorIntercepted() { + return constructorIntercepted; + } + +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedService.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedService.java new file mode 100644 index 00000000000..fec8f10385c --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedService.java @@ -0,0 +1,314 @@ +/* + * 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.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.codegen.CodegenException; +import io.helidon.common.types.Annotation; +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.codegen.RegistryCodegenContext; +import io.helidon.service.codegen.RegistryRoundContext; +import io.helidon.service.codegen.ServiceContracts; +import io.helidon.service.codegen.ServiceSuperType; + +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_POINT_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_QUALIFIED_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_SERVICES_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPT_G_WRAPPER_IP_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPT_G_WRAPPER_QUALIFIED_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPT_G_WRAPPER_SERVICES_FACTORY; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPT_G_WRAPPER_SUPPLIER_FACTORY; + +/** + * A service (as declared and annotated with a scope by the user). + * It may be a service provider (if it implements one of the provider interfaces), or it is a contract + * implementation on its own. + */ +class DescribedService { + private static final Annotation WILDCARD_NAMED = Annotation.create(INJECTION_NAMED, "*"); + + private final DescribedType serviceType; + private final DescribedType providedType; + + /* + the following is only relevant on service itself (not on provided type) + */ + // type of provider (if this is a provider at all) + private final FactoryType providerType; + // qualifiers of provided type are inherited from service + private final Set qualifiers; + // provided type does not have a descriptor, only service does + private final TypeName descriptorType; + // scope of provided type is inherited from the service + private final TypeName scope; + // required for descriptor generation + private final ServiceSuperType superType; + // in case this service is a qualified provider, we also get the qualifier it handles + private final TypeName qualifiedProviderQualifier; + + private DescribedService(DescribedType serviceType, + DescribedType providedType, + ServiceSuperType superType, + TypeName scope, + TypeName descriptorType, + Set qualifiers, + FactoryType providerType, + TypeName qualifiedProviderQualifier) { + + this.serviceType = serviceType; + this.providedType = providedType; + this.superType = superType; + this.descriptorType = descriptorType; + this.scope = scope; + this.qualifiers = Set.copyOf(qualifiers); + this.providerType = providerType; + this.qualifiedProviderQualifier = qualifiedProviderQualifier; + } + + static DescribedService create(RegistryCodegenContext ctx, + RegistryRoundContext roundContext, + Interception interception, + TypeInfo serviceInfo, + ServiceSuperType superType, + TypeName scope) { + TypeName serviceType = serviceInfo.typeName(); + TypeName descriptorType = ctx.descriptorType(serviceType); + + Set directContracts = new HashSet<>(); + Set providedContracts = new HashSet<>(); + FactoryType providerType = FactoryType.SERVICE; + TypeName qualifiedProviderQualifier = null; + TypeInfo providedTypeInfo = null; + TypeName providedTypeName = null; + + ServiceContracts serviceContracts = roundContext.serviceContracts(serviceInfo); + + // now we know which contracts are OK to use, and we can check the service types and real contracts + // service is a factory only if it implements the interface directly; this is never inherited + List typeInfos = serviceInfo.interfaceTypeInfo(); + Map implementedInterfaceTypes = new HashMap<>(); + typeInfos.forEach(it -> implementedInterfaceTypes.put(it.typeName(), it)); + + /* + For each service type we support, gather contracts + */ + var response = serviceContracts.analyseFactory(TypeNames.SUPPLIER); + if (response.valid()) { + providerType = FactoryType.SUPPLIER; + directContracts.add(ResolvedType.create(response.factoryType())); + providedContracts.addAll(response.providedContracts()); + providedTypeName = response.providedType(); + providedTypeInfo = response.providedTypeInfo(); + implementedInterfaceTypes.remove(TypeNames.SUPPLIER); + } + response = serviceContracts.analyseFactory(INJECTION_SERVICES_FACTORY); + if (response.valid()) { + // if this is not a service type, throw + if (providerType != FactoryType.SERVICE) { + throw new CodegenException("Service implements more than one provider type: " + + providerType + ", and services provider.", + serviceInfo.originatingElementValue()); + } + providerType = FactoryType.SERVICES; + directContracts.add(ResolvedType.create(response.providedType())); + providedContracts.addAll(response.providedContracts()); + providedTypeName = response.providedType(); + providedTypeInfo = response.providedTypeInfo(); + implementedInterfaceTypes.remove(INJECTION_SERVICES_FACTORY); + } + response = serviceContracts.analyseFactory(INJECTION_POINT_FACTORY); + if (response.valid()) { + // if this is not a service type, throw + if (providerType != FactoryType.SERVICE) { + throw new CodegenException("Service implements more than one provider type: " + + providerType + ", and injection point provider.", + serviceInfo.originatingElementValue()); + } + providerType = FactoryType.INJECTION_POINT; + directContracts.add(ResolvedType.create(response.providedType())); + providedContracts.addAll(response.providedContracts()); + providedTypeName = response.providedType(); + providedTypeInfo = response.providedTypeInfo(); + implementedInterfaceTypes.remove(INJECTION_POINT_FACTORY); + } + response = serviceContracts.analyseFactory(INJECTION_QUALIFIED_FACTORY); + if (response.valid()) { + // if this is not a service type, throw + if (providerType != FactoryType.SERVICE) { + throw new CodegenException("Service implements more than one provider type: " + + providerType + ", and qualified provider.", + serviceInfo.originatingElementValue()); + } + providerType = FactoryType.QUALIFIED; + directContracts.add(ResolvedType.create(response.providedType())); + providedContracts.addAll(response.providedContracts()); + qualifiedProviderQualifier = ServiceContracts + .requiredTypeArgument(implementedInterfaceTypes.remove(INJECTION_QUALIFIED_FACTORY), 1); + providedTypeName = response.providedType(); + providedTypeInfo = response.providedTypeInfo(); + implementedInterfaceTypes.remove(INJECTION_QUALIFIED_FACTORY); + } + + // add direct contracts + HashSet processedDirectContracts = new HashSet<>(); + implementedInterfaceTypes.forEach((type, typeInfo) -> { + serviceContracts.addContracts(directContracts, + processedDirectContracts, + typeInfo); + }); + // add contracts from super type(s) + serviceInfo.superTypeInfo().ifPresent(it -> serviceContracts.addContracts(directContracts, + processedDirectContracts, + it)); + + DescribedType serviceDescriptor; + DescribedType providedDescriptor; + + if (providerType == FactoryType.SERVICE) { + var serviceElements = DescribedElements.create(ctx, interception, directContracts, serviceInfo); + serviceDescriptor = new DescribedType(serviceInfo, + serviceInfo.typeName(), + directContracts, + serviceElements); + + providedDescriptor = null; + } else { + var serviceElements = DescribedElements.create(ctx, interception, Set.of(), serviceInfo); + serviceDescriptor = new DescribedType(serviceInfo, + serviceInfo.typeName(), + directContracts, + serviceElements); + DescribedElements providedElements = DescribedElements.create(ctx, interception, providedContracts, providedTypeInfo); + + providedDescriptor = new DescribedType(providedTypeInfo, + providedTypeName, + providedContracts, + providedElements); + } + + return new DescribedService( + serviceDescriptor, + providedDescriptor, + superType, + scope, + descriptorType, + gatherQualifiers(serviceInfo), + providerType, + qualifiedProviderQualifier + ); + } + + @Override + public String toString() { + return serviceType.typeName().fqName(); + } + + TypeName interceptionWrapperSuperType() { + return switch (providerType()) { + case NONE, SERVICE -> serviceType.typeName(); + case SUPPLIER -> createType(INTERCEPT_G_WRAPPER_SUPPLIER_FACTORY, providedType.typeName()); + case SERVICES -> createType(INTERCEPT_G_WRAPPER_SERVICES_FACTORY, providedType.typeName()); + case INJECTION_POINT -> createType(INTERCEPT_G_WRAPPER_IP_FACTORY, providedType.typeName()); + case QUALIFIED -> + createType(INTERCEPT_G_WRAPPER_QUALIFIED_FACTORY, providedType.typeName(), qualifiedProviderQualifier); + }; + } + + TypeName providerInterface() { + return switch (providerType()) { + case NONE, SERVICE -> serviceType.typeName(); + case SUPPLIER -> createType(TypeNames.SUPPLIER, providedType.typeName()); + case SERVICES -> createType(INJECTION_SERVICES_FACTORY, providedType.typeName()); + case INJECTION_POINT -> createType(INJECTION_POINT_FACTORY, providedType.typeName()); + case QUALIFIED -> + createType(INJECTION_QUALIFIED_FACTORY, providedType.typeName(), qualifiedProviderQualifier); + }; + } + + boolean isFactory() { + return providerType() != FactoryType.SERVICE && providerType() != FactoryType.NONE; + } + + DescribedType providedDescriptor() { + return providedType; + } + + DescribedType serviceDescriptor() { + return serviceType; + } + + ServiceSuperType superType() { + return superType; + } + + TypeName descriptorType() { + return descriptorType; + } + + TypeName scope() { + return scope; + } + + Set qualifiers() { + return Set.copyOf(qualifiers); + } + + FactoryType providerType() { + return providerType; + } + + TypeName qualifiedProviderQualifier() { + return qualifiedProviderQualifier; + } + + private static TypeName createType(TypeName... types) { + TypeName.Builder builder = TypeName.builder() + .from(types[0]); + + for (int i = 1; i < types.length; i++) { + builder.addTypeArgument(types[i]); + } + + return builder.build(); + } + + private static Set gatherQualifiers(TypeInfo serviceTypeInfo) { + Set qualifiers = new LinkedHashSet<>(); + if (serviceTypeInfo.hasAnnotation(INJECTION_PER_INSTANCE)) { + qualifiers.add(WILDCARD_NAMED); + } + + for (Annotation annotation : serviceTypeInfo.annotations()) { + if (serviceTypeInfo.hasMetaAnnotation(annotation.typeName(), + InjectCodegenTypes.INJECTION_QUALIFIER)) { + qualifiers.add(annotation); + } + } + return qualifiers; + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedType.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedType.java new file mode 100644 index 00000000000..4afecde9352 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/DescribedType.java @@ -0,0 +1,82 @@ +/* + * 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.Objects; +import java.util.Set; + +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; + +/** + * A described type (class, interface). + * User service can have up to two types - one is the service itself, another one is a provided contract, + * if the service is a provider. + */ +public class DescribedType { + private final TypeInfo typeInfo; + private final boolean isAbstract; + private final TypeName typeName; + private final Set contracts; + private final DescribedElements elements; + + DescribedType(TypeInfo typeInfo, TypeName typeName, Set contracts, DescribedElements elements) { + Objects.requireNonNull(typeInfo); + Objects.requireNonNull(typeName); + Objects.requireNonNull(contracts); + Objects.requireNonNull(elements); + + this.typeInfo = typeInfo; + this.isAbstract = isAbstract(typeInfo); + this.typeName = typeName; + this.contracts = contracts; + this.elements = elements; + } + + boolean isAbstract() { + return isAbstract; + } + + TypeInfo typeInfo() { + return typeInfo; + } + + TypeName typeName() { + return typeName; + } + + Set contracts() { + return contracts; + } + + DescribedElements elements() { + return elements; + } + + private static boolean isAbstract(TypeInfo typeInfo) { + if (typeInfo == null) { + return false; + } + if (typeInfo.kind() == ElementKind.CLASS) { + return typeInfo.elementModifiers().contains(Modifier.ABSTRACT); + } + return typeInfo.kind() == ElementKind.INTERFACE; + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java new file mode 100644 index 00000000000..50ebda400ac --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventEmitterObserverProvider.java @@ -0,0 +1,247 @@ +/* + * 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.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Annotations; +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.common.types.TypedElementInfo; +import io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.codegen.RegistryRoundContext; +import io.helidon.service.inject.codegen.spi.InjectCodegenObserver; +import io.helidon.service.inject.codegen.spi.InjectCodegenObserverProvider; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.EVENT_EMITTER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.EVENT_MANAGER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_INJECT; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_SINGLETON; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_QUALIFIER; + +/** + * {@link java.util.ServiceLoader} provider implementation that generates services for event emitters. + */ +public class EventEmitterObserverProvider implements InjectCodegenObserverProvider { + /** + * Public constructor required by {@link java.util.ServiceLoader}. + */ + public EventEmitterObserverProvider() { + } + + @Override + public InjectCodegenObserver create(RegistryCodegenContext context) { + return new EventEmitterObserver(); + } + + private static final class EventEmitterObserver implements InjectCodegenObserver { + private static final TypeName GENERATOR = TypeName.create(EventEmitterObserver.class); + private static final Map, TypeName>> CACHE = new ConcurrentHashMap<>(); + + private EventEmitterObserver() { + } + + @Override + public void onInjectionPoint(RegistryRoundContext roundContext, + TypeInfo service, + TypedElementInfo element, + TypedElementInfo argument) { + + TypeName typeName = argument.typeName(); + if (typeName.equals(EVENT_EMITTER)) { + if (typeName.typeArguments().isEmpty()) { + throw new CodegenException("Cannot generate an event emitter when type argument is missing", + argument.originatingElementValue()); + } + Set qualifiers = Qualifiers.qualifiers(argument); + TypeName eventObject = typeName.typeArguments().getFirst(); + TypeName generatedType = emitterTypeName(service.typeName(), eventObject, qualifiers); + if (roundContext.generatedType(generatedType).isEmpty()) { + // it may be already generated - maybe there are two injection points for the same event with same qualifiers + generateEventEmitter(roundContext, service, generatedType, service.typeName(), eventObject, qualifiers); + } + } + } + + private static TypeName emitterTypeName(TypeName serviceType, TypeName eventObject, Set qualifiers) { + ResolvedType event = ResolvedType.create(eventObject); + + var map = CACHE.computeIfAbsent(new ClassNameCacheKey(serviceType, event), k -> new ConcurrentHashMap<>()); + return map.computeIfAbsent(qualifiers, it -> { + String className = serviceType.classNameWithEnclosingNames().replace('.', '_') + + "__Emitter"; + var builder = TypeName.builder() + .packageName(serviceType.packageName()); + if (map.isEmpty()) { + return builder.className(className) + .build(); + } + return builder.className(className + "_" + map.size()) + .build(); + }); + } + + private void generateEventEmitter(RegistryRoundContext roundContext, + TypeInfo serviceInfo, + TypeName generatedType, + TypeName serviceTypeName, + TypeName eventObject, + Set qualifiers) { + ClassModel.Builder classModel = ClassModel.builder() + .copyright(CodegenUtil.copyright(GENERATOR, + serviceTypeName, + generatedType)) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + serviceTypeName, + generatedType, + "1", + "")) + .type(generatedType) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .description("Event emitter service for {@link " + eventObject.fqName() + "}.") + .addInterface(emitterInterface(eventObject)) + .addAnnotation(Annotation.create(INJECTION_SINGLETON)); + + // constant for event type + classModel.addField(eventObjectConstant -> eventObjectConstant + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(TypeNames.RESOLVED_TYPE_NAME) + .name("EVENT_OBJECT") + .addContentCreate(ResolvedType.create(eventObject))); + + // qualifiers (if any) + if (!qualifiers.isEmpty()) { + Qualifiers.generateQualifiersConstant(classModel, qualifiers); + } + + // event manager (must be injected) + classModel.addField(eventManager -> eventManager + .accessModifier(AccessModifier.PRIVATE) + .isFinal(true) + .type(EVENT_MANAGER) + .name("manager")); + + // constructor + classModel.addConstructor(ctr -> ctr + .addAnnotation(Annotation.create(INJECTION_INJECT)) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .addParameter(eventManager -> eventManager + .type(EVENT_MANAGER) + .name("manager")) + .addContentLine("this.manager = manager;")); + + // emit methods + boolean qualified = !qualifiers.isEmpty(); + addEmit(classModel, "emit", eventObject, TypeNames.PRIMITIVE_VOID, qualified); + addEmit(classModel, "emitAsync", eventObject, completionStage(eventObject), qualified); + if (qualified) { + addMergeQualifiers(classModel); + qualifiers.forEach(it -> classModel.addAnnotation(it)); + } + + roundContext.addGeneratedType(generatedType, classModel, serviceTypeName, serviceInfo); + } + + private void addMergeQualifiers(ClassModel.Builder classModel) { + classModel.addMethod(merge -> merge + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .name("mergeQualifiers") + .addParameter(qualifiers -> qualifiers + .name("qualifiers") + .type(INJECT_QUALIFIER) + .vararg(true)) + .returnType(InjectionExtension.SET_OF_QUALIFIERS) + .addContentLine("if (qualifiers.length == 0) {") + .addContentLine("return QUALIFIERS;") + .addContentLine("}") + .addContent("var qualifierSet = new ") + .addContent(HashSet.class) + .addContentLine("(QUALIFIERS);") + .addContent("qualifierSet.addAll(") + .addContent(Set.class) + .addContentLine(".of(qualifiers));") + .addContentLine("return qualifierSet;") + ); + } + + private void addEmit(ClassModel.Builder classModel, + String methodName, + TypeName eventObject, + TypeName returnType, + boolean isQualified) { + classModel.addMethod(method -> method + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(AccessModifier.PUBLIC) + .returnType(returnType) + .name(methodName) + .addParameter(event -> event + .type(eventObject) + .name("eventObject")) + .addParameter(qualifiers -> qualifiers + .vararg(true) + .type(INJECT_QUALIFIER) + .name("qualifiers")) + .update(it -> { + if (!returnType.equals(TypeNames.PRIMITIVE_VOID)) { + it.addContent("return "); + } + }) + .addContent("manager.") + .addContent(methodName) + .update(it -> { + if (isQualified) { + it.addContentLine("(EVENT_OBJECT, eventObject, mergeQualifiers(qualifiers));"); + } else { + it.addContent("(EVENT_OBJECT, eventObject, ") + .addContent(Set.class) + .addContentLine(".of(qualifiers));"); + } + })); + } + + private TypeName completionStage(TypeName eventObject) { + return TypeName.builder() + .from(TypeName.create(CompletionStage.class)) + .addTypeArgument(eventObject) + .build(); + } + + private TypeName emitterInterface(TypeName eventObject) { + return TypeName.builder() + .from(EVENT_EMITTER) + .addTypeArgument(eventObject) + .build(); + } + } + + private record ClassNameCacheKey(TypeName serviceType, ResolvedType eventType) { + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventObserverExtensionProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventObserverExtensionProvider.java new file mode 100644 index 00000000000..d8275f687ae --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/EventObserverExtensionProvider.java @@ -0,0 +1,204 @@ +/* + * 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.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.classmodel.ClassModel; +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.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.codegen.RegistryRoundContext; +import io.helidon.service.codegen.spi.RegistryCodegenExtension; +import io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.EVENT_MANAGER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.EVENT_OBSERVER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.EVENT_OBSERVER_ASYNC; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_INJECT; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_SINGLETON; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_G_EVENT_OBSERVER_REGISTRATION; + +/** + * {@link java.util.ServiceLoader} provider implementation that adds support for generating event observer registrations. + */ +public class EventObserverExtensionProvider implements RegistryCodegenExtensionProvider { + /** + * Public constructor required by {@link java.util.ServiceLoader}. + */ + public EventObserverExtensionProvider() { + } + + @Override + public RegistryCodegenExtension create(RegistryCodegenContext codegenContext) { + return new EventObserverExtension(); + } + + @Override + public Set supportedAnnotations() { + return Set.of(EVENT_OBSERVER, + EVENT_OBSERVER_ASYNC); + } + + private static final class EventObserverExtension implements RegistryCodegenExtension { + private static final TypeName GENERATOR = TypeName.create(EventObserverExtensionProvider.EventObserverExtension.class); + private static final Map, TypeName>> CACHE = new ConcurrentHashMap<>(); + + @Override + public void process(RegistryRoundContext roundContext) { + Collection elements = roundContext.annotatedElements(EVENT_OBSERVER); + process(roundContext, elements, ""); + elements = roundContext.annotatedElements(EVENT_OBSERVER_ASYNC); + process(roundContext, elements, "Async"); + } + + private static TypeName registration(TypeName serviceType, TypeName eventObject, Set qualifiers) { + ResolvedType event = ResolvedType.create(eventObject); + + var map = CACHE.computeIfAbsent(new ClassNameCacheKey(serviceType, event), k -> new ConcurrentHashMap<>()); + return map.computeIfAbsent(qualifiers, it -> { + String className = serviceType.classNameWithEnclosingNames().replace('.', '_') + + "__Observer"; + var builder = TypeName.builder() + .packageName(serviceType.packageName()); + if (map.isEmpty()) { + return builder.className(className) + .build(); + } + return builder.className(className + "_" + map.size()) + .build(); + }); + } + + private void process(RegistryRoundContext roundContext, Collection elements, String suffix) { + for (TypedElementInfo element : elements) { + if (element.kind() != ElementKind.METHOD) { + throw new CodegenException("Event observer annotations are only allowed on methods", + element.originatingElementValue()); + } + if (element.accessModifier() == AccessModifier.PRIVATE) { + throw new CodegenException("Event observer annotations are only allowed on non-private methods", + element.originatingElementValue()); + } + if (!element.typeName().equals(TypeNames.PRIMITIVE_VOID)) { + throw new CodegenException("Event observer annotations are only allowed on void methods", + element.originatingElementValue()); + } + if (element.parameterArguments().size() != 1) { + throw new CodegenException("Event observer annotations are only allowed on methods with exactly one " + + "parameter", + element.originatingElementValue()); + } + TypedElementInfo parameter = element.parameterArguments().getFirst(); + TypeName eventObject = parameter.typeName(); + Set qualifiers = Qualifiers.qualifiers(element); + TypeInfo owningType = element.enclosingType() + .flatMap(roundContext::typeInfo) + .orElseThrow(() -> new CodegenException("Could not obtain type defining an observer", + element.originatingElementValue())); + generateObserverRegistration(roundContext, owningType, element, qualifiers, eventObject, suffix); + } + } + + private void generateObserverRegistration(RegistryRoundContext roundContext, + TypeInfo owningType, + TypedElementInfo element, + Set qualifiers, + TypeName eventObject, + String suffix) { + TypeName serviceTypeName = owningType.typeName(); + TypeName generatedType = registration(serviceTypeName, eventObject, qualifiers); + + ClassModel.Builder classModel = ClassModel.builder() + .copyright(CodegenUtil.copyright(GENERATOR, + serviceTypeName, + generatedType)) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + serviceTypeName, + generatedType, + "1", + "")) + .type(generatedType) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .description("Event observer registration service for {@link " + eventObject.fqName() + "}.") + .addInterface(INJECT_G_EVENT_OBSERVER_REGISTRATION) + .addAnnotation(Annotation.create(INJECTION_SINGLETON)); + + // constant for event type + classModel.addField(eventObjectConstant -> eventObjectConstant + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(TypeNames.RESOLVED_TYPE_NAME) + .name("EVENT_OBJECT") + .addContentCreate(ResolvedType.create(eventObject))); + + // qualifiers (if any) + Qualifiers.generateQualifiersConstant(classModel, qualifiers); + + // service field + classModel.addField(eventObserver -> eventObserver + .accessModifier(AccessModifier.PRIVATE) + .isFinal(true) + .type(serviceTypeName) + .name("eventObserver") + ); + + // constructor + classModel.addConstructor(ctr -> ctr + .addAnnotation(Annotation.create(INJECTION_INJECT)) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .addParameter(eventObserver -> eventObserver + .type(serviceTypeName) + .name("eventObserver")) + .addContentLine("this.eventObserver = eventObserver;")); + + // and the register method to register it + classModel.addMethod(register -> register + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(AccessModifier.PUBLIC) + .returnType(TypeNames.PRIMITIVE_VOID) + .name("register") + .addParameter(eventManager -> eventManager + .type(EVENT_MANAGER) + .name("manager")) + .addContent("manager.register") + .addContent(suffix) + .addContent("(EVENT_OBJECT, eventObserver::") + .addContent(element.elementName()) + .addContentLine(", QUALIFIERS);") + ); + + roundContext.addGeneratedType(generatedType, classModel, serviceTypeName, owningType); + } + + private record ClassNameCacheKey(TypeName serviceType, ResolvedType eventType) { + } + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/FactoryType.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/FactoryType.java new file mode 100644 index 00000000000..58ac1f7f7dd --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/FactoryType.java @@ -0,0 +1,56 @@ +/* + * 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; + +/** + * Described service type. + *

    + * Core services (services defined for core service registry) can be only {@link #SERVICE} or {@link #SUPPLIER}. + *

    + * This enum is duplicated in Inject API, as we do not want to have a common dependency. + */ +enum FactoryType { + /** + * This is just a descriptor that cannot instantiate anything. + */ + NONE, + /** + * Direct implementation of a service. + *

    + * This is the case when service does not implement any of the service provider interfaces, but it does + * implement at least one contract. + */ + SERVICE, + /** + * The service implements a {@link java.util.function.Supplier} of a contract. + */ + SUPPLIER, + /** + * The service implements a provider of a list of contract instances. + */ + SERVICES, + /** + * The service implements a provider that satisfies a specific injection point (either a single contract, + * or a list of contract instances). + */ + INJECTION_POINT, + /** + * The service implements a provider that is called for specific qualifiers (either a single contract, + * or a list of contract instances). + */ + QUALIFIED +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java new file mode 100644 index 00000000000..765b6e9521b --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectCodegenTypes.java @@ -0,0 +1,215 @@ +/* + * 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.codegen; + +import io.helidon.common.types.TypeName; + +/** + * Types for code generation from Helidon Service Inject API and Helidon Service Inject. + */ +public class InjectCodegenTypes { + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Point}. + */ + public static final TypeName INJECTION_INJECT = TypeName.create("io.helidon.service.inject.api.Injection.Inject"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Singleton}. + */ + public static final TypeName INJECTION_SINGLETON = TypeName.create("io.helidon.service.inject.api.Injection.Singleton"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Named}. + */ + public static final TypeName INJECTION_NAMED = TypeName.create("io.helidon.service.inject.api.Injection.Named"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.NamedByType}. + */ + public static final TypeName INJECTION_NAMED_BY_TYPE = + TypeName.create("io.helidon.service.inject.api.Injection.NamedByType"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Qualifier}. + */ + public static final TypeName INJECTION_QUALIFIER = TypeName.create("io.helidon.service.inject.api.Injection.Qualifier"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Describe}. + */ + public static final TypeName INJECTION_DESCRIBE = TypeName.create("io.helidon.service.inject.api.Injection.Describe"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.Scope}. + */ + public static final TypeName INJECTION_SCOPE = TypeName.create("io.helidon.service.inject.api.Injection.Scope"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.PerLookup}. + */ + public static final TypeName INJECTION_PER_LOOKUP = TypeName.create("io.helidon.service.inject.api.Injection.PerLookup"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.PerInstance}. + */ + public static final TypeName INJECTION_PER_INSTANCE = TypeName.create("io.helidon.service.inject.api.Injection.PerInstance"); + /** + * {@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.InjectionPointFactory}. + */ + public static final TypeName INJECTION_POINT_FACTORY = TypeName.create( + "io.helidon.service.inject.api.Injection.InjectionPointFactory"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.Injection.ScopeHandler}. + */ + public static final TypeName INJECTION_SCOPE_HANDLER = + TypeName.create("io.helidon.service.inject.api.Injection.ScopeHandler"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.ServicesFactory}. + */ + public static final TypeName INJECTION_SERVICES_FACTORY = + TypeName.create("io.helidon.service.inject.api.Injection.ServicesFactory"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.QualifiedFactory}. + */ + public static final TypeName INJECTION_QUALIFIED_FACTORY = + TypeName.create("io.helidon.service.inject.api.Injection.QualifiedFactory"); + + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Interception.Intercepted}. + */ + public static final TypeName INTERCEPTION_INTERCEPTED = + TypeName.create("io.helidon.service.inject.api.Interception.Intercepted"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Interception.Delegate}. + */ + public static final TypeName INTERCEPTION_DELEGATE = TypeName.create("io.helidon.service.inject.api.Interception.Delegate"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Interception.ExternalDelegates}. + */ + public static final TypeName INTERCEPTION_EXTERNAL_DELEGATE = + TypeName.create("io.helidon.service.inject.api.Interception.ExternalDelegate"); + + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.FactoryType}. + */ + public static final TypeName INJECT_FACTORY_TYPE = TypeName.create("io.helidon.service.inject.api.FactoryType"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Qualifier}. + */ + public static final TypeName INJECT_QUALIFIER = TypeName.create("io.helidon.service.inject.api.Qualifier"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Ip}. + */ + public static final TypeName INJECT_INJECTION_POINT = TypeName.create("io.helidon.service.inject.api.Ip"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.ServiceInstance}. + */ + public static final TypeName INJECT_SERVICE_INSTANCE = TypeName.create("io.helidon.service.inject.api.ServiceInstance"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.inject.api.InjectServiceDescriptor}. + */ + 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.api.InvocationException}. + */ + public static final TypeName INTERCEPT_EXCEPTION = + TypeName.create("io.helidon.service.inject.api.InterceptionException"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.InterceptionMetadata}. + */ + public static final TypeName INTERCEPT_METADATA = TypeName.create( + "io.helidon.service.inject.api.InterceptionMetadata"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Invoker}. + */ + public static final TypeName INTERCEPT_INVOKER = + TypeName.create("io.helidon.service.inject.api.InterceptionInvoker"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.PerInstanceDescriptor}. + */ + public static final TypeName INJECT_G_PER_INSTANCE_DESCRIPTOR = TypeName.create( + "io.helidon.service.inject.api.GeneratedInjectService.PerInstanceDescriptor"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.QualifiedFactoryDescriptor}. + */ + public static final TypeName INJECT_G_QUALIFIED_FACTORY_DESCRIPTOR = TypeName.create( + "io.helidon.service.inject.api.GeneratedInjectService.QualifiedFactoryDescriptor"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.ScopeHandlerDescriptor}. + */ + public static final TypeName INJECT_G_SCOPE_HANDLER_DESCRIPTOR = TypeName.create( + "io.helidon.service.inject.api.GeneratedInjectService.ScopeHandlerDescriptor"); + /** + * {@link io.helidon.common.types.TypeName} for {@code "io.helidon.service.inject.api.GeneratedInjectService.IpSupport"}. + */ + public static final TypeName INJECT_G_IP_SUPPORT = TypeName.create( + "io.helidon.service.inject.api.GeneratedInjectService.IpSupport"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.SupplierFactoryInterceptionWrapper}. + */ + public static final TypeName INTERCEPT_G_WRAPPER_SUPPLIER_FACTORY = + TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.SupplierFactoryInterceptionWrapper"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.ServicesFactoryInterceptionWrapper}. + */ + public static final TypeName INTERCEPT_G_WRAPPER_SERVICES_FACTORY = + TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.ServicesFactoryInterceptionWrapper"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.IpFactoryInterceptionWrapper}. + */ + public static final TypeName INTERCEPT_G_WRAPPER_IP_FACTORY = + TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.IpFactoryInterceptionWrapper"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.QualifiedFactoryInterceptionWrapper}. + */ + public static final TypeName INTERCEPT_G_WRAPPER_QUALIFIED_FACTORY = + TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.QualifiedFactoryInterceptionWrapper"); + + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.Event.Observer}. + */ + public static final TypeName EVENT_OBSERVER = TypeName.create("io.helidon.service.inject.api.Event.Observer"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.Event.AsyncObserver}. + */ + public static final TypeName EVENT_OBSERVER_ASYNC = TypeName.create("io.helidon.service.inject.api.Event.AsyncObserver"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.Event.Emitter}. + */ + public static final TypeName EVENT_EMITTER = TypeName.create("io.helidon.service.inject.api.Event.Emitter"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.EventManager}. + */ + public static final TypeName EVENT_MANAGER = TypeName.create("io.helidon.service.inject.api.EventManager"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.inject.api.GeneratedInjectService.EventObserverRegistration}. + */ + public static final TypeName INJECT_G_EVENT_OBSERVER_REGISTRATION = + TypeName.create("io.helidon.service.inject.api.GeneratedInjectService.EventObserverRegistration"); + + private InjectCodegenTypes() { + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java new file mode 100644 index 00000000000..66e8e72db63 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectOptions.java @@ -0,0 +1,53 @@ +/* + * 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.codegen; + +import java.util.Set; + +import io.helidon.codegen.Option; +import io.helidon.common.GenericType; +import io.helidon.common.types.TypeName; + +/** + * Supported options specific to Helidon Inject. + */ +public final class InjectOptions { + /** + * Which {@code InterceptionStrategy} to use. + */ + public static final Option INTERCEPTION_STRATEGY = + Option.create("helidon.inject.interceptionStrategy", + "Which interception strategy to use (NONE, EXPLICIT, ALL_RUNTIME, ALL_RETAINED)", + InterceptionStrategy.EXPLICIT, + InterceptionStrategy::valueOf, + GenericType.create(InterceptionStrategy.class)); + + /** + * Additional meta annotations that mark scope annotations. This can be used to include + * jakarta.enterprise.context.NormalScope annotated types as scopes. + */ + public static final Option> SCOPE_META_ANNOTATIONS = + Option.createSet("helidon.inject.scopeMetaAnnotations", + "Additional meta annotations that mark scope annotations. This can be used to include" + + "jakarta.enterprise.context.NormalScope annotated types as scopes.", + Set.of(), + TypeName::create, + new GenericType>() { }); + + 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 new file mode 100644 index 00000000000..28fb4954854 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtension.java @@ -0,0 +1,2396 @@ +/* + * 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.codegen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.classmodel.TypeArgument; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotated; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.Modifier; +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.common.types.TypedElementInfo; +import io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.codegen.RegistryRoundContext; +import io.helidon.service.codegen.ServiceCodegenTypes; +import io.helidon.service.codegen.ServiceSuperType; +import io.helidon.service.codegen.spi.RegistryCodegenExtension; +import io.helidon.service.inject.codegen.spi.InjectAssignment; +import io.helidon.service.inject.codegen.spi.InjectCodegenObserver; +import io.helidon.service.inject.codegen.spi.InjectCodegenObserverProvider; + +import static io.helidon.codegen.CodegenUtil.toConstantName; +import static io.helidon.service.codegen.ServiceCodegenTypes.BUILDER_BLUEPRINT; +import static io.helidon.service.codegen.ServiceCodegenTypes.GENERATED_ANNOTATION; +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_NAMED; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_PER_INSTANCE; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_PER_LOOKUP; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_SCOPE_HANDLER; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_SINGLETON; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_FACTORY_TYPE; +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_SERVICE_INSTANCE; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPTION_DELEGATE; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPTION_EXTERNAL_DELEGATE; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPT_METADATA; +import static java.util.function.Predicate.not; + +class InjectionExtension implements RegistryCodegenExtension { + static final TypeName LIST_OF_ANNOTATIONS = TypeName.builder(TypeNames.LIST) + .addTypeArgument(TypeNames.ANNOTATION) + .build(); + static final TypeName SET_OF_QUALIFIERS = TypeName.builder(TypeNames.SET) + .addTypeArgument(InjectCodegenTypes.INJECT_QUALIFIER) + .build(); + static final TypeName SET_OF_RESOLVED_TYPES = TypeName.builder(TypeNames.SET) + .addTypeArgument(TypeNames.RESOLVED_TYPE_NAME) + .build(); + static final TypeName SET_OF_SIGNATURES = TypeName.builder(TypeNames.SET) + .addTypeArgument(TypeNames.STRING) + .build(); + private static final TypeName LIST_OF_IP_IDS = TypeName.builder(TypeNames.LIST) + .addTypeArgument(InjectCodegenTypes.INJECT_INJECTION_POINT) + .build(); + private static final TypeName SERVICE_SOURCE_TYPE = TypeName.builder(InjectCodegenTypes.INJECT_SERVICE_DESCRIPTOR) + .addTypeArgument(TypeName.create("T")) + .build(); + private static final TypeName GENERATOR = TypeName.create(InjectionExtension.class); + private static final TypeName GENERIC_T_TYPE = TypeName.createFromGenericDeclaration("T"); + private static final TypeName ANY_GENERIC_TYPE = TypeName.builder(TypeNames.GENERIC_TYPE) + .addTypeArgument(TypeName.create("?")) + .build(); + + private final RegistryCodegenContext ctx; + private final Assignments assignments; + private final Interception interception; + private final InterceptionSupport interceptionSupport; + private final Set scopeMetaAnnotations; + private final List observers; + + InjectionExtension(RegistryCodegenContext codegenContext) { + this.ctx = codegenContext; + + CodegenOptions options = codegenContext.options(); + this.interception = new Interception(InjectOptions.INTERCEPTION_STRATEGY.value(options)); + this.scopeMetaAnnotations = InjectOptions.SCOPE_META_ANNOTATIONS.value(options); + + this.interceptionSupport = InterceptionSupport.create(ctx); + this.assignments = new Assignments(ctx); + this.observers = HelidonServiceLoader.create(ServiceLoader.load(InjectCodegenObserverProvider.class, + InjectionExtension.class.getClassLoader())) + .stream() + .map(it -> it.create(codegenContext)) + .toList(); + } + + static void annotationsField(ClassModel.Builder classModel, TypeInfo service) { + classModel.addField(annotations -> annotations + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .isStatic(true) + .isFinal(true) + .name("ANNOTATIONS") + .type(LIST_OF_ANNOTATIONS) + .addContent(List.class) + .addContent(".of(") + .update(it -> { + Iterator iterator = service.annotations() + .stream() + .filter(annot -> !annot.typeName().equals(GENERATED_ANNOTATION)) + .iterator(); + while (iterator.hasNext()) { + Annotation next = iterator.next(); + it.addContentCreate(next); + if (iterator.hasNext()) { + it.addContent(", "); + } + } + }) + .addContent(")")); + } + + static List declareCtrParamsAndGetThem(Method.Builder method, List params) { + List constructorParams = params.stream() + .filter(it -> it.kind() == ElementKind.CONSTRUCTOR) + .toList(); + + // for each parameter, obtain its value from context + for (ParamDefinition param : constructorParams) { + method.addContent(param.declaredType()) + .addContent(" ") + .addContent(param.ipParamName()) + .addContent(" = ") + .update(it -> param.assignmentHandler().accept(it)) + .addContentLine(";"); + } + if (!params.isEmpty()) { + method.addContentLine(""); + } + return constructorParams; + } + + @Override + public void process(RegistryRoundContext roundContext) { + List descriptorsRequired = new ArrayList<>(roundContext.types()); + + for (TypeInfo typeInfo : descriptorsRequired) { + if (typeInfo.hasAnnotation(INTERCEPTION_EXTERNAL_DELEGATE)) { + generateInterceptionExternalDelegates(roundContext, typeInfo); + } + if (typeInfo.hasAnnotation(INJECTION_DESCRIBE)) { + generateScopeDescriptor(roundContext, typeInfo, typeInfo.annotation(INJECTION_DESCRIBE)); + } else { + generateDescriptor(roundContext, descriptorsRequired, typeInfo); + } + } + + notifyObservers(roundContext, descriptorsRequired); + } + + private static void addAnnotationValue(ContentBuilder contentBuilder, Object objectValue) { + switch (objectValue) { + case String value -> contentBuilder.addContent("\"" + value + "\""); + case Boolean value -> contentBuilder.addContent(String.valueOf(value)); + case Long value -> contentBuilder.addContent(String.valueOf(value) + 'L'); + case Double value -> contentBuilder.addContent(String.valueOf(value) + 'D'); + case Integer value -> contentBuilder.addContent(String.valueOf(value)); + case Byte value -> contentBuilder.addContent("(byte)" + value); + case Character value -> contentBuilder.addContent("'" + value + "'"); + case Short value -> contentBuilder.addContent("(short)" + value); + case Float value -> contentBuilder.addContent(String.valueOf(value) + 'F'); + case Class value -> contentBuilder.addContentCreate(TypeName.create(value)); + case TypeName value -> contentBuilder.addContentCreate(value); + case Annotation value -> contentBuilder.addContentCreate(value); + case Enum value -> toEnumValue(contentBuilder, value); + case List values -> toListValues(contentBuilder, values); + default -> throw new IllegalStateException("Unexpected annotation value type " + objectValue.getClass() + .getName() + ": " + objectValue); + } + } + + private static void toListValues(ContentBuilder contentBuilder, List values) { + contentBuilder.addContent(List.class) + .addContent(".of("); + int size = values.size(); + for (int i = 0; i < size; i++) { + Object value = values.get(i); + addAnnotationValue(contentBuilder, value); + if (i != size - 1) { + contentBuilder.addContent(","); + } + } + contentBuilder.addContent(")"); + } + + private static void toEnumValue(ContentBuilder contentBuilder, Enum enumValue) { + contentBuilder.addContent(enumValue.getDeclaringClass()) + .addContent(".") + .addContent(enumValue.name()); + } + + private static void addInterfaceAnnotations(List elementAnnotations, + List declaredElements) { + + for (TypedElements.DeclaredElement declaredElement : declaredElements) { + declaredElement.element() + .annotations() + .forEach(it -> addInterfaceAnnotation(elementAnnotations, it)); + } + } + + private static void addInterfaceAnnotation(List elementAnnotations, Annotation annotation) { + // only add if not already there + if (!elementAnnotations.contains(annotation)) { + elementAnnotations.add(annotation); + } + } + + private TypeName generateProvidedInterceptionDelegate(RegistryRoundContext roundContext, DescribedService service) { + DescribedType providedDescriptor = service.providedDescriptor(); + TypeInfo typeInfo = providedDescriptor + .typeInfo(); + + TypeName expectedInterceptionDelegate = interceptionSupport.interceptedDelegateType(typeInfo.typeName()); + + // now we need to check this is generated + if (ctx.typeInfo(expectedInterceptionDelegate).isPresent()) { + // it was already generated for another provider maybe + return expectedInterceptionDelegate; + } else { + if (canDelegate(service.serviceDescriptor().typeInfo(), + typeInfo)) { + // we must generate it right now + interceptionSupport.generateDelegateInterception(roundContext, + typeInfo, + providedDescriptor.elements(), + expectedInterceptionDelegate); + + return expectedInterceptionDelegate; + } + + throw new CodegenException("Attempting to create delegate interception for non interface type. " + + "If the type is ready to be delegated, annotate it with " + + INTERCEPTION_DELEGATE.fqName() + ", or annotate the service provider with " + + INTERCEPTION_EXTERNAL_DELEGATE.fqName() + + "(" + typeInfo.typeName().classNameWithEnclosingNames() + + ".class) if it is not under your control", + service.serviceDescriptor().typeInfo().originatingElementValue()); + } + } + + private boolean canDelegate(TypeInfo providerType, TypeInfo providedType) { + // interfaces are always supported + if (providedType.kind() == ElementKind.INTERFACE) { + return true; + } + // it is itself marked as a delegate + if (providedType.hasAnnotation(INTERCEPTION_DELEGATE)) { + return true; + } + // only if marked as external delegate + return providerType.hasAnnotation(INTERCEPTION_EXTERNAL_DELEGATE) + && providerType.annotation(INTERCEPTION_EXTERNAL_DELEGATE) + .typeValue().map(it -> providedType.typeName().equals(it)) + .orElse(false); + } + + private void generateInterceptionExternalDelegates(RegistryRoundContext roundContext, TypeInfo typeInfo) { + Annotation annotation = typeInfo.annotation(INTERCEPTION_EXTERNAL_DELEGATE); + List typeNames = annotation.typeValues().orElseGet(List::of); + boolean supportClasses = annotation.booleanValue("classDelegates").orElse(false); + + for (TypeName typeName : typeNames) { + TypeInfo delegateType = ctx.typeInfo(typeName) + .orElseThrow(() -> new CodegenException("Cannot resolve type " + typeName.fqName() + " for " + + " external interception delegates", + typeInfo.originatingElementValue())); + if (!supportClasses && typeInfo.kind() != ElementKind.INTERFACE) { + throw new CodegenException("Attempting to create external delegate interception for non interface type: " + + typeName.fqName(), + typeInfo.originatingElementValue()); + } + interceptionSupport.generateDelegateInterception(roundContext, + delegateType, + delegateType.typeName(), + typeInfo.typeName().packageName()); + } + } + + private void generateScopeDescriptor(RegistryRoundContext roundContext, TypeInfo serviceInfo, Annotation describeAnnotation) { + DescribedService service = DescribedService.create(ctx, + roundContext, + interception, + serviceInfo, + ServiceSuperType.create(), + describeAnnotation.typeValue().orElse(INJECTION_SINGLETON)); + + DescribedType serviceDescriptor = service.serviceDescriptor(); + TypeName serviceTypeName = serviceDescriptor.typeName(); + + ClassModel.Builder classModel = ClassModel.builder() + .copyright(CodegenUtil.copyright(GENERATOR, + serviceTypeName, + service.descriptorType())) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + serviceTypeName, + service.descriptorType(), + "1", + "")) + .addInterface(SERVICE_SOURCE_TYPE) + .type(service.descriptorType()) + .addGenericArgument(TypeArgument.create("T extends " + serviceTypeName.fqName())) + .javadoc(Javadoc.builder() + .add("Service descriptor for {@link " + serviceTypeName.fqName() + "}.") + .addGenericArgument("T", "type of the service, for extensibility") + .build()) + // we need to keep insertion order, as constants may depend on each other + .sortStaticFields(false); + + var contracts = serviceDescriptor.contracts(); + + singletonInstanceField(classModel, service); + + serviceTypeMethod(classModel, service); + descriptorTypeMethod(classModel, service); + scopeMethod(classModel, service); + contractsMethod(classModel, service, contracts, Set.of()); + + // annotations of the type + annotationsField(classModel, serviceDescriptor.typeInfo()); + // add protected constructor + classModel.addConstructor(constructor -> constructor.description("Constructor with no side effects") + .accessModifier(AccessModifier.PROTECTED)); + // methods (some methods define fields as well) + qualifiersMethod(classModel, service); + weightMethod(classModel, service); + runLevelMethod(classModel, service); + factoryType(classModel, service, FactoryType.NONE); + + // service type is an implicit contract + Set serviceContracts = new HashSet<>(contracts); + serviceContracts.add(ResolvedType.create(serviceTypeName)); + + roundContext.addDescriptor("inject", + serviceTypeName, + service.descriptorType(), + classModel, + weight(serviceDescriptor.typeInfo()).orElse(Weighted.DEFAULT_WEIGHT), + serviceContracts, + Set.of(), + serviceDescriptor.typeInfo().originatingElementValue()); + } + + // 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") + private void generateDescriptor(RegistryRoundContext roundContext, + Collection services, + TypeInfo typeInfo) { + if (typeInfo.kind() == ElementKind.INTERFACE || typeInfo.kind() == ElementKind.ANNOTATION_TYPE) { + // we cannot support multiple inheritance, so full descriptors for interfaces do not make sense + return; + } + + TypeName scope = scope(typeInfo).orElse(INJECTION_PER_LOOKUP); + DescribedService service = DescribedService.create(ctx, + roundContext, + interception, + typeInfo, + superType(typeInfo, services), + scope); + DescribedType serviceDescriptor = service.serviceDescriptor(); + DescribedElements serviceElements = serviceDescriptor.elements(); + TypeName serviceTypeName = serviceDescriptor.typeName(); + + List params = new ArrayList<>(); + List methods = new ArrayList<>(); + + TypedElementInfo constructorInjectElement = injectConstructor(typeInfo); + List fieldInjectElements = fieldInjectElements(typeInfo); + + params(services, + service, + methods, + params, + constructorInjectElement, + fieldInjectElements); + + notifyIpObservers(roundContext, service, params); + + ClassModel.Builder classModel = ClassModel.builder() + .copyright(CodegenUtil.copyright(GENERATOR, + serviceTypeName, + service.descriptorType())) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + serviceTypeName, + service.descriptorType(), + "1", + "")) + .type(service.descriptorType()) + .addGenericArgument(TypeArgument.create("T extends " + serviceTypeName.fqName())) + .javadoc(Javadoc.builder() + .add("Service descriptor for {@link " + serviceTypeName.fqName() + "}.") + .addGenericArgument("T", "type of the service, for extensibility") + .build()) + // we need to keep insertion order, as constants may depend on each other + .sortStaticFields(false); + + singletonInstanceField(classModel, service); + + Map genericTypes = genericTypes(classModel, params, methods); + + var contracts = serviceDescriptor.contracts(); + Set factoryContracts; + + if (service.isFactory()) { + if (serviceTypeName.className().endsWith("__Interception_Wrapper")) { + contracts = service.providedDescriptor().contracts(); + factoryContracts = service.serviceDescriptor().contracts(); + } else { + // check if contracts are intercepted + var providedElements = service.providedDescriptor().elements(); + + if (providedElements.methodsIntercepted()) { + // remove contracts from the original service, service descriptor will only be used to instantiate provider + contracts = Set.of(); + factoryContracts = Set.of(); + // generate delegate injection (unless already generated) in current package + TypeName delegateType = generateProvidedInterceptionDelegate(roundContext, service); + // then generate a service that injects the original service, and wraps provider method(s) using delegation + generateDelegationService(roundContext, service, delegateType); + } else { + contracts = service.providedDescriptor().contracts(); + factoryContracts = service.serviceDescriptor().contracts(); + } + } + } else { + factoryContracts = Set.of(); + } + + // declare the class + if (service.superType().present()) { + classModel.superType(service.superType().descriptorType()); + if (service.superType().serviceType().equals("core")) { + classModel.addInterface(SERVICE_SOURCE_TYPE); + } + } else { + classModel.addInterface(SERVICE_SOURCE_TYPE); + } + + // the basic fields and methods + serviceTypeMethod(classModel, service); + descriptorTypeMethod(classModel, service); + scopeMethod(classModel, service); + contractsMethod(classModel, service, contracts, factoryContracts); + qualifiersMethod(classModel, service); + + // Additional fields + + methodFields(classModel, methods); + methodElementFields(classModel, service); + + // public fields are last, so they do not intersect with private fields (it is not as nice to read) + // they cannot be first, as they require some of the private fields + injectionPointFields(classModel, typeInfo, genericTypes, params); + // dependencies require IP IDs, so they really must be last + dependenciesField(classModel, params); + // annotations of the type + annotationsField(classModel, serviceDescriptor.typeInfo()); + + if (serviceElements.intercepted()) { + // if constructor intercepted, add its element + if (serviceElements.constructorIntercepted()) { + constructorElementField(classModel, constructorInjectElement); + } + // if injected field intercepted, add its element (other fields cannot be intercepted) + fieldInjectElements.stream() + .filter(it -> isIntercepted(serviceElements.interceptedElements(), it)) + .forEach(fieldElement -> fieldElementField(classModel, fieldElement)); + // all other interception is done on method level and is handled by the + // service descriptor delegating to a generated type + } + + // add protected constructor + classModel.addConstructor(constructor -> constructor.description("Constructor with no side effects") + .accessModifier(AccessModifier.PROTECTED)); + + // methods (some methods define fields as well) + dependenciesMethod(classModel, service, params); + isAbstractMethod(classModel, service); + instantiateMethod(classModel, service, params); + injectMethod(classModel, service, params, methods); + postConstructMethod(classModel, service); + preDestroyMethod(classModel, service); + weightMethod(classModel, service); + runLevelMethod(classModel, service); + createForMethod(classModel, service); + qualifiedProvider(classModel, service); + scopeHandler(typeInfo, classModel, contracts); + factoryType(classModel, service, service.providerType()); + + // service type is an implicit contract + Set metaInfServiceContracts = new HashSet<>(contracts); + Set metaInfFactoryContracts = new HashSet<>(factoryContracts); + if (service.providedDescriptor() == null || contracts.isEmpty()) { + // this is either NOT a factory, or it is delegated for interception + metaInfServiceContracts.add(ResolvedType.create(serviceTypeName)); + } else { + metaInfFactoryContracts.add(ResolvedType.create(serviceTypeName)); + } + + roundContext.addDescriptor("inject", + serviceTypeName, + service.descriptorType(), + classModel, + weight(typeInfo).orElse(Weighted.DEFAULT_WEIGHT), + metaInfServiceContracts, + metaInfFactoryContracts, + typeInfo.originatingElementValue()); + + if (serviceElements.methodsIntercepted()) { + generateInterceptedType(roundContext, typeInfo, service, constructorInjectElement); + } + } + + private void generateDelegationService(RegistryRoundContext roundContext, + DescribedService service, + TypeName delegateType) { + TypeName typeName = service.serviceDescriptor().typeName(); + + String typeNameSuffix = "__Interception_Wrapper"; + TypeName wrapperType = TypeName.builder() + .packageName(typeName.packageName()) + .className(typeName.classNameWithEnclosingNames().replace('.', '_') + typeNameSuffix) + .build(); + var classModel = ClassModel.builder() + .type(wrapperType) + .addAnnotation(Annotation.create(service.scope())) + .addInterface(service.providerInterface()) + .superType(service.interceptionWrapperSuperType()) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .copyright(CodegenUtil.copyright(GENERATOR, + typeName, + wrapperType)) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + typeName, + wrapperType, + "1", + "")); + service.qualifiers() + .forEach(classModel::addAnnotation); + + classModel.addField(interceptMeta -> interceptMeta + .type(INTERCEPT_METADATA) + .name("interceptMeta") + .isFinal(true) + .accessModifier(AccessModifier.PRIVATE)); + + classModel.addConstructor(ctr -> ctr + .addAnnotation(Annotation.create(INJECTION_INJECT)) + .addParameter(delegate -> delegate + .update(it -> service.qualifiers().forEach(it::addAnnotation)) + .type(typeName) + .name("delegate")) + .addParameter(interceptMeta -> interceptMeta + .type(INTERCEPT_METADATA) + .name("interceptMeta")) + .addContentLine("super(delegate);") + .addContentLine("this.interceptMeta = interceptMeta;") + ); + + TypeName descriptorType = service.descriptorType(); + classModel.addMethod(wrap -> wrap + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(AccessModifier.PROTECTED) + .name("wrap") + .returnType(service.providedDescriptor().typeName()) + .addParameter(instance -> instance + .type(service.providedDescriptor().typeName()) + .name("instance")) + .addContent("return ") + .addContent(delegateType) + .addContentLine(".create(") + .increaseContentPadding() + .increaseContentPadding() + .addContentLine("interceptMeta,") + .addContent(descriptorType) + .addContentLine(".INSTANCE,") + .addContentLine("instance);") + ); + + roundContext.addGeneratedType(wrapperType, + classModel, + typeName, + service.serviceDescriptor().typeInfo().originatingElementValue()); + } + + private void factoryType(ClassModel.Builder classModel, DescribedService service, FactoryType factoryType) { + if (service.superType().empty() && factoryType == FactoryType.SERVICE) { + // default + return; + } + classModel.addMethod(providerTypeMethod -> providerTypeMethod + .name("factoryType") + .accessModifier(AccessModifier.PUBLIC) + .addAnnotation(Annotations.OVERRIDE) + .returnType(INJECT_FACTORY_TYPE) + .addContent("return ") + .addContent(INJECT_FACTORY_TYPE) + .addContent(".") + .addContent(factoryType.name()) + .addContentLine(";") + ); + } + + private void methodElementFields(ClassModel.Builder classModel, + DescribedService service) { + Set elementSignatures = service.serviceDescriptor() + .elements() + .interceptedMethods(); + List interceptedElements = service.serviceDescriptor() + .elements() + .interceptedElements() + .stream() + .filter(it -> elementSignatures.contains(it.element().signature())) + .collect(Collectors.toUnmodifiableList()); + TypeInfo typeInfo = service.serviceDescriptor().typeInfo(); + + for (TypedElements.ElementMeta element : interceptedElements) { + var method = element.element(); + String uniqueName = ctx.uniqueName(typeInfo, method); + String constantName = "METHOD_" + toConstantName(uniqueName); + + // add inherited annotations from interfaces + List elementAnnotations = new ArrayList<>(method.annotations()); + addInterfaceAnnotations(elementAnnotations, element.abstractMethods()); + TypedElementInfo typedElementInfo = TypedElementInfo.builder() + .from(method) + .annotations(elementAnnotations) + .build(); + classModel.addField(constant -> constant + .description("Element info for method: {@code " + method.signature() + "}.") + .accessModifier(AccessModifier.PUBLIC) + .isStatic(true) + .isFinal(true) + .type(TypeNames.TYPED_ELEMENT_INFO) + .name(constantName) + .addContentCreate(typedElementInfo)); + } + /* + TypedElements.gatherElements(typeInfo) + .stream() + .filter(element -> ElementInfoPredicates.isMethod(element.element())) + .filter(element -> !ElementInfoPredicates.isPrivate(element.element())) + .forEach(element -> { + var method = element.element(); + String uniqueName = ctx.uniqueName(typeInfo, method); + String constantName = "METHOD_" + toConstantName(uniqueName); + + // add inherited annotations from interfaces + List elementAnnotations = new ArrayList<>(method.annotations()); + addInterfaceAnnotations(elementAnnotations, element.abstractMethods()); + TypedElementInfo typedElementInfo = TypedElementInfo.builder() + .from(method) + .annotations(elementAnnotations) + .build(); + classModel.addField(constant -> constant + .description("Element info for method: {@code " + method.signature() + "}.") + .accessModifier(AccessModifier.PUBLIC) + .isStatic(true) + .isFinal(true) + .type(TypeNames.TYPED_ELEMENT_INFO) + .name(constantName) + .addContentCreate(typedElementInfo)); + }); + */ + } + + private void generateInterceptedType(RegistryRoundContext roundContext, + TypeInfo typeInfo, + DescribedService service, + TypedElementInfo constructorInjectElement) { + TypeName typeName = service.serviceDescriptor().typeName(); + + TypeName interceptedType = interceptedTypeName(typeName); + + var generator = new InterceptedTypeGenerator(ctx, + typeInfo, + typeName, + service.descriptorType(), + interceptedType, + constructorInjectElement, + service.serviceDescriptor() + .elements() + .interceptedElements() + .stream() + .filter(it -> it.element().kind() == ElementKind.METHOD) + .toList()); + + roundContext.addGeneratedType(interceptedType, + generator.generate(), + typeName, + typeInfo.originatingElementValue()); + } + + private TypeName interceptedTypeName(TypeName serviceType) { + return TypeName.builder(serviceType) + .className(serviceType.classNameWithEnclosingNames().replace('.', '_') + "__Intercepted") + .enclosingNames(List.of()) + .build(); + } + + private void qualifiedProvider(ClassModel.Builder classModel, DescribedService service) { + var typeName = service.qualifiedProviderQualifier(); + if (typeName == null) { + // just use default from interface, we only need to declare this on a type that explicitly implements + // QualifiedProvider + return; + } + classModel.addInterface(INJECT_G_QUALIFIED_FACTORY_DESCRIPTOR); + + classModel.addField(qpField -> qpField + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .name("QP_QUALIFIER") + .type(TypeNames.TYPE_NAME) + .addContentCreate(typeName) + ); + classModel.addMethod(qpMethod -> + qpMethod + .accessModifier(AccessModifier.PUBLIC) + .returnType(TypeNames.TYPE_NAME) + .name("qualifierType") + .addAnnotation(Annotations.OVERRIDE) + .addContentLine("return QP_QUALIFIER;") + ); + } + + private void scopeHandler(TypeInfo typeInfo, ClassModel.Builder classModel, Set contracts) { + if (contracts.stream() + .noneMatch(it -> it.type().equals(INJECTION_SCOPE_HANDLER))) { + return; + } + + TypeName handledScope = findHandledScope(typeInfo); + + classModel.addInterface(INJECT_G_SCOPE_HANDLER_DESCRIPTOR); + classModel.addField(scopeField -> scopeField + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .name("SCOPE_HANDLER_SCOPE") + .type(TypeNames.TYPE_NAME) + .addContentCreate(handledScope)); + + classModel.addMethod(handledScopeMethod -> handledScopeMethod + .accessModifier(AccessModifier.PUBLIC) + .addAnnotation(Annotations.OVERRIDE) + .returnType(TypeNames.TYPE_NAME) + .name("handledScope") + .addContentLine("return SCOPE_HANDLER_SCOPE;")); + } + + private TypeName findHandledScope(TypeInfo typeInfo) { + return typeInfo.findAnnotation(INJECTION_NAMED) + .flatMap(Annotation::value) + .map(TypeName::create) + .orElseThrow(() -> new CodegenException( + "Type implementing ScopeHandler must be qualified with the scope type name: " + typeInfo.typeName())); + } + + private void createForMethod(ClassModel.Builder classModel, + DescribedService service) { + + TypeInfo serviceTypeInfo = service.serviceDescriptor().typeInfo(); + TypeName serviceTypeName = service.serviceDescriptor().typeName(); + + Optional createFor = serviceTypeInfo.findAnnotation(INJECTION_PER_INSTANCE); + if (service.superType().empty() && createFor.isEmpty()) { + // this is the default + return; + } + if (createFor.isPresent()) { + if (service.providerType() != FactoryType.SERVICE) { + throw new CodegenException("Service " + serviceTypeName.classNameWithEnclosingNames() + + " is annotated with @" + + INJECTION_PER_INSTANCE.classNameWithEnclosingNames() + + ", and as such it must not implement any " + + "provider interfaces. Provider type: " + service.providerType(), + serviceTypeInfo.originatingElementValue()); + } + + TypeName createForType = createFor.get() + .typeValue() + .orElseThrow(() -> new CodegenException(INJECTION_PER_INSTANCE.fqName() + + ".value() is required, yet not found on type: " + + serviceTypeName.fqName())); + + String createForClassName = createForType.className(); + if (createForClassName.endsWith("Blueprint")) { + // this may be a config blueprint, use the config instance + Optional createForTypeInfo = ctx.typeInfo(createForType); + if (createForTypeInfo.isPresent()) { + if (createForTypeInfo.get().hasAnnotation(BUILDER_BLUEPRINT)) { + createForType = TypeName.builder(createForType) + .className(createForClassName.substring(0, createForClassName.length() - "Blueprint".length())) + .build(); + } + } + } + + if (createForType.packageName().isBlank()) { + throw new CodegenException(INJECTION_PER_INSTANCE.classNameWithEnclosingNames() + + " type used on " + serviceTypeName.fqName() + " does not have a " + + "package defined. Package is mandatory. If the type is a generated" + + " prototype, please use the Blueprint type instead."); + } + + // used from lambda + TypeName createForTypeFinal = createForType; + classModel.addInterface(InjectCodegenTypes.INJECT_G_PER_INSTANCE_DESCRIPTOR); + + classModel.addField(createForField -> createForField + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .name("CREATE_FOR") + .type(TypeNames.TYPE_NAME) + .addContentCreate(createForTypeFinal)); + + classModel.addMethod(createForMethod -> createForMethod + .accessModifier(AccessModifier.PUBLIC) + .addAnnotation(Annotations.OVERRIDE) + .returnType(TypeNames.TYPE_NAME) + .name("createFor") + .addContentLine("return CREATE_FOR;")); + } + + } + + private ServiceSuperType superType(TypeInfo typeInfo, Collection services) { + // find super type if it is also a service (or has a service descriptor) + + // check if the super type is part of current annotation processing + Optional superTypeInfoOptional = typeInfo.superTypeInfo(); + if (superTypeInfoOptional.isEmpty()) { + return ServiceSuperType.create(); + } + TypeInfo superType = superTypeInfoOptional.get(); + String serviceType = superType.hasAnnotation(SERVICE_ANNOTATION_PROVIDER) + ? "core" + : "inject"; + + TypeName expectedSuperDescriptor = ctx.descriptorType(superType.typeName()); + TypeName superTypeToExtend = TypeName.builder(expectedSuperDescriptor) + .addTypeArgument(TypeName.create("T")) + .build(); + for (TypeInfo service : services) { + if (service.typeName().equals(superType.typeName())) { + return ServiceSuperType.create(service, serviceType, superTypeToExtend); + } + } + // if not found in current list, try checking existing types + return ctx.typeInfo(expectedSuperDescriptor) + .map(it -> ServiceSuperType.create(superType, serviceType, superTypeToExtend)) + .orElseGet(ServiceSuperType::create); + } + + // find constructor with @Inject, if none, find the first constructor (assume @Inject) + private TypedElementInfo injectConstructor(TypeInfo typeInfo) { + var constructors = typeInfo.elementInfo() + .stream() + .filter(it -> it.kind() == ElementKind.CONSTRUCTOR) + .filter(it -> it.hasAnnotation(InjectCodegenTypes.INJECTION_INJECT)) + .collect(Collectors.toUnmodifiableList()); + if (constructors.size() > 1) { + throw new CodegenException("There can only be one constructor annotated with " + + InjectCodegenTypes.INJECTION_INJECT.fqName() + ", but there were " + + constructors.size(), + typeInfo.originatingElementValue()); + } + if (!constructors.isEmpty()) { + // @Injection.Inject + TypedElementInfo first = constructors.getFirst(); + if (ElementInfoPredicates.isPrivate(first)) { + throw new CodegenException("Constructor annotated with " + InjectCodegenTypes.INJECTION_INJECT.fqName() + + " must not be private."); + } + return first; + } + + // or first non-private constructor + var allConstructors = typeInfo.elementInfo() + .stream() + .filter(it -> it.kind() == ElementKind.CONSTRUCTOR) + .collect(Collectors.toUnmodifiableList()); + + if (allConstructors.isEmpty()) { + // there is no constructor declared, we can use default + return TypedElements.DEFAULT_CONSTRUCTOR.element(); + } + var nonPrivateConstructors = allConstructors.stream() + .filter(not(ElementInfoPredicates::isPrivate)) + .collect(Collectors.toUnmodifiableList()); + if (nonPrivateConstructors.isEmpty()) { + throw new CodegenException("There is no non-private constructor defined for " + typeInfo.typeName().fqName(), + typeInfo.originatingElementValue()); + } + if (nonPrivateConstructors.size() > 1) { + throw new CodegenException("There are more non-private constructors defined for " + typeInfo.typeName().fqName(), + typeInfo.originatingElementValue()); + } + return nonPrivateConstructors.getFirst(); + } + + private List fieldInjectElements(TypeInfo typeInfo) { + var injectFields = typeInfo.elementInfo() + .stream() + .filter(not(ElementInfoPredicates::isStatic)) + .filter(ElementInfoPredicates::isField) + .filter(ElementInfoPredicates.hasAnnotation(InjectCodegenTypes.INJECTION_INJECT)) + .toList(); + var firstFound = injectFields.stream() + .filter(ElementInfoPredicates::isPrivate) + .findFirst(); + if (firstFound.isPresent()) { + throw new CodegenException("Discovered " + InjectCodegenTypes.INJECTION_INJECT.fqName() + + " annotation on private field(s). We cannot support private field injection.", + firstFound.get().originatingElementValue()); + } + firstFound = injectFields.stream() + .filter(ElementInfoPredicates::isStatic) + .findFirst(); + if (firstFound.isPresent()) { + throw new CodegenException("Discovered " + InjectCodegenTypes.INJECTION_INJECT.fqName() + + " annotation on static field(s).", + firstFound.get().originatingElementValue()); + } + return injectFields; + } + + private List methodParams(Collection services, + DescribedService service, + List allParams, + AtomicInteger methodCounter, + AtomicInteger paramCounter) { + TypeName serviceType = service.serviceDescriptor().typeName(); + TypeInfo serviceTypeInfo = service.serviceDescriptor().typeInfo(); + + // Discover all methods on this type that are not private or static and that have @Inject + List atInjectMethods = serviceTypeInfo.elementInfo() + .stream() + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(not(ElementInfoPredicates::isStatic)) + .filter(ElementInfoPredicates::isMethod) + .filter(ElementInfoPredicates.hasAnnotation(InjectCodegenTypes.INJECTION_INJECT)) + .toList(); + + List result = new ArrayList<>(); + // add all @Inject methods(always) + // there is no supertype, no need to check anything else + atInjectMethods.stream() + .map(it -> { + TypeName declaringType; + boolean overrides; + + if (service.superType().present()) { + declaringType = overrides(services, + service.superType().typeInfo(), + it, + it.parameterArguments() + .stream() + .map(TypedElementInfo::typeName) + .toList(), + serviceType.packageName()) + .orElse(serviceType); + overrides = !declaringType.equals(serviceType); + } else { + declaringType = serviceType; + overrides = false; + } + return toMethodDefinition(service, + allParams, + methodCounter, + paramCounter, + it, + declaringType, + overrides, + true); + }) + .forEach(result::add); + + if (service.superType().empty()) { + return result; + } + + // discover all methods that are not private or static and that do NOT have @Inject + List otherMethods = serviceTypeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isStatic)) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(it -> !it.hasAnnotation(InjectCodegenTypes.INJECTION_INJECT)) + .toList(); + + // some of the methods we declare that do not have @Inject may disable injection, we need to check that + + for (TypedElementInfo otherMethod : otherMethods) { + // now find all methods that override a method that is annotated from any supertype (ouch) + Optional overrides = overrides(services, + service.superType().typeInfo(), + otherMethod, + otherMethod.parameterArguments() + .stream() + .map(TypedElementInfo::typeName) + .toList(), + serviceType.packageName(), + InjectCodegenTypes.INJECTION_INJECT); + if (overrides.isPresent()) { + // we do override a method, we need to declare it + result.add(toMethodDefinition(service, + allParams, + methodCounter, + paramCounter, + otherMethod, + overrides.get(), + true, + false)); + } + } + return result; + } + + @SuppressWarnings("checkstyle:ParameterNumber") // there is no sense in creating an object when all are required + private MethodDefinition toMethodDefinition(DescribedService service, + List allParams, + AtomicInteger methodCounter, + AtomicInteger paramCounter, + TypedElementInfo method, + TypeName declaringType, + boolean overrides, + boolean isInjectionPoint) { + + int methodIndex = methodCounter.getAndIncrement(); + String methodId = method.elementName() + "_" + methodIndex; + String constantName = "METHOD_" + methodIndex; + List methodParams = toMethodParams(service, paramCounter, method, methodId, constantName); + + if (isInjectionPoint) { + // we want to declare these + allParams.addAll(methodParams); + } + + return new MethodDefinition(declaringType, + method.accessModifier(), + methodId, + constantName, + method.elementName(), + overrides, + methodParams, + isInjectionPoint, + method.elementModifiers().contains(Modifier.FINAL)); + } + + private List toMethodParams(DescribedService service, + AtomicInteger paramCounter, + TypedElementInfo method, + String methodId, + String methodConstantName) { + return method.parameterArguments() + .stream() + .map(param -> { + String constantName = "IP_PARAM_" + paramCounter.getAndIncrement(); + var assignment = translateParameter(param.typeName(), constantName); + return new ParamDefinition(method, + methodConstantName, + param, + constantName, + param.typeName(), + assignment.usedType(), + assignment.codeGenerator(), + ElementKind.METHOD, + method.elementName(), + param.elementName(), + methodId + "_" + param.elementName(), + method.elementModifiers().contains(Modifier.STATIC), + param.annotations(), + qualifiers(service, param), + contract("Method " + service.serviceDescriptor().typeName() + .fqName() + "#" + method.elementName() + ", parameter: " + + param.elementName(), + assignment.usedType()), + method.accessModifier(), + methodId); + }) + .toList(); + } + + /** + * Find if there is a method in the hierarchy that this method overrides. + * + * @param services list of services being processed + * @param type first immediate supertype we will be checking + * @param method method we are investigating + * @param arguments method signature + * @param currentPackage package of the current type declaring the method + * @param expectedAnnotations only look for methods annotated with a specific annotation + * @return type name of the top level declaring type of this method + */ + private Optional overrides(Collection services, + TypeInfo type, + TypedElementInfo method, + List arguments, + String currentPackage, + TypeName... expectedAnnotations) { + + String methodName = method.elementName(); + // we look only for exact match (including types) + Optional found = type.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(ElementInfoPredicates.elementName(methodName)) + .filter(it -> { + for (TypeName expectedAnnotation : expectedAnnotations) { + if (!it.hasAnnotation(expectedAnnotation)) { + return false; + } + } + return true; + }) + .filter(ElementInfoPredicates.hasParams(arguments)) + .findFirst(); + + // if found, we either return this one, or look further up the hierarchy + if (found.isPresent()) { + TypedElementInfo superMethod = found.get(); + + // method has same signature, but is package local and is in a different package + boolean realOverride = superMethod.accessModifier() != AccessModifier.PACKAGE_PRIVATE + || currentPackage.equals(type.typeName().packageName()); + + if (realOverride) { + // let's find the declaring type + ServiceSuperType superType = superType(type, services); + if (superType.present()) { + // do not care about annotations, we already have a match + var fromSuperHierarchy = overrides(services, superType.typeInfo(), method, arguments, currentPackage); + return Optional.of(fromSuperHierarchy.orElseGet(type::typeName)); + } + // there is no supertype, this type declares the method + return Optional.of(type.typeName()); + } + } + + // we did not find a method on this type, let's look above + ServiceSuperType superType = superType(type, services); + if (superType.present()) { + return overrides(services, superType.typeInfo(), method, arguments, currentPackage, expectedAnnotations); + } + return Optional.empty(); + } + + private void params(Collection services, + DescribedService describedService, + List methods, + List params, + TypedElementInfo constructor, + List fieldInjectElements) { + AtomicInteger paramCounter = new AtomicInteger(); + AtomicInteger methodCounter = new AtomicInteger(); + + if (!constructor.parameterArguments().isEmpty()) { + injectConstructorParams(describedService, params, paramCounter, constructor); + } + + fieldInjectElements + .forEach(it -> fieldParam(describedService, params, paramCounter, it)); + + methods.addAll(methodParams(services, + describedService, + params, + methodCounter, + paramCounter)); + + } + + private void injectConstructorParams(DescribedService service, + List result, + AtomicInteger paramCounter, + TypedElementInfo constructor) { + constructor.parameterArguments() + .stream() + .map(param -> { + String constantName = "IP_PARAM_" + paramCounter.getAndIncrement(); + var assignment = translateParameter(param.typeName(), constantName); + return new ParamDefinition(constructor, + null, + param, + constantName, + param.typeName(), + assignment.usedType(), + assignment.codeGenerator(), + ElementKind.CONSTRUCTOR, + constructor.elementName(), + param.elementName(), + param.elementName(), + false, + param.annotations(), + qualifiers(service, param), + contract(service.serviceDescriptor().typeName() + .fqName() + " Constructor parameter: " + param.elementName(), + assignment.usedType()), + constructor.accessModifier(), + ""); + }) + .forEach(result::add); + } + + private void fieldParam(DescribedService describedService, + List result, + AtomicInteger paramCounter, + TypedElementInfo field) { + String constantName = "IP_PARAM_" + paramCounter.getAndIncrement(); + var assignment = translateParameter(field.typeName(), constantName); + + result.add(new ParamDefinition(field, + null, + field, + constantName, + field.typeName(), + assignment.usedType(), + assignment.codeGenerator(), + ElementKind.FIELD, + field.elementName(), + field.elementName(), + field.elementName(), + field.elementModifiers().contains(Modifier.STATIC), + field.annotations(), + qualifiers(describedService, field), + contract("Field " + describedService.serviceDescriptor().typeName().fqName() + + "." + field.elementName(), + assignment.usedType()), + field.accessModifier(), + null)); + } + + private Set qualifiers(DescribedService service, Annotated element) { + Set result = new LinkedHashSet<>(); + + for (Annotation anno : element.annotations()) { + if (service.serviceDescriptor() + .typeInfo() + .hasMetaAnnotation(anno.typeName(), InjectCodegenTypes.INJECTION_QUALIFIER)) { + result.add(anno); + } + } + + // note: should qualifiers be inheritable? Right now we assume not to support the jsr-330 spec (see note above). + return result; + } + + private TypeName contract(String description, TypeName typeName) { + /* + get the contract expected for this injection point + IP may be: + - Optional + - List + - ServiceProvider + - Supplier + - Optional + - Optional + - List + - List + - ServiceInstance + - List + - Optional + */ + + if (typeName.isOptional()) { + if (typeName.typeArguments().isEmpty()) { + throw new IllegalArgumentException("Injection point with Optional type must have a declared type argument: " + + description); + } + TypeName firstType = typeName.typeArguments().getFirst(); + if (firstType.equals(INJECT_SERVICE_INSTANCE)) { + if (typeName.typeArguments().isEmpty()) { + throw new IllegalArgumentException("Injection point with Optional type must have a" + + " declared type argument: " + description); + } + return contract(description, firstType.typeArguments().getFirst()); + } else { + return contract(description, firstType); + } + } + if (typeName.isList()) { + if (typeName.typeArguments().isEmpty()) { + throw new IllegalArgumentException("Injection point with List type must have a declared type argument: " + + description); + } + TypeName firstType = typeName.typeArguments().getFirst(); + if (firstType.equals(INJECT_SERVICE_INSTANCE)) { + if (typeName.typeArguments().isEmpty()) { + throw new IllegalArgumentException("Injection point with List type must have a" + + " declared type argument: " + description); + } + return contract(description, firstType.typeArguments().getFirst()); + } else { + return contract(description, firstType); + } + } + if (typeName.isSupplier()) { + if (typeName.typeArguments().isEmpty()) { + throw new IllegalArgumentException("Injection point with Supplier type must have a declared type argument: " + + description); + } + return contract(description, typeName.typeArguments().getFirst()); + } + if (typeName.equals(INJECT_SERVICE_INSTANCE)) { + if (typeName.typeArguments().isEmpty()) { + throw new IllegalArgumentException("Injection point with ServiceInstance type must have a" + + " declared type argument: " + description); + } + return contract(description, typeName.typeArguments().getFirst()); + } + + return typeName; + } + + private Map genericTypes(ClassModel.Builder classModel, + List params, + List methods) { + // we must use map by string (as type name is equal if the same class, not full generic declaration) + Map result = new LinkedHashMap<>(); + AtomicInteger counter = new AtomicInteger(); + + for (ParamDefinition param : params) { + result.computeIfAbsent(param.translatedType().resolvedName(), + type -> { + var response = new GenericTypeDeclaration("TYPE_" + counter.getAndIncrement(), + param.declaredType()); + addTypeConstant(classModel, param.translatedType(), response); + return response; + }); + result.computeIfAbsent(param.contract().resolvedName(), + type -> { + var response = new GenericTypeDeclaration("TYPE_" + counter.getAndIncrement(), + param.declaredType()); + addTypeConstant(classModel, param.contract(), response); + return response; + }); + } + + for (MethodDefinition method : methods) { + for (ParamDefinition param : method.params()) { + result.computeIfAbsent(param.declaredType().resolvedName(), + type -> { + var response = + new GenericTypeDeclaration("TYPE_" + counter.getAndIncrement(), + param.declaredType()); + addTypeConstant(classModel, + param.declaredType(), + response + ); + return response; + }); + } + } + + return result; + } + + private void addTypeConstant(ClassModel.Builder classModel, + TypeName typeName, + GenericTypeDeclaration generic) { + String stringType = typeName.resolvedName(); + // constants for injection point parameter types (used by next section) + classModel.addField(field -> field + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(TypeNames.TYPE_NAME) + .name(generic.constantName()) + .update(it -> { + if (stringType.indexOf('.') < 0) { + // there is no package, we must use class (if this is a generic type, we have a problem) + it.addContent(TypeNames.TYPE_NAME) + .addContent(".create(") + .addContent(typeName) + .addContent(".class)"); + } else { + it.addContentCreate(typeName); + } + })); + classModel.addField(field -> field + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(ANY_GENERIC_TYPE) + .name("G" + generic.constantName()) + .update(it -> { + if (typeName.primitive()) { + it.addContent(TypeNames.GENERIC_TYPE) + .addContent(".create(") + .addContent(typeName.className()) + .addContent(".class)"); + } else { + it.addContent("new ") + .addContent(TypeNames.GENERIC_TYPE) + .addContent("<") + .addContent(typeName) + .addContent(">() {}"); + } + }) + ); + } + + private Optional scope(TypeInfo service) { + Set result = new LinkedHashSet<>(); + + for (Annotation anno : service.annotations()) { + TypeName annoType = anno.typeName(); + if (service.hasMetaAnnotation(annoType, InjectCodegenTypes.INJECTION_SCOPE)) { + result.add(annoType); + continue; + } + for (TypeName scopeMetaAnnotation : scopeMetaAnnotations) { + if (service.hasMetaAnnotation(annoType, scopeMetaAnnotation)) { + result.add(annoType); + } + } + } + + if (result.size() > 1) { + throw new CodegenException("Type " + service.typeName().fqName() + " has more than one scope defined. " + + "This is not supported. Scopes. " + result); + } + + if (result.isEmpty() && service.hasAnnotation(INJECTION_PER_INSTANCE)) { + result.add(INJECTION_SINGLETON); + } + + return result.stream().findFirst(); + } + + private void singletonInstanceField(ClassModel.Builder classModel, DescribedService service) { + // singleton instance of the descriptor + classModel.addField(instance -> instance.description("Global singleton instance for this descriptor.") + .accessModifier(AccessModifier.PUBLIC) + .isStatic(true) + .isFinal(true) + .type(descriptorInstanceType(service.serviceDescriptor().typeName(), service.descriptorType())) + .name("INSTANCE") + .defaultValueContent("new " + service.descriptorType().className() + "<>()")); + } + + private void injectionPointFields(ClassModel.Builder classModel, + TypeInfo service, + Map genericTypes, + List params) { + // constant for injection points + for (ParamDefinition param : params) { + classModel.addField(field -> field + .accessModifier(AccessModifier.PUBLIC) + .isStatic(true) + .isFinal(true) + .type(InjectCodegenTypes.INJECT_INJECTION_POINT) + .name(param.constantName()) + .description(ipIdDescription(service, param)) + .update(it -> { + it.addContent(InjectCodegenTypes.INJECT_INJECTION_POINT) + .addContentLine(".builder()") + .increaseContentPadding() + .increaseContentPadding() + .addContent(".typeName(") + .addContent(genericTypes.get(param.translatedType().resolvedName()).constantName()) + .addContentLine(")") + .update(maybeElementKind -> { + if (param.kind() != ElementKind.CONSTRUCTOR) { + // constructor is default and does not need to be defined + maybeElementKind.addContent(".elementKind(") + .addContent(TypeNames.ELEMENT_KIND) + .addContent(".") + .addContent(param.kind().name()) + .addContentLine(")"); + } + }) + .update(maybeMethod -> { + if (param.kind() == ElementKind.METHOD) { + maybeMethod.addContent(".method(") + .addContent(param.methodConstantName()) + .addContentLine(")"); + } + }) + .addContent(".name(\"") + .addContent(param.fieldId()) + .addContentLine("\")") + .addContentLine(".service(SERVICE_TYPE)") + .addContentLine(".descriptor(TYPE)") + .addContent(".descriptorConstant(\"") + .addContent(param.constantName()) + .addContentLine("\")") + .addContent(".contract(") + .addContent(genericTypes.get(param.contract().resolvedName()).constantName()) + .addContentLine(")") + .addContent(".contractType(G") + .addContent(genericTypes.get(param.contract().resolvedName()).constantName()) + .addContentLine(")"); + if (param.access() != AccessModifier.PACKAGE_PRIVATE) { + it.addContent(".access(") + .addContent(TypeNames.ACCESS_MODIFIER) + .addContent(".") + .addContent(param.access().name()) + .addContentLine(")"); + } + + if (param.isStatic()) { + it.addContentLine(".isStatic(true)"); + } + + if (!param.qualifiers().isEmpty()) { + for (Annotation qualifier : param.qualifiers()) { + it.addContent(".addQualifier(qualifier -> qualifier.typeName(") + .addContentCreate(qualifier.typeName().genericTypeName()) + .addContentLine(")"); + qualifier.values() + .keySet() + .forEach(propertyName -> { + it.addContent(".putValue(\"") + .addContent(propertyName) + .addContent("\", "); + addAnnotationValue(it, qualifier.objectValue(propertyName).get()); + it.addContentLine(")"); + }); + it.addContentLine(")"); + } + } + + it.addContent(".build()") + .decreaseContentPadding() + .decreaseContentPadding(); + })); + } + } + + private String ipIdDescription(TypeInfo service, ParamDefinition param) { + TypeName serviceType = service.typeName(); + StringBuilder result = new StringBuilder("Injection point dependency for "); + boolean servicePublic = service.accessModifier() == AccessModifier.PUBLIC; + boolean elementPublic = param.owningElement().accessModifier() == AccessModifier.PUBLIC; + + if (servicePublic) { + result.append("{@link ") + .append(serviceType.fqName()); + if (!elementPublic) { + result.append("}"); + } + } else { + result.append(serviceType.classNameWithEnclosingNames()); + } + + if (servicePublic && elementPublic) { + // full javadoc reference + switch (param.kind()) { + case CONSTRUCTOR -> result + .append("#") + .append(serviceType.className()) + .append("(") + .append(toDescriptionSignature(param.owningElement(), true)) + .append(")"); + case METHOD -> result + .append("#") + .append(param.owningElement().elementName()) + .append("(") + .append(toDescriptionSignature(param.owningElement(), true)) + .append(")"); + case FIELD -> result + .append("#") + .append(param.elementInfo().elementName()); + default -> { + } // do nothing, this should not be possible + } + result.append("}"); + } else { + // just text + switch (param.kind()) { + case CONSTRUCTOR -> result.append("(") + .append(toDescriptionSignature(param.owningElement(), false)) + .append(")"); + case METHOD -> result.append("#") + .append(param.owningElement().elementName()) + .append("(") + .append(toDescriptionSignature(param.owningElement(), false)) + .append(")"); + case FIELD -> result.append(".") + .append(param.elementInfo().elementName()); + default -> { + } // do nothing, this should not be possible + } + } + + switch (param.kind()) { + case CONSTRUCTOR, METHOD -> result + .append(", parameter ") + .append(param.elementInfo().elementName()); + default -> { + } // do nothing, this should not be possible + } + + result.append("."); + return result.toString(); + } + + private String toDescriptionSignature(TypedElementInfo method, boolean javadoc) { + if (javadoc) { + return method.parameterArguments() + .stream() + .map(it -> it.typeName().fqName()) + .collect(Collectors.joining(", ")); + } else { + return method.parameterArguments() + .stream() + .map(it -> it.typeName().classNameWithEnclosingNames() + " " + it.elementName()) + .collect(Collectors.joining(", ")); + } + } + + private void dependenciesField(ClassModel.Builder classModel, List params) { + classModel.addField(dependencies -> dependencies + .isStatic(true) + .isFinal(true) + .name("DEPENDENCIES") + .type(LIST_OF_IP_IDS) + .addContent(List.class) + .addContent(".of(") + .update(it -> { + Iterator iterator = params.iterator(); + while (iterator.hasNext()) { + it.addContent(iterator.next().constantName()); + if (iterator.hasNext()) { + it.addContent(", "); + } + } + }) + .addContent(")")); + } + + private void methodFields(ClassModel.Builder classModel, List methods) { + for (MethodDefinition method : methods) { + classModel.addField(methodField -> methodField + .isStatic(true) + .isFinal(true) + .name(method.constantName()) + .type(TypeNames.STRING) + .update(it -> fieldForMethodConstantBody(method, it))); + } + } + + private void fieldForMethodConstantBody(MethodDefinition method, Field.Builder fieldBuilder) { + // fully.qualified.Type.methodName(fully.qualified.Params,another.Param) + fieldBuilder.addContent("\"") + .addContent(method.declaringType().fqName()) + .addContent(".") + .addContent(method.methodName()) + .addContent("(") + .addContent(method.params().stream() + .map(ParamDefinition::declaredType) + .map(TypeName::resolvedName) + .collect(Collectors.joining(","))) + .addContent(")\""); + } + + private void fieldElementField(ClassModel.Builder classModel, TypedElementInfo fieldElement) { + classModel.addField(ctorElement -> ctorElement + .isStatic(true) + .isFinal(true) + .name(fieldElementConstantName(fieldElement.elementName())) + .type(TypeNames.TYPED_ELEMENT_INFO) + .addContentCreate(fieldElement)); + } + + private String fieldElementConstantName(String elementName) { + return "FIELD_INFO_" + toConstantName(elementName); + } + + private void constructorElementField(ClassModel.Builder classModel, TypedElementInfo constructorInjectElement) { + classModel.addField(ctorElement -> ctorElement + .isStatic(true) + .isFinal(true) + .name("CTOR_ELEMENT") + .type(TypeNames.TYPED_ELEMENT_INFO) + .addContentCreate(constructorInjectElement)); + } + + private void serviceTypeMethod(ClassModel.Builder classModel, DescribedService service) { + classModel.addField(field -> field + .isStatic(true) + .isFinal(true) + .accessModifier(AccessModifier.PRIVATE) + .type(TypeNames.TYPE_NAME) + .name("SERVICE_TYPE") + .addContentCreate(service.serviceDescriptor().typeName().genericTypeName())); + + // TypeName serviceType() + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .returnType(TypeNames.TYPE_NAME) + .name("serviceType") + .addContentLine("return SERVICE_TYPE;")); + } + + private void descriptorTypeMethod(ClassModel.Builder classModel, DescribedService service) { + classModel.addField(field -> field + .isStatic(true) + .isFinal(true) + .accessModifier(AccessModifier.PRIVATE) + .type(TypeNames.TYPE_NAME) + .name("TYPE") + .addContentCreate(service.descriptorType().genericTypeName())); + + // TypeName descriptorType() + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .returnType(TypeNames.TYPE_NAME) + .name("descriptorType") + .addContentLine("return TYPE;")); + } + + private void contractsMethod(ClassModel.Builder classModel, + DescribedService service, + Set serviceContracts, + Set factoryContracts) { + var superType = service.superType(); + + if (!serviceContracts.isEmpty() || superType.present()) { + classModel.addField(contractsField -> contractsField + .isStatic(true) + .isFinal(true) + .name("CONTRACTS") + .type(SET_OF_RESOLVED_TYPES) + .addContent(Set.class) + .addContent(".of(") + .update(it -> { + Iterator iterator = serviceContracts.iterator(); + while (iterator.hasNext()) { + it.addContentCreate(iterator.next()); + if (iterator.hasNext()) { + it.addContent(", "); + } + } + }) + .addContent(")")); + + // Set> contracts() + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .name("contracts") + .returnType(SET_OF_RESOLVED_TYPES) + .addContentLine("return CONTRACTS;")); + } + if (!factoryContracts.isEmpty() || superType.present()) { + // we must declare the contracts method + classModel.addField(contractsField -> contractsField + .isStatic(true) + .isFinal(true) + .name("FACTORY_CONTRACTS") + .type(SET_OF_RESOLVED_TYPES) + .addContent(Set.class) + .addContent(".of(") + .update(it -> { + Iterator iterator = factoryContracts.iterator(); + while (iterator.hasNext()) { + it.addContentCreate(iterator.next()); + if (iterator.hasNext()) { + it.addContent(", "); + } + } + }) + .addContent(")")); + + // Set> factoryContracts() + classModel.addMethod(method -> method + .addAnnotation(Annotations.OVERRIDE) + .name("factoryContracts") + .returnType(SET_OF_RESOLVED_TYPES) + .addContentLine("return FACTORY_CONTRACTS;")); + } + } + + private void dependenciesMethod(ClassModel.Builder classModel, DescribedService service, List params) { + // List dependencies() + boolean hasSuperType = service.superType().present(); + if (hasSuperType || !params.isEmpty()) { + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .returnType(LIST_OF_IP_IDS) + .name("dependencies") + .update(it -> { + if (hasSuperType && !service.superType().serviceType().equals("core")) { + it.addContent("return ") + .addContent(INJECT_G_IP_SUPPORT) + .addContentLine(".combineIps(DEPENDENCIES, super.dependencies());"); + } else { + // when super type is a core service, it only can have constructor dependencies - no need to combine + it.addContentLine("return DEPENDENCIES;"); + } + })); + } + } + + private void instantiateMethod(ClassModel.Builder classModel, + DescribedService service, + List params) { + DescribedType serviceDescriptor = service.serviceDescriptor(); + if (serviceDescriptor.isAbstract()) { + return; + } + + // T instantiate(InjectionContext ctx__helidonInject, InterceptionMetadata interceptMeta__helidonInject) + var elements = serviceDescriptor.elements(); + TypeName toInstantiate = elements.methodsIntercepted() + ? interceptedTypeName(serviceDescriptor.typeName()) + : serviceDescriptor.typeName(); + + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .returnType(serviceDescriptor.typeName()) + .name("instantiate") + .addParameter(ctxParam -> ctxParam.type(ServiceCodegenTypes.SERVICE_DEPENDENCY_CONTEXT) + .name("ctx__helidonInject")) + .addParameter(interceptMeta -> interceptMeta.type(InjectCodegenTypes.INTERCEPT_METADATA) + .name("interceptMeta__helidonInject")) + .update(it -> { + if (elements.constructorIntercepted()) { + createInstantiateInterceptBody(it, params); + } else { + createInstantiateBody(toInstantiate, it, params, elements.methodsIntercepted()); + } + })); + + if (elements.constructorIntercepted()) { + classModel.addMethod(method -> method.returnType(serviceDescriptor.typeName()) + .name("doInstantiate") + .accessModifier(AccessModifier.PRIVATE) + .addParameter(interceptMeta -> interceptMeta.type(InjectCodegenTypes.INTERCEPT_METADATA) + .name("interceptMeta")) + .addParameter(ctrParams -> ctrParams.type(TypeName.create("Object...")) + .name("params")) + .update(it -> createDoInstantiateBody(toInstantiate, it, params, elements.methodsIntercepted()))); + } + } + + private void createInstantiateInterceptBody(Method.Builder method, + List params) { + List constructorParams = declareCtrParamsAndGetThem(method, params); + + method.addContentLine("try {") + .addContentLine("return interceptMeta__helidonInject.createInvoker(this,") + .increaseContentPadding() + .increaseContentPadding() + .increaseContentPadding() + .addContentLine("QUALIFIERS,") + .addContentLine("ANNOTATIONS,") + .addContentLine("CTOR_ELEMENT,") + .addContentLine("params__helidonInject -> doInstantiate(interceptMeta__helidonInject, params__helidonInject),") + .addContent(Set.class) + .addContentLine(".of())") // checked exceptions in constructor not supported, as there is no consumer + .decreaseContentPadding() + .decreaseContentPadding() + .addContent(".invoke(") + .addContent(constructorParams.stream() + .map(ParamDefinition::ipParamName) + .collect(Collectors.joining(", "))) + .addContentLine(");") + .decreaseContentPadding() + .addContentLine("} catch (RuntimeException e__helidonInject) {") + .addContentLine("throw e__helidonInject;") + .addContentLine("} catch (Exception e__helidonInject) {") + .addContent(" throw new ") + .addContent(InjectCodegenTypes.INTERCEPT_EXCEPTION) + .addContentLine("(\"Failed to instantiate \" + SERVICE_TYPE.fqName(), e__helidonInject, false);") + .addContentLine("}"); + } + + private void createInstantiateBody(TypeName serviceType, + Method.Builder method, + List params, + boolean interceptedMethods) { + List constructorParams = declareCtrParamsAndGetThem(method, params); + String paramsDeclaration = constructorParams.stream() + .map(ParamDefinition::ipParamName) + .collect(Collectors.joining(", ")); + + if (interceptedMethods) { + // return new MyImpl__Intercepted(interceptMeta, this, ANNOTATIONS, casted params + method.addContent("return new ") + .addContent(serviceType.genericTypeName()) + .addContentLine("(interceptMeta__helidonInject,") + .increaseContentPadding() + .increaseContentPadding() + .addContentLine("this,") + .addContentLine("QUALIFIERS,") + .addContent("ANNOTATIONS"); + if (!constructorParams.isEmpty()) { + method.addContentLine(","); + method.addContent(paramsDeclaration); + } + method.addContentLine(");") + .decreaseContentPadding() + .decreaseContentPadding(); + } else { + // return new MyImpl(parameter, parameter2) + method.addContent("return new ") + .addContent(serviceType.genericTypeName()) + .addContent("(") + .addContent(paramsDeclaration) + .addContentLine(");"); + } + boolean hasGenericType = constructorParams.stream() + .anyMatch(it -> !it.declaredType().typeArguments().isEmpty()); + + if (hasGenericType) { + method.addAnnotation(Annotation.create(TypeName.create(SuppressWarnings.class), "unchecked")); + } + } + + private void createDoInstantiateBody(TypeName serviceType, + Method.Builder method, + List params, + boolean interceptedMethods) { + List constructorParams = params.stream() + .filter(it -> it.kind() == ElementKind.CONSTRUCTOR) + .toList(); + + List paramDeclarations = new ArrayList<>(); + for (int i = 0; i < constructorParams.size(); i++) { + ParamDefinition param = constructorParams.get(i); + paramDeclarations.add("(" + param.declaredType().resolvedName() + ") params[" + i + "]"); + } + String paramsDeclaration = String.join(", ", paramDeclarations); + + if (interceptedMethods) { + method.addContent("return new ") + .addContent(serviceType.genericTypeName()) + .addContentLine("(interceptMeta,") + .addContentLine("this,") + .addContentLine("QUALIFIERS,") + .addContent("ANNOTATIONS"); + if (!constructorParams.isEmpty()) { + method.addContentLine(","); + method.addContent(paramsDeclaration); + } + method.addContentLine(");"); + } else { + // return new MyImpl(IP_PARAM_0.type().cast(params[0]) + method.addContent("return new ") + .addContent(serviceType.genericTypeName()) + .addContent("(") + .addContent(paramsDeclaration) + .addContentLine(");"); + } + } + + private void isAbstractMethod(ClassModel.Builder classModel, DescribedService service) { + boolean isAbstract = service.serviceDescriptor().isAbstract(); + + if (!isAbstract && service.superType().empty()) { + return; + } + // only override for abstract types (and subtypes, where we do not want to check if super is abstract), default is false + classModel.addMethod(isAbstractMethod -> isAbstractMethod + .name("isAbstract") + .returnType(TypeNames.PRIMITIVE_BOOLEAN) + .addAnnotation(Annotations.OVERRIDE) + .addContentLine("return " + isAbstract + ";")); + } + + private void injectMethod(ClassModel.Builder classModel, + DescribedService service, + List params, + List methods) { + + // method for field and method injections + List fields = params.stream() + .filter(it -> it.kind() == ElementKind.FIELD) + .toList(); + if (fields.isEmpty() && methods.isEmpty()) { + // only generate this method if we do something + return; + } + classModel.addMethod(method -> method.addAnnotation(Annotations.OVERRIDE) + .name("inject") + .addParameter(ctxParam -> ctxParam.type(ServiceCodegenTypes.SERVICE_DEPENDENCY_CONTEXT) + .name("ctx__helidonInject")) + .addParameter(interceptMeta -> interceptMeta.type(InjectCodegenTypes.INTERCEPT_METADATA) + .name("interceptMeta__helidonInject")) + .addParameter(injectedParam -> injectedParam.type(SET_OF_SIGNATURES) + .name("injected__helidonInject")) + .addParameter(instanceParam -> instanceParam.type(GENERIC_T_TYPE) + .name("instance__helidonInject")) + .update(it -> createInjectBody(it, + service.superType().present(), + methods, + fields, + service.serviceDescriptor().elements().intercepted(), + service.serviceDescriptor().elements().interceptedElements()))); + } + + private void createInjectBody(Method.Builder methodBuilder, + boolean hasSuperType, + List methods, + List fields, + boolean canIntercept, + List maybeIntercepted) { + + boolean hasGenericType = methods.stream() + .flatMap(it -> it.params().stream()) + .anyMatch(it -> !it.declaredType().typeArguments().isEmpty()) + || fields.stream() + .anyMatch(it -> !it.declaredType().typeArguments().isEmpty()); + + if (hasGenericType) { + methodBuilder.addAnnotation(Annotation.create(TypeName.create(SuppressWarnings.class), "unchecked")); + } + + // two passes for methods - first mark method to be injected, then call super, then inject + for (MethodDefinition method : methods) { + if (method.isInjectionPoint() && !method.isFinal()) { + methodBuilder.addContentLine("boolean " + method.invokeName() + + " = injected__helidonInject.add(" + method.constantName() + ")" + + ";"); + } else { + methodBuilder.addContentLine("injected__helidonInject.add(" + method.constantName() + ");"); + } + } + methodBuilder.addContentLine(""); + + if (hasSuperType) { + // must be done at the very end, so the same method is not injected first in the supertype + methodBuilder.addContentLine("super.inject(ctx__helidonInject, interceptMeta__helidonInject, " + + "injected__helidonInject, instance__helidonInject);"); + methodBuilder.addContentLine(""); + } + + /* + Inject fields + */ + for (ParamDefinition field : fields) { + /* + instance.myField = ctx__helidonInject(IP_PARAM_X) + */ + injectFieldBody(methodBuilder, field, canIntercept, maybeIntercepted); + } + + if (!fields.isEmpty()) { + methodBuilder.addContentLine(""); + } + + // now finally invoke the methods + for (MethodDefinition method : methods) { + if (!method.isInjectionPoint()) { + // this method "disabled" injection point from superclass + continue; + } + if (!method.isFinal()) { + methodBuilder.addContentLine("if (" + method.invokeName() + ") {"); + } + List params = method.params(); + + methodBuilder.addContent("instance__helidonInject." + method.methodName() + "("); + if (params.size() > 2) { + methodBuilder.addContentLine(""); + methodBuilder.increaseContentPadding(); + methodBuilder.increaseContentPadding(); + + // multiline + for (int i = 0; i < params.size(); i++) { + ParamDefinition param = params.get(i); + param.assignmentHandler().accept(methodBuilder); + if (i != params.size() - 1) { + // not the last one + methodBuilder.addContentLine(","); + } + } + methodBuilder.decreaseContentPadding(); + methodBuilder.decreaseContentPadding(); + } else { + for (int i = 0; i < params.size(); i++) { + ParamDefinition param = params.get(i); + param.assignmentHandler().accept(methodBuilder); + if (i != params.size() - 1) { + // not the last one + methodBuilder.addContent(","); + } + } + } + // single line + methodBuilder.addContentLine(");"); + + if (!method.isFinal()) { + methodBuilder.addContentLine("}"); + } + methodBuilder.addContentLine(""); + } + } + + private void injectFieldBody(Method.Builder methodBuilder, + ParamDefinition field, + boolean canIntercept, + List maybeIntercepted) { + if (canIntercept && isIntercepted(maybeIntercepted, field.elementInfo())) { + methodBuilder.addContentLine(field.declaredType().resolvedName() + " " + + field.ipParamName() + + " = ctx__helidonInject.dependency(" + field.constantName() + ");"); + String interceptorsName = field.ipParamName() + "__interceptors"; + String constantName = fieldElementConstantName(field.ipParamName()); + methodBuilder.addContent("var ") + .addContent(interceptorsName) + .addContent(" = interceptMeta__helidonInject.interceptors(QUALIFIERS, ANNOTATIONS, ") + .addContent(constantName) + .addContentLine(");") + .addContent("if(") + .addContent(interceptorsName) + .addContentLine(".isEmpty() {") + .addContent("instance__helidonInject.") + .addContent(field.ipParamName()) + .addContent(" = ") + .addContent(field.ipParamName()) + .addContentLine(";") + .addContentLine("} else {") + .addContent("instance__helidonInject.") + .addContent(field.ipParamName()) + .addContent(" = interceptMeta__helidonInject.invoke(this,") + .addContentLine("ANNOTATIONS,") + .addContent(constantName) + .addContentLine(",") + .addContent(interceptorsName) + .addContentLine(",") + .addContent("params__helidonInject -> ") + .addContent(field.constantName()) + .addContentLine(".type().cast(params__helidonInject[0]),") + .addContent(field.ipParamName()) + .addContentLine(");") + .addContentLine("}"); + } else { + methodBuilder.addContent("instance__helidonInject." + field.ipParamName() + " = ") + .update(it -> field.assignmentHandler().accept(it)) + .addContentLine(";"); + } + } + + private boolean isIntercepted(List maybeIntercepted, TypedElementInfo typedElementInfo) { + for (TypedElements.ElementMeta methodMetadata : maybeIntercepted) { + // yes, we want instance comparison, as we must use the same instances from the same type info + if (methodMetadata.element() == typedElementInfo) { + return true; + } + } + return false; + } + + private void postConstructMethod(ClassModel.Builder classModel, DescribedService service) { + TypeInfo typeInfo = service.serviceDescriptor().typeInfo(); + TypeName typeName = service.serviceDescriptor().typeName(); + + // postConstruct() + lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_POST_CONSTRUCT).ifPresent(method -> { + classModel.addMethod(postConstruct -> postConstruct.name("postConstruct") + .addAnnotation(Annotations.OVERRIDE) + .addParameter(instance -> instance.type(typeName) + .name("instance")) + .addContentLine("instance." + method.elementName() + "();")); + }); + } + + private void preDestroyMethod(ClassModel.Builder classModel, DescribedService service) { + TypeInfo typeInfo = service.serviceDescriptor().typeInfo(); + TypeName typeName = service.serviceDescriptor().typeName(); + + // preDestroy + lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_PRE_DESTROY).ifPresent(method -> { + classModel.addMethod(preDestroy -> preDestroy.name("preDestroy") + .addAnnotation(Annotations.OVERRIDE) + .addParameter(instance -> instance.type(typeName) + .name("instance")) + .addContentLine("instance." + method.elementName() + "();")); + }); + } + + private Optional lifecycleMethod(TypeInfo typeInfo, TypeName annotationType) { + List list = typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates.hasAnnotation(annotationType)) + .toList(); + if (list.isEmpty()) { + return Optional.empty(); + } + if (list.size() > 1) { + throw new IllegalStateException("There is more than one method annotated with " + annotationType.fqName() + + ", which is not allowed on type " + typeInfo.typeName().fqName()); + } + TypedElementInfo method = list.getFirst(); + if (method.accessModifier() == AccessModifier.PRIVATE) { + throw new IllegalStateException("Method annotated with " + annotationType.fqName() + + ", is private, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName()); + } + if (!method.parameterArguments().isEmpty()) { + throw new IllegalStateException("Method annotated with " + annotationType.fqName() + + ", has parameters, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName()); + } + if (!method.typeName().equals(TypeNames.PRIMITIVE_VOID)) { + throw new IllegalStateException("Method annotated with " + annotationType.fqName() + + ", is not void, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName()); + } + return Optional.of(method); + } + + private void qualifiersMethod(ClassModel.Builder classModel, DescribedService service) { + Set qualifiers = service.qualifiers(); + // qualifier field is always needed, as it is used for interception + Qualifiers.generateQualifiersConstant(classModel, qualifiers); + + if (qualifiers.isEmpty() && service.superType().empty()) { + return; + } + + // List qualifiers() + classModel.addMethod(qualifiersMethod -> qualifiersMethod.name("qualifiers") + .addAnnotation(Annotations.OVERRIDE) + .returnType(SET_OF_QUALIFIERS) + .addContentLine("return QUALIFIERS;")); + } + + private void scopeMethod(ClassModel.Builder classModel, DescribedService service) { + // TypeName scope() + classModel.addField(scopesField -> scopesField + .isStatic(true) + .isFinal(true) + .name("SCOPE") + .type(TypeNames.TYPE_NAME) + .addContentCreate(service.scope())); + + classModel.addMethod(scopeMethod -> scopeMethod.name("scope") + .addAnnotation(Annotations.OVERRIDE) + .returnType(TypeNames.TYPE_NAME) + .addContentLine("return SCOPE;")); + } + + private void weightMethod(ClassModel.Builder classModel, DescribedService service) { + TypeInfo typeInfo = service.serviceDescriptor().typeInfo(); + + boolean hasSuperType = service.superType().present(); + // double weight() + Optional weight = weight(typeInfo); + + if (!hasSuperType && weight.isEmpty()) { + return; + } + double usedWeight = weight.orElse(Weighted.DEFAULT_WEIGHT); + if (!hasSuperType && usedWeight == Weighted.DEFAULT_WEIGHT) { + return; + } + + classModel.addMethod(weightMethod -> weightMethod.name("weight") + .addAnnotation(Annotations.OVERRIDE) + .returnType(TypeNames.PRIMITIVE_DOUBLE) + .addContentLine("return " + usedWeight + ";")); + } + + private Optional weight(TypeInfo typeInfo) { + return typeInfo.findAnnotation(TypeName.create(Weight.class)) + .flatMap(Annotation::doubleValue); + } + + private void runLevelMethod(ClassModel.Builder classModel, DescribedService service) { + TypeInfo typeInfo = service.serviceDescriptor().typeInfo(); + + boolean hasSuperType = service.superType().present(); + // double runLevel() + Optional runLevel = runLevel(typeInfo); + + if (!hasSuperType && runLevel.isEmpty()) { + return; + } + + if (runLevel.isEmpty()) { + classModel.addMethod(runLevelMethod -> runLevelMethod.name("runLevel") + .addAnnotation(Annotations.OVERRIDE) + .returnType(TypeName.builder(TypeNames.OPTIONAL) + .addTypeArgument(TypeNames.BOXED_DOUBLE) + .build()) + .addContent("return ") + .addContent(Optional.class) + .addContentLine(".empty();")); + return; + } + + double usedRunLevel = runLevel.get(); + + classModel.addMethod(runLevelMethod -> runLevelMethod.name("runLevel") + .addAnnotation(Annotations.OVERRIDE) + .returnType(TypeName.builder(TypeNames.OPTIONAL) + .addTypeArgument(TypeNames.BOXED_DOUBLE) + .build()) + .addContent("return ") + .addContent(Optional.class) + .addContent(".of(") + .addContent(String.valueOf(usedRunLevel)) + .addContentLine("D);")); + } + + private Optional runLevel(TypeInfo typeInfo) { + return typeInfo.findAnnotation(InjectCodegenTypes.INJECTION_RUN_LEVEL) + .flatMap(Annotation::doubleValue); + } + + private void notifyIpObservers(RegistryRoundContext roundContext, DescribedService service, List params) { + if (observers.isEmpty()) { + return; + } + + for (ParamDefinition param : params) { + TypeInfo typeInfo = service.serviceDescriptor().typeInfo(); + TypedElementInfo owningElement = param.owningElement(); + TypedElementInfo ipElement = param.elementInfo(); + observers.forEach(it -> it.onInjectionPoint( + roundContext, + typeInfo, + owningElement, + ipElement)); + } + } + + private void notifyObservers(RegistryRoundContext roundContext, Collection descriptorsRequired) { + if (observers.isEmpty()) { + return; + } + + // we have correct classloader set in current thread context + Set elements = descriptorsRequired.stream() + .flatMap(it -> it.elementInfo().stream()) + .collect(Collectors.toSet()); + observers.forEach(it -> it.onProcessingEvent(roundContext, elements)); + } + + private InjectAssignment.Assignment translateParameter(TypeName typeName, String constantName) { + return assignments.assignment(typeName, "ctx__helidonInject.dependency(" + constantName + ")"); + } + + private TypeName descriptorInstanceType(TypeName serviceType, TypeName descriptorType) { + return TypeName.builder(descriptorType) + .addTypeArgument(serviceType) + .build(); + } + + private record GenericTypeDeclaration(String constantName, + TypeName typeName) { + } +} 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 new file mode 100644 index 00000000000..e34e787efaf --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InjectionExtensionProvider.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.codegen; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.codegen.Option; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.TypeName; +import io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.codegen.ServiceOptions; +import io.helidon.service.codegen.spi.RegistryCodegenExtension; +import io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider; +import io.helidon.service.inject.codegen.spi.InjectCodegenObserverProvider; + +/** + * A {@link java.util.ServiceLoader} provider implementation that adds code generation for Helidon Inject. + * This extension creates service descriptors, and intercepted types. + */ +@Weight(Weighted.DEFAULT_WEIGHT - 20) // lower weight than config beans, as we may need them +public class InjectionExtensionProvider implements RegistryCodegenExtensionProvider { + private static final List OBSERVER_PROVIDERS = + HelidonServiceLoader.create(ServiceLoader.load(InjectCodegenObserverProvider.class, + InjectionExtension.class.getClassLoader())) + .asList(); + + /** + * Required default constructor for {@link java.util.ServiceLoader}. + * + * @deprecated only for {@link java.util.ServiceLoader} + */ + @Deprecated + public InjectionExtensionProvider() { + super(); + } + + @Override + public Set> supportedOptions() { + return Stream.concat(Stream.of( + ServiceOptions.AUTO_ADD_NON_CONTRACT_INTERFACES, + InjectOptions.INTERCEPTION_STRATEGY, + InjectOptions.SCOPE_META_ANNOTATIONS + ), + OBSERVER_PROVIDERS.stream() + .map(InjectCodegenObserverProvider::supportedOptions) + .flatMap(Set::stream)) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public Set supportedAnnotations() { + return Set.of(InjectCodegenTypes.INJECTION_INJECT, + InjectCodegenTypes.INJECTION_DESCRIBE, + InjectCodegenTypes.INJECTION_PER_INSTANCE); + } + + @Override + public Set supportedMetaAnnotations() { + return Set.of(InjectCodegenTypes.INJECTION_SCOPE); + } + + @Override + public RegistryCodegenExtension create(RegistryCodegenContext codegenContext) { + return new InjectionExtension(codegenContext); + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptedTypeGenerator.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptedTypeGenerator.java new file mode 100644 index 00000000000..54d80e344a2 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptedTypeGenerator.java @@ -0,0 +1,365 @@ +/* + * 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.codegen; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Constructor; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Annotations; +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 io.helidon.service.codegen.RegistryCodegenContext; + +import static io.helidon.codegen.CodegenUtil.toConstantName; + +class InterceptedTypeGenerator { + public static final String INTERCEPT_META_PARAM = "helidonInject__interceptMeta"; + public static final String SERVICE_DESCRIPTOR_PARAM = "helidonInject__serviceDescriptor"; + public static final String TYPE_QUALIFIERS_PARAM = "helidonInject__typeQualifiers"; + public static final String TYPE_ANNOTATIONS_PARAM = "helidonInject__typeAnnotations"; + private static final TypeName GENERATOR = TypeName.create(InterceptedTypeGenerator.class); + private static final TypeName RUNTIME_EXCEPTION_TYPE = TypeName.create(RuntimeException.class); + private final TypeName serviceType; + private final TypeName descriptorType; + private final TypeName interceptedType; + private final TypedElementInfo constructor; + private final List interceptedMethods; + + InterceptedTypeGenerator(RegistryCodegenContext ctx, + TypeInfo typeInfo, + TypeName serviceType, + TypeName descriptorType, + TypeName interceptedType, + TypedElementInfo constructor, + List interceptedMethods) { + this.serviceType = serviceType; + this.descriptorType = descriptorType; + this.interceptedType = interceptedType; + this.constructor = constructor; + this.interceptedMethods = MethodDefinition.toDefinitions(ctx, typeInfo, interceptedMethods); + } + + static void generateElementInfoFields(ClassModel.Builder classModel, List interceptedMethods) { + for (MethodDefinition interceptedMethod : interceptedMethods) { + classModel.addField(methodField -> methodField + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(TypeNames.TYPED_ELEMENT_INFO) + .name(interceptedMethod.constantName()) + .addContentCreate(interceptedMethod.info())); + } + } + + static void generateInvokerFields(ClassModel.Builder classModel, + List interceptedMethods) { + for (MethodDefinition interceptedMethod : interceptedMethods) { + classModel.addField(methodField -> methodField + .accessModifier(AccessModifier.PRIVATE) + .isFinal(true) + .type(invokerType(interceptedMethod.info().typeName())) + .name(interceptedMethod.invokerName())); + } + } + + static void generateInterceptedMethods(ClassModel.Builder classModel, List interceptedMethods) { + for (MethodDefinition interceptedMethod : interceptedMethods) { + TypedElementInfo info = interceptedMethod.info(); + String invoker = interceptedMethod.invokerName(); + + classModel.addMethod(method -> method + .addAnnotation(Annotations.OVERRIDE) + .accessModifier(info.accessModifier()) + .name(info.elementName()) + .returnType(info.typeName()) + .update(it -> info.parameterArguments().forEach(arg -> it.addParameter(param -> param.type(arg.typeName()) + .name(arg.elementName())))) + .update(it -> { + // add throws statements + if (!interceptedMethod.exceptionTypes().isEmpty()) { + for (TypeName exceptionType : interceptedMethod.exceptionTypes()) { + it.addThrows(exceptionType, "thrown by intercepted method"); + } + } + }) + .update(it -> { + String invokeLine = invoker + + ".invoke(" + + info.parameterArguments() + .stream().map(TypedElementInfo::elementName) + .collect(Collectors.joining(", ")) + + ");"; + // body of the method + it.addContentLine("try {") + .addContent(interceptedMethod.isVoid() ? "" : "return ") + .addContentLine(invokeLine) + .addContent("}"); + for (TypeName exceptionType : interceptedMethod.exceptionTypes()) { + it.addContent(" catch (") + .addContent(exceptionType) + .addContentLine(" helidonInject__e) {") + .addContentLine(" throw helidonInject__e;") + .addContent("}"); + + } + if (!interceptedMethod.exceptionTypes().contains(RUNTIME_EXCEPTION_TYPE)) { + it.addContent(" catch (") + .addContent(RuntimeException.class) + .addContentLine(" helidonInject__e) {") + .addContentLine("throw helidonInject__e;") + .addContent("}"); + } + it.addContent(" catch (") + .addContent(Exception.class) + .addContentLine(" helidonInject__e) {") + .addContent("throw new ") + .addContent(RuntimeException.class) + .addContentLine("(helidonInject__e);") + .addContentLine("}"); + + })); + + } + } + + /** + * Create invokes for intercepted methods. + * + * @param cModel model to add the invokers to (constructor) + * @param descriptorType type of the descriptor we are processing + * @param interceptedMethods list of intercepted methods + * @param useDescriptorConstant whether to use descriptor constant, or local constant for method info + * @param interceptMetaName name of the interceptor meta parameter + * @param descriptorName name of the descriptor parameter + * @param qualifiersName name of the qualifiers parameter + * @param annotationsName name of the annotations parameter + * @param invocationTargetName name of the invocation target parameter + */ + @SuppressWarnings("checkstyle:ParameterNumber") + static void createInvokers(Constructor.Builder cModel, + TypeName descriptorType, + List interceptedMethods, + boolean useDescriptorConstant, + String interceptMetaName, + String descriptorName, + String qualifiersName, + String annotationsName, + String invocationTargetName) { + boolean hasGenericType = false; + + for (MethodDefinition interceptedMethod : interceptedMethods) { + cModel.addContent("this.") + .addContent(interceptedMethod.invokerName) + .addContent(" = ") + .addContent(interceptMetaName) + .addContentLine(".createInvoker(") + .increaseContentPadding() + .addContentLine("this,") + .addContent(descriptorName) + .addContentLine(",") + .addContent(qualifiersName) + .addContentLine(",") + .addContent(annotationsName) + .addContentLine(",") + .update(it -> { + if (useDescriptorConstant) { + it.addContent(descriptorType) + .addContent("."); + } + }) + .addContent(interceptedMethod.constantName()) + .addContentLine(",") + .addContent("helidonInject__params -> "); + if (interceptedMethod.isVoid()) { + cModel.addContentLine("{"); + } + cModel.addContent(invocationTargetName) + .addContent(".") + .addContent(interceptedMethod.info().elementName()) + .addContent("("); + + List allArgs = new ArrayList<>(); + List args = interceptedMethod.info().parameterArguments(); + for (int i = 0; i < args.size(); i++) { + TypedElementInfo arg = args.get(i); + allArgs.add("(" + arg.typeName().resolvedName() + ") helidonInject__params[" + i + "]"); + if (!arg.typeName().typeArguments().isEmpty()) { + hasGenericType = true; + } + } + cModel.addContent(String.join(", ", allArgs)); + cModel.addContent(")"); + + if (interceptedMethod.isVoid()) { + cModel.addContentLine(";"); + cModel.addContentLine("return null;"); + cModel.addContent("}"); + } + cModel.addContent(", ") + .addContent(Set.class) + .addContent(".of(") + .addContent(interceptedMethod.exceptionTypes() + .stream() + .map(it -> it.fqName() + ".class") + .collect(Collectors.joining(", "))) + .addContentLine("));") + .decreaseContentPadding(); + } + if (hasGenericType) { + cModel.addAnnotation(Annotation.create(TypeName.create(SuppressWarnings.class), "unchecked")); + } + } + + ClassModel.Builder generate() { + ClassModel.Builder classModel = ClassModel.builder() + .copyright(CodegenUtil.copyright(GENERATOR, + serviceType, + interceptedType)) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + serviceType, + interceptedType, + "1", + "")) + .description("Intercepted sub-type for {@link " + serviceType.fqName() + "}.") + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .type(interceptedType) + .superType(serviceType); + + generateInvokerFields(classModel, interceptedMethods); + + generateConstructor(classModel); + + generateInterceptedMethods(classModel, interceptedMethods); + + return classModel; + } + + private static TypeName invokerType(TypeName type) { + return TypeName.builder(InjectCodegenTypes.INTERCEPT_INVOKER) + .addTypeArgument(type.boxed()) + .build(); + } + + private void generateConstructor(ClassModel.Builder classModel) { + classModel.addConstructor(constructor -> constructor + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .addParameter(interceptMeta -> interceptMeta.type(InjectCodegenTypes.INTERCEPT_METADATA) + .name(INTERCEPT_META_PARAM)) + .addParameter(descriptor -> descriptor.type(descriptorType) + .name(SERVICE_DESCRIPTOR_PARAM)) + .addParameter(qualifiers -> qualifiers.type(InjectionExtension.SET_OF_QUALIFIERS) + .name(TYPE_QUALIFIERS_PARAM)) + .addParameter(qualifiers -> qualifiers.type(InjectionExtension.LIST_OF_ANNOTATIONS) + .name(TYPE_ANNOTATIONS_PARAM)) + .update(this::addConstructorParameters) + .update(this::callSuperConstructor) + .update(it -> createInvokers(it, + descriptorType, + interceptedMethods, + true, + INTERCEPT_META_PARAM, + SERVICE_DESCRIPTOR_PARAM, + TYPE_QUALIFIERS_PARAM, + TYPE_ANNOTATIONS_PARAM, + "super")) + ); + } + + private void callSuperConstructor(Constructor.Builder cModel) { + cModel.addContent("super("); + cModel.addContent(this.constructor.parameterArguments() + .stream() + .map(TypedElementInfo::elementName) + .collect(Collectors.joining(", "))); + cModel.addContentLine(");"); + cModel.addContentLine(""); + } + + private void addConstructorParameters(Constructor.Builder cModel) { + // for each constructor parameter, add it as is (same type and name as super type) + // this will not create conflicts, unless somebody names their constructor parameters same + // as the ones above (which we will not do, and others should not do) + this.constructor.parameterArguments().forEach(constructorArg -> { + cModel.addParameter(generatedCtrParam -> generatedCtrParam.type(constructorArg.typeName()) + .name(constructorArg.elementName())); + }); + } + + record MethodDefinition(TypedElementInfo info, + String constantName, + String invokerName, + boolean isVoid, + Set exceptionTypes) { + + static List toDefinitions(RegistryCodegenContext ctx, + TypeInfo typeInfo, + List interceptedMethods) { + List sortedMethods = new ArrayList<>(interceptedMethods); + + List result = new ArrayList<>(); + for (int i = 0; i < sortedMethods.size(); i++) { + TypedElements.ElementMeta elementMeta = sortedMethods.get(i); + + List elementAnnotations = new ArrayList<>(elementMeta.element().annotations()); + addInterfaceAnnotations(elementAnnotations, elementMeta.abstractMethods()); + + TypedElementInfo typedElementInfo = TypedElementInfo.builder() + .from(elementMeta.element()) + .annotations(elementAnnotations) + .build(); + + String constantName = "METHOD_" + toConstantName(ctx.uniqueName(typeInfo, elementMeta.element())); + String invokerName = typedElementInfo.elementName() + "_" + i + "_invoker"; + + result.add(new MethodDefinition(typedElementInfo, + constantName, + invokerName, + TypeNames.PRIMITIVE_VOID.equals(typedElementInfo.typeName()), + typedElementInfo.throwsChecked())); + } + result.sort(Comparator.comparing(o -> o.invokerName)); + return result; + } + + private static void addInterfaceAnnotations(List elementAnnotations, + List declaredElements) { + + for (TypedElements.DeclaredElement declaredElement : declaredElements) { + declaredElement.element() + .annotations() + .forEach(it -> addInterfaceAnnotation(elementAnnotations, it)); + } + } + + private static void addInterfaceAnnotation(List elementAnnotations, Annotation annotation) { + // only add if not already there + if (!elementAnnotations.contains(annotation)) { + elementAnnotations.add(annotation); + } + } + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Interception.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Interception.java new file mode 100644 index 00000000000..daf856bacc1 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Interception.java @@ -0,0 +1,180 @@ +/* + * 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.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import io.helidon.codegen.CodegenException; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotated; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeNames; + +final class Interception { + private static final Annotation RUNTIME_RETENTION = Annotation.create(Retention.class, RetentionPolicy.RUNTIME.name()); + private static final Annotation CLASS_RETENTION = Annotation.create(Retention.class, RetentionPolicy.CLASS.name()); + + private final InterceptionStrategy interceptionStrategy; + + Interception(InterceptionStrategy interceptionStrategy) { + this.interceptionStrategy = interceptionStrategy; + } + + /** + * Find all elements that may be intercepted. + * This method also returns fields (as injection into fields can be intercepted). + * + * @param typeInfo type being processed + * @return all elements that may be intercepted (constructors, fields, methods) + */ + List maybeIntercepted(TypeInfo typeInfo) { + if (interceptionStrategy == InterceptionStrategy.NONE) { + return List.of(); + } + + List result = new ArrayList<>(); + + // depending on strategy + List allElements = TypedElements.gatherElements(typeInfo); + + if (hasInterceptTrigger(typeInfo)) { + // we cannot intercept private stuff (never modify source code or bytecode!) + allElements.stream() + .filter(it -> it.element().accessModifier() != AccessModifier.PRIVATE) + .forEach(result::add); + result.add(TypedElements.DEFAULT_CONSTRUCTOR); // we must intercept construction as well + } else { + allElements.stream() + .filter(methodMetadata -> hasInterceptTrigger(typeInfo, methodMetadata)) + .peek(it -> { + if (it.element().accessModifier() == AccessModifier.PRIVATE) { + throw new CodegenException(typeInfo.typeName() + .fqName() + "#" + it.element() + .elementName() + " is declared " + + "as private, but has interceptor trigger " + + "annotation declared. " + + "This cannot be supported, as we do not modify " + + "sources or bytecode.", + it.element().originatingElementValue()); + } + }) + .forEach(result::add); + } + + return result; + } + + /** + * Find all elements that may be intercepted. + * This method also returns fields (as injection into fields can be intercepted). + * + * @param typeInfo the type + * @param elements elements to process + * @return all elements that may be intercepted (constructors, fields, methods) + */ + List maybeIntercepted(TypeInfo typeInfo, List elements) { + if (interceptionStrategy == InterceptionStrategy.NONE) { + return List.of(); + } + + List result = new ArrayList<>(); + + if (hasInterceptTrigger(typeInfo)) { + // we cannot intercept private stuff (never modify source code or bytecode!) + elements.stream() + .filter(it -> it.element().accessModifier() != AccessModifier.PRIVATE) + .forEach(result::add); + result.add(TypedElements.DEFAULT_CONSTRUCTOR); // we must intercept construction as well + } else { + elements.stream() + .filter(methodMetadata -> hasInterceptTrigger(typeInfo, methodMetadata)) + .peek(it -> { + if (it.element().accessModifier() == AccessModifier.PRIVATE) { + throw new CodegenException(typeInfo.typeName() + .fqName() + "#" + it.element() + .elementName() + " is declared " + + "as private, but has interceptor trigger " + + "annotation declared. " + + "This cannot be supported, as we do not modify " + + "sources or bytecode.", + it.element().originatingElementValue()); + } + }) + .forEach(result::add); + } + + return result; + } + + // intercept trigger on the type (or on implemented interface) + private boolean hasInterceptTrigger(TypeInfo typeInfo) { + if (hasInterceptTrigger(typeInfo, typeInfo)) { + return true; + } + // check all implemented interfaces + for (TypeInfo ifaceType : typeInfo.interfaceTypeInfo()) { + if (hasInterceptTrigger(ifaceType)) { + return true; + } + } + return false; + } + + private boolean hasInterceptTrigger(TypeInfo typeInfo, TypedElements.ElementMeta methodMeta) { + if (hasInterceptTrigger(typeInfo, methodMeta.element())) { + // the method is declared with the annotation + return true; + } + for (TypedElements.DeclaredElement interfaceMethod : methodMeta.abstractMethods()) { + return hasInterceptTrigger(interfaceMethod.abstractType(), interfaceMethod.element()); + } + + return false; + } + + private boolean hasInterceptTrigger(TypeInfo typeInfo, Annotated element) { + for (Annotation annotation : element.annotations()) { + if (interceptionStrategy.ordinal() >= InterceptionStrategy.EXPLICIT.ordinal()) { + if (typeInfo.hasMetaAnnotation(annotation.typeName(), InjectCodegenTypes.INTERCEPTION_INTERCEPTED)) { + return true; + } + } + if (interceptionStrategy.ordinal() >= InterceptionStrategy.ALL_RUNTIME.ordinal()) { + Optional retention = typeInfo.metaAnnotation(annotation.typeName(), + TypeNames.RETENTION); + boolean isRuntime = retention.map(RUNTIME_RETENTION::equals).orElse(false); + if (isRuntime) { + return true; + } + } + if (interceptionStrategy.ordinal() >= InterceptionStrategy.ALL_RETAINED.ordinal()) { + Optional retention = typeInfo.metaAnnotation(annotation.typeName(), + TypeNames.RETENTION); + boolean isClass = retention.map(CLASS_RETENTION::equals).orElse(false); + if (isClass) { + return true; + } + } + } + return false; + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptionStrategy.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptionStrategy.java new file mode 100644 index 00000000000..eb2e6fa630f --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptionStrategy.java @@ -0,0 +1,47 @@ +/* + * 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.codegen; + +/** + * Possible strategies to interception. + * Whether interception is supported, and this is honored depends on implementation. + *

    + * The strategy is (in Helidon inject) only honored at compilation time. At runtime, it can only be enabled or disabled. + */ +enum InterceptionStrategy { + /** + * No annotations will qualify in triggering interceptor creation (interception is disabled). + */ + NONE, + /** + * Meta-annotation based. Only annotations annotated with {@code Interception.Intercepted} will + * qualify. + * This is the default strategy. + */ + EXPLICIT, + /** + * All annotations marked as {@link java.lang.annotation.RetentionPolicy#RUNTIME} will qualify. + * Also includes all usages of {@link #EXPLICIT}. + */ + ALL_RUNTIME, + /** + * All annotations marked as {@link java.lang.annotation.RetentionPolicy#RUNTIME} and + * {@link java.lang.annotation.RetentionPolicy#CLASS} will qualify. + * Also includes all usages of {@link #EXPLICIT}. + */ + ALL_RETAINED +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptionSupport.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptionSupport.java new file mode 100644 index 00000000000..01538aa753b --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/InterceptionSupport.java @@ -0,0 +1,484 @@ +/* + * 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.EnumSet; +import java.util.HashSet; +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.common.types.AccessModifier; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.ElementKind; +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 io.helidon.service.codegen.RegistryCodegenContext; +import io.helidon.service.codegen.RegistryRoundContext; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECT_SERVICE_DESCRIPTOR; +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INTERCEPT_METADATA; +import static java.util.function.Predicate.not; + +final class InterceptionSupport { + private static final TypeName GENERATOR = TypeName.create(InterceptionSupport.class); + private static final TypeName DESCRIPTOR_TYPE = TypeName.builder(INJECT_SERVICE_DESCRIPTOR) + .addTypeArgument(TypeName.create("?")) + .build(); + private static final String INTERCEPT_META_PARAM = "interceptMeta"; + private static final String DESCRIPTOR_PARAM = "descriptor"; + private static final String TYPE_ANNOTATIONS_FIELD = "ANNOTATIONS"; + private static final String DELEGATE_PARAM = "delegate"; + + private final RegistryCodegenContext ctx; + private final Interception interception; + + private InterceptionSupport(RegistryCodegenContext ctx, Interception interception) { + this.ctx = ctx; + this.interception = interception; + } + + /** + * Create a new instance. + * + * @param ctx codegen context + * @return a new interception support instance + */ + static InterceptionSupport create(RegistryCodegenContext ctx) { + return new InterceptionSupport(ctx, + new Interception(InjectOptions.INTERCEPTION_STRATEGY.value(ctx.options()))); + } + + /** + * Generates required code to handle interception by delegation. + *

    + * Creates the following types: + *

      + *
    • Type__InterceptedDelegate - the type implementing interception, package private
    • + *
    + * + * Usage: When processing types, you can call this method to generate the types above. This is not done automatically, + * as we do not know which interface is the "right" one to intercept (there may be a generated type, or it may be implemented + * directly by a service, in which case the interception is generated by Helidon automatically). + * The steps you need to do: + *
      + *
    1. Invoke {@code Contract__InterceptedDelegate.create(interceptMeta, ServiceDescriptor.INSTANCE, instance)} + * to wrap your instance, the service descriptor must be your descriptor that describes the service
    2. + *
    + * + * @param typeInfo interface type info that will be intercepted + * @param interceptedType type of the interface (or class) used for interception (may differ from typeInfo type) + * @param packageName package to generate the delegate into (may differ from type, when using external delegates) + * @return type name of the generated delegate implementation + * @throws io.helidon.codegen.CodegenException in case the type is not an interface + */ + TypeName generateDelegateInterception(RegistryRoundContext roundContext, + TypeInfo typeInfo, + TypeName interceptedType, + String packageName) { + boolean samePackage = packageName.equals(interceptedType.packageName()); + + List elementMetas = interception.maybeIntercepted(typeInfo); + Set interceptedSignatures = new HashSet<>(); + elementMetas.stream() + .map(TypedElements.ElementMeta::element) + .peek(it -> { + if (samePackage) { + return; + } + if (it.accessModifier() != AccessModifier.PUBLIC) { + throw new CodegenException("Cannot generate interception delegate for a non-public method" + + " when the delegate is in a different package", + it.originatingElementValue()); + } + }) + .map(TypedElementInfo::signature) + .forEach(interceptedSignatures::add); + List otherMethods = typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isStatic)) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(it -> { + if (samePackage) { + return true; + } + return ElementInfoPredicates.isPublic(it); + }) + .filter(it -> !interceptedSignatures.contains(it.signature())) + .collect(Collectors.toUnmodifiableList()); + + TypeName generatedType = interceptedDelegateType(interceptedType); + generatedType = TypeName.builder() + .from(generatedType) + .packageName(packageName) + .build(); + + if (typeInfo.kind() == ElementKind.INTERFACE) { + return generateInterceptionDelegateInterface(roundContext, + typeInfo, + interceptedType, + generatedType, + elementMetas, + otherMethods); + } else { + return generateInterceptionDelegateClass(roundContext, + typeInfo, + interceptedType, + generatedType, + elementMetas, + otherMethods); + } + + } + + /** + * Type name for the intercepted delegate generated type. + * + * @param interfaceType type of the interface + * @return type name that will be generated for it + */ + TypeName interceptedDelegateType(TypeName interfaceType) { + return TypeName.builder() + .packageName(interfaceType.packageName()) + .className(interfaceType.classNameWithEnclosingNames().replace('.', '_') + "__InterceptedDelegate") + .build(); + } + + void generateDelegateInterception(RegistryRoundContext roundContext, + TypeInfo typeInfo, + DescribedElements describedElements, + TypeName interceptionDelegateType) { + TypeName interceptedTypeName = typeInfo.typeName(); + boolean samePackage = interceptedTypeName.packageName().equals(interceptionDelegateType.packageName()); + + // validate we can generate everything + describedElements.interceptedElements() + .stream() + .forEach(it -> { + if (ElementInfoPredicates.isStatic(it.element())) { + throw new CodegenException("Interception of static methods is not possible", + it.element().originatingElementValue()); + } + if (it.element().accessModifier() == AccessModifier.PRIVATE) { + throw new CodegenException("Cannot generate interception delegate for a private method: ", + it.element().originatingElementValue()); + } + if (samePackage) { + return; + } + Set allowedModifiers = EnumSet.of(AccessModifier.PUBLIC); + if (typeInfo.kind() == ElementKind.CLASS) { + allowedModifiers.add(AccessModifier.PROTECTED); + } + if (!allowedModifiers.contains(it.element().accessModifier())) { + throw new CodegenException("Cannot generate interception delegate for a method that is not public" + + " or protected, when" + + " the delegate is in a different package than the intercepted type" + + " intercepted type: " + interceptedTypeName.fqName() + + ", delegate: " + interceptionDelegateType.fqName(), + it.element().originatingElementValue()); + } + }); + // now we are sure we can implement all intercepted methods + List interceptedMethods = describedElements.interceptedElements() + .stream() + .collect(Collectors.toUnmodifiableList()); + List plainSignatures = describedElements.plainElements() + .stream() + .map(TypedElements.ElementMeta::element) + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isStatic)) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(it -> { + if (samePackage) { + return true; + } + return ElementInfoPredicates.isPublic(it); + }) + .collect(Collectors.toUnmodifiableList()); + + if (typeInfo.kind() == ElementKind.INTERFACE) { + generateInterceptionDelegateInterface(roundContext, + typeInfo, + interceptedTypeName, + interceptionDelegateType, + interceptedMethods, + plainSignatures); + } else { + generateInterceptionDelegateClass(roundContext, + typeInfo, + interceptedTypeName, + interceptionDelegateType, + interceptedMethods, + plainSignatures); + } + } + + private TypeName generateInterceptionDelegateClass(RegistryRoundContext roundContext, + TypeInfo typeInfo, + TypeName classType, + TypeName generatedType, + List elementMetas, + List otherMethods) { + + Optional foundCtr = typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isConstructor) + .filter(ElementInfoPredicates.hasParams()) + .filter(not(ElementInfoPredicates::isPrivate)) + .findFirst(); + + if (foundCtr.isEmpty()) { + throw new CodegenException("Interception delegate requires accessible no-arg constructor.", + typeInfo.originatingElementValue()); + } + + var definitions = InterceptedTypeGenerator.MethodDefinition.toDefinitions(ctx, typeInfo, elementMetas); + + var classModel = ClassModel.builder() + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .superType(classType) + .type(generatedType) + .copyright(CodegenUtil.copyright(GENERATOR, + classType, + generatedType)) + .description("Intercepted class implementation, that delegates to the provided instance.") + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + classType, + generatedType, + "", + "")); + + // add type annotations + InjectionExtension.annotationsField(classModel, typeInfo); + // this is a special case, where we may not have the correct descriptor + InterceptedTypeGenerator.generateElementInfoFields(classModel, definitions); + InterceptedTypeGenerator.generateInvokerFields(classModel, definitions); + InterceptedTypeGenerator.generateInterceptedMethods(classModel, definitions); + generateOtherMethods(classModel, otherMethods); + + classModel.addField(delegate -> delegate + .name(DELEGATE_PARAM) + .accessModifier(AccessModifier.PRIVATE) + .isFinal(true) + .type(classType)); + + classModel.addConstructor(ctr -> ctr + .accessModifier(AccessModifier.PRIVATE) + .addParameter(interceptMeta -> interceptMeta + .type(INTERCEPT_METADATA) + .name(INTERCEPT_META_PARAM)) + .addParameter(descriptor -> descriptor + .type(DESCRIPTOR_TYPE) + .name(DESCRIPTOR_PARAM)) + .addParameter(delegate -> delegate + .type(classType) + .name(DELEGATE_PARAM)) + .addContentLine("// no-arg constructor is required for delegation") + .addContentLine("super();") + .addContentLine("") + .addContent("this.") + .addContent(DELEGATE_PARAM) + .addContent(" = ") + .addContent(DELEGATE_PARAM) + .addContentLine(";") + .update(it -> InterceptedTypeGenerator.createInvokers( + it, + DESCRIPTOR_TYPE, + definitions, + false, + INTERCEPT_META_PARAM, + DESCRIPTOR_PARAM, + DESCRIPTOR_PARAM + ".qualifiers()", + TYPE_ANNOTATIONS_FIELD, + DELEGATE_PARAM))); + + // and finally the create method (to be invoked by user code) + classModel.addMethod(create -> create + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .isStatic(true) + .returnType(classType) + .name("create") + .addParameter(interceptMeta -> interceptMeta + .type(INTERCEPT_METADATA) + .name(INTERCEPT_META_PARAM)) + .addParameter(descriptor -> descriptor + .type(DESCRIPTOR_TYPE) + .name(DESCRIPTOR_PARAM)) + .addParameter(delegate -> delegate + .type(classType) + .name(DELEGATE_PARAM)) + .addContent("return new ") + .addContent(generatedType) + .addContent("(") + .addContent(INTERCEPT_META_PARAM) + .addContentLine(",") + .increaseContentPadding() + .increaseContentPadding() + .addContent(DESCRIPTOR_PARAM) + .addContentLine(",") + .addContent(DELEGATE_PARAM) + .addContentLine(");") + ); + + roundContext.addGeneratedType(generatedType, classModel, classType, typeInfo.originatingElementValue()); + + return generatedType; + } + + private TypeName generateInterceptionDelegateInterface(RegistryRoundContext roundContext, + TypeInfo typeInfo, + TypeName interfaceType, + TypeName generatedType, + List elementMetas, + List otherMethods) { + + var definitions = InterceptedTypeGenerator.MethodDefinition.toDefinitions(ctx, typeInfo, elementMetas); + + var classModel = ClassModel.builder() + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .addInterface(interfaceType) + .type(generatedType) + .copyright(CodegenUtil.copyright(GENERATOR, + interfaceType, + generatedType)) + .description("Intercepted interface implementation, that delegates to the provided instance.") + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + interfaceType, + generatedType, + "", + "")); + + // add type annotations + InjectionExtension.annotationsField(classModel, typeInfo); + // this is a special case, where we may not have the correct descriptor + InterceptedTypeGenerator.generateElementInfoFields(classModel, definitions); + InterceptedTypeGenerator.generateInvokerFields(classModel, definitions); + InterceptedTypeGenerator.generateInterceptedMethods(classModel, definitions); + generateOtherMethods(classModel, otherMethods); + + classModel.addField(delegate -> delegate + .name(DELEGATE_PARAM) + .accessModifier(AccessModifier.PRIVATE) + .isFinal(true) + .type(interfaceType)); + + classModel.addConstructor(ctr -> ctr + .accessModifier(AccessModifier.PRIVATE) + .addParameter(interceptMeta -> interceptMeta + .type(INTERCEPT_METADATA) + .name(INTERCEPT_META_PARAM)) + .addParameter(descriptor -> descriptor + .type(DESCRIPTOR_TYPE) + .name(DESCRIPTOR_PARAM)) + .addParameter(delegate -> delegate + .type(interfaceType) + .name(DELEGATE_PARAM)) + .addContent("this.") + .addContent(DELEGATE_PARAM) + .addContent(" = ") + .addContent(DELEGATE_PARAM) + .addContentLine(";") + .update(it -> InterceptedTypeGenerator.createInvokers(it, + DESCRIPTOR_TYPE, + definitions, + false, + INTERCEPT_META_PARAM, + DESCRIPTOR_PARAM, + DESCRIPTOR_PARAM + ".qualifiers()", + TYPE_ANNOTATIONS_FIELD, + DELEGATE_PARAM))); + + // and finally the create method (to be invoked by user code) + classModel.addMethod(create -> create + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .isStatic(true) + .returnType(interfaceType) + .name("create") + .addParameter(interceptMeta -> interceptMeta + .type(INTERCEPT_METADATA) + .name(INTERCEPT_META_PARAM)) + .addParameter(descriptor -> descriptor + .type(DESCRIPTOR_TYPE) + .name(DESCRIPTOR_PARAM)) + .addParameter(delegate -> delegate + .type(interfaceType) + .name(DELEGATE_PARAM)) + .addContent("return new ") + .addContent(generatedType) + .addContent("(") + .addContent(INTERCEPT_META_PARAM) + .addContentLine(",") + .increaseContentPadding() + .increaseContentPadding() + .addContent(DESCRIPTOR_PARAM) + .addContentLine(",") + .addContent(DELEGATE_PARAM) + .addContentLine(");") + ); + + roundContext.addGeneratedType(generatedType, classModel, interfaceType, typeInfo.originatingElementValue()); + + return generatedType; + } + + private void generateOtherMethods(ClassModel.Builder classModel, List otherMethods) { + for (TypedElementInfo info : otherMethods) { + classModel.addMethod(method -> method + .addAnnotation(Annotations.OVERRIDE) + .name(info.elementName()) + .accessModifier(info.accessModifier()) + .name(info.elementName()) + .returnType(info.typeName()) + .update(it -> info.parameterArguments().forEach(arg -> it.addParameter(param -> param.type(arg.typeName()) + .name(arg.elementName())))) + .update(it -> { + // add throws statements + for (TypeName checkedException : info.throwsChecked()) { + it.addThrows(checkedException, ""); + } + }) + .update(it -> { + // body + if (!info.typeName().equals(TypeNames.PRIMITIVE_VOID)) { + it.addContent("return "); + } + it.addContent(DELEGATE_PARAM) + .addContent(".") + .addContent(info.elementName()) + .addContent("("); + + it.addContent(info.parameterArguments() + .stream() + .map(TypedElementInfo::elementName) + .collect(Collectors.joining(", "))); + + it.addContentLine(");"); + })); + } + + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/MapNamedByTypeMapperProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/MapNamedByTypeMapperProvider.java new file mode 100644 index 00000000000..f3871649d53 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/MapNamedByTypeMapperProvider.java @@ -0,0 +1,71 @@ +/* + * 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.codegen; + +import java.util.Collection; +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.spi.AnnotationMapper; +import io.helidon.codegen.spi.AnnotationMapperProvider; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeName; + +/** + * A {@link java.util.ServiceLoader} provider implementation to map class named annotations to named annotations. + */ +@Weight(Weighted.DEFAULT_WEIGHT - 10) // lower weight than JavaxAnnotationMapper +public class MapNamedByTypeMapperProvider implements AnnotationMapperProvider { + /** + * Required default constructor. + * + * @deprecated only for {@link java.util.ServiceLoader}. + */ + @Deprecated + public MapNamedByTypeMapperProvider() { + } + + @Override + public Set supportedAnnotations() { + return Set.of(InjectCodegenTypes.INJECTION_NAMED_BY_TYPE); + } + + @Override + public AnnotationMapper create(CodegenOptions options) { + return new NamedByTypeMapper(); + } + + private static class NamedByTypeMapper implements AnnotationMapper { + + private NamedByTypeMapper() { + } + + @Override + public boolean supportsAnnotation(Annotation annotation) { + return annotation.typeName().equals(InjectCodegenTypes.INJECTION_NAMED_BY_TYPE); + } + + @Override + public Collection mapAnnotation(CodegenContext ctx, Annotation original, ElementKind elementKind) { + return Set.of(Annotation.create(InjectCodegenTypes.INJECTION_NAMED, original.value().orElse(""))); + } + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/MethodDefinition.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/MethodDefinition.java new file mode 100644 index 00000000000..c1da2cb22a2 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/MethodDefinition.java @@ -0,0 +1,51 @@ +/* + * 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.codegen; + +import java.util.List; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; + +import static io.helidon.codegen.CodegenUtil.capitalize; + +/** + * A relevant method definition. + * + * @param declaringType type that declares this method + * @param access access modifier of the method + * @param methodId unique ID of a method within a type + * @param constantName name of the constant of this method + * @param methodName name of the method + * @param overrides whether it overrides another method + * @param params list of parameter definitions + * @param isInjectionPoint whether this is an injection point + * @param isFinal whether the method is declared as final + */ +record MethodDefinition(TypeName declaringType, + AccessModifier access, + String methodId, + String constantName, + String methodName, + boolean overrides, + List params, + boolean isInjectionPoint, + boolean isFinal) { + public String invokeName() { + return "invoke" + capitalize(methodId()); + } +} diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ParamDefinition.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/ParamDefinition.java similarity index 98% rename from service/codegen/src/main/java/io/helidon/service/codegen/ParamDefinition.java rename to service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/ParamDefinition.java index 28b0f0fd2c4..f99f6812542 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ParamDefinition.java +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/ParamDefinition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.service.codegen; +package io.helidon.service.inject.codegen; import java.util.List; import java.util.Set; diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Qualifiers.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Qualifiers.java new file mode 100644 index 00000000000..e67bbbe4671 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/Qualifiers.java @@ -0,0 +1,75 @@ +/* + * 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.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Field; +import io.helidon.common.types.Annotated; +import io.helidon.common.types.Annotation; + +import static io.helidon.service.inject.codegen.InjectCodegenTypes.INJECTION_QUALIFIER; + +final class Qualifiers { + private Qualifiers() { + } + + static Set qualifiers(Annotated annotated) { + return annotated.annotations() + .stream() + .filter(it -> it.hasMetaAnnotation(INJECTION_QUALIFIER)) + .collect(Collectors.toUnmodifiableSet()); + } + + static void generateQualifiersConstant(ClassModel.Builder classModel, Set qualifiers) { + classModel.addField(qualifiersField -> qualifiersField + .isStatic(true) + .isFinal(true) + .name("QUALIFIERS") + .type(InjectionExtension.SET_OF_QUALIFIERS) + .addContent(Set.class) + .addContent(".of(") + .update(it -> { + Iterator iterator = qualifiers.iterator(); + while (iterator.hasNext()) { + codeGenQualifier(it, iterator.next()); + if (iterator.hasNext()) { + it.addContent(", "); + } + } + }) + .addContent(")")); + } + + private static void codeGenQualifier(Field.Builder field, Annotation qualifier) { + if (qualifier.value().isPresent()) { + field.addContent(InjectCodegenTypes.INJECT_QUALIFIER) + .addContent(".create(") + .addContentCreate(qualifier.typeName()) + .addContent(", \"" + qualifier.value().get() + "\")"); + return; + } + + field.addContent(InjectCodegenTypes.INJECT_QUALIFIER) + .addContent(".create(") + .addContentCreate(qualifier.typeName()) + .addContent(")"); + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/TypedElements.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/TypedElements.java new file mode 100644 index 00000000000..58660594a9d --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/TypedElements.java @@ -0,0 +1,175 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ElementSignature; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import static java.util.function.Predicate.not; + +final class TypedElements { + static final ElementMeta DEFAULT_CONSTRUCTOR = new ElementMeta(TypedElementInfo.builder() + .typeName(TypeNames.OBJECT) + .accessModifier(AccessModifier.PUBLIC) + .kind(ElementKind.CONSTRUCTOR) + .build()); + + private TypedElements() { + } + + static List gatherElements(TypeInfo typeInfo) { + List result = new ArrayList<>(); + + List declaredElements = typeInfo.elementInfo() + .stream() + .toList(); + + for (TypedElementInfo declaredElement : declaredElements) { + List abstractMethods = new ArrayList<>(); + + if (declaredElement.kind() == ElementKind.METHOD) { + // now find the same method on any interface (if declared there) + for (TypeInfo info : typeInfo.interfaceTypeInfo()) { + findAbstractMethod(info, declaredElement, abstractMethods); + } + // and on any super class (must be abstract) + Optional superClass = typeInfo.superTypeInfo(); + while (superClass.isPresent()) { + TypeInfo superClassInfo = superClass.get(); + findAbstractMethod(superClassInfo, declaredElement, abstractMethods); + superClass = superClassInfo.superTypeInfo(); + } + } + result.add(new TypedElements.ElementMeta(declaredElement, abstractMethods)); + } + + return result; + } + + static List gatherElements(CodegenContext ctx, + Collection contracts, + TypeInfo typeInfo) { + List result = new ArrayList<>(); + Set processedSignatures = new HashSet<>(); + + typeInfo.elementInfo() + .stream() + .filter(it -> it.kind() != ElementKind.CLASS) + .forEach(declaredElement -> { + List abstractMethods = new ArrayList<>(); + + if (declaredElement.kind() == ElementKind.METHOD) { + // now find the same method on any interface (if declared there) + for (TypeInfo info : typeInfo.interfaceTypeInfo()) { + if (!contracts.contains(ResolvedType.create(info.typeName()))) { + // only interested in contracts + continue; + } + + findAbstractMethod(info, declaredElement, abstractMethods); + } + + // and on any super class (must be abstract) + Optional superClass = typeInfo.superTypeInfo(); + while (superClass.isPresent()) { + TypeInfo superClassInfo = superClass.get(); + findAbstractMethod(superClassInfo, declaredElement, abstractMethods); + superClass = superClassInfo.superTypeInfo(); + } + } + result.add(new TypedElements.ElementMeta(declaredElement, abstractMethods)); + processedSignatures.add(declaredElement.signature()); + }); + + // we have gathered all the declared elements, now let's gather inherited elements (default methods etc.) + for (ResolvedType contract : contracts) { + Optional contractTypeInfo = ctx.typeInfo(contract.type()); + if (contractTypeInfo.isPresent()) { + TypeInfo inheritedContract = contractTypeInfo.get(); + inheritedContract.elementInfo() + .stream() + .filter(it -> it.kind() != ElementKind.CLASS) + .forEach(superElement -> { + if (!processedSignatures.add(superElement.signature())) { + // already processed + return; + } + List interfaceMethods = new ArrayList<>(); + if (superElement.kind() == ElementKind.METHOD) { + interfaceMethods.add(new DeclaredElement(inheritedContract, superElement)); + } + result.add(new TypedElements.ElementMeta(superElement, interfaceMethods)); + }); + } + } + + return result; + } + + private static void findAbstractMethod(TypeInfo info, + TypedElementInfo declaredElement, + List abstractMethods) { + info.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isStatic)) + .filter(not(ElementInfoPredicates::isPrivate)) + // we want all methods from interfaces, but only abstract methods from abstract classes + .filter(it -> ElementInfoPredicates.isAbstract(it) || ElementInfoPredicates.isDefault(it)) + .filter(it -> declaredElement.signature().equals(it.signature())) + .findFirst() + .ifPresent(it -> abstractMethods.add(new TypedElements.DeclaredElement(info, it))); + } + + /** + * Metadata of an element (field, constructor, method). + * + * @param element element declared on a type + * @param abstractMethods if the element is a method, this list contains all interface / abstract class abstract methods that + * define the contract of the method + */ + record ElementMeta(TypedElementInfo element, + List abstractMethods) { + ElementMeta(TypedElementInfo element) { + this(element, List.of()); + } + } + + /** + * Who declares the method. + * + * @param abstractType interface or abstract class + * @param element element declared on that type + */ + record DeclaredElement(TypeInfo abstractType, + TypedElementInfo element) { + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/package-info.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/package-info.java new file mode 100644 index 00000000000..5e914a90535 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Code generation for Helidon Service Injection. + */ +package io.helidon.service.inject.codegen; diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/AssignmentImpl.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/AssignmentImpl.java new file mode 100644 index 00000000000..40fa5f6107e --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/AssignmentImpl.java @@ -0,0 +1,34 @@ +/* + * 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.codegen.spi; + +import java.util.function.Consumer; + +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.common.types.TypeName; + +/** + * Assignment for code generation. The original intended purpose is to support {@code Provider} from javax and jakarta + * without a dependency (or need to understand it) in the generator code. + * + * @param usedType type to use as the dependency type using only Helidon supported types + * (i.e. {@link java.util.function.Supplier} instead of jakarta {@code Provider} + * @param codeGenerator code generator that creates appropriate type required by the target + */ +record AssignmentImpl(TypeName usedType, Consumer> codeGenerator) + implements InjectAssignment.Assignment { +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectAssignment.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectAssignment.java new file mode 100644 index 00000000000..b676b7298c3 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectAssignment.java @@ -0,0 +1,82 @@ +/* + * 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.codegen.spi; + +import java.util.Optional; +import java.util.function.Consumer; + +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.common.types.TypeName; + +/** + * Provides customized assignments for injected types. + *

    + * When supporting third party injection frameworks (such as Jakarta Inject - JSR-330), it is quite easy to map annotations + * to Helidon equivalents, but we also need to support some prescribed types for injection. + * For example in Jakarta we need to support {@code Provider} type (same as {@link java.util.function.Supplier}, just predates + * its existence). + * As we need to assign the correct type to injection points, and it must behave similar to our Service provider, we need + * to provide source code mapping from {@link java.util.function.Supplier} to the type (always three: plain type, + * {@link java.util.Optional} type, and a {@link java.util.List} of types). + */ +public interface InjectAssignment { + /** + * Map the type to the correct one. + * + * @param typeName type of the processed injection point, may be a generic type such as {@link java.util.List}, + * {@link java.util.Optional} (these are the types expected to be supported) + * @param valueSource code that obtains value from Helidon injection (if this method returns a non-empty optional, + * the provided value will be an {@link java.util.Optional} {@link java.util.function.Supplier}, + * {@link java.util.List} of {@link java.util.function.Supplier}, or a {@link java.util.function.Supplier} + * as returned by the {@link Assignment#usedType()}; + * other type combinations are not supported + * @return assignment to use, or an empty assignment if this provider does not understand the type + */ + Optional assignment(TypeName typeName, String valueSource); + + /** + * Assignment for code generation. The original intended purpose is to support {@code Provider} from javax and jakarta + * without a dependency (or need to understand it) in the generator code. + */ + interface Assignment { + /** + * Create a new assignment instance. + * + * @param usedType type to use + * @param codeGenerator code generator + * @return a new assignment + */ + static Assignment create(TypeName usedType, Consumer> codeGenerator) { + return new AssignmentImpl(usedType, codeGenerator); + } + + /** + * Type to use as the dependency type using only Helidon supported types + * (i.e. {@link java.util.function.Supplier} instead of jakarta {@code Provider}). + * + * @return Helidon supported type + */ + TypeName usedType(); + + /** + * Code generator that creates appropriate type required by the target. + * + * @return consumer of method content builder + */ + Consumer> codeGenerator(); + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectAssignmentProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectAssignmentProvider.java new file mode 100644 index 00000000000..ceb50937fa1 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectAssignmentProvider.java @@ -0,0 +1,34 @@ +/* + * 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.codegen.spi; + +import io.helidon.codegen.CodegenContext; + +/** + * A {@link java.util.ServiceLoader} provider interface to customize assignments. + * + * @see InjectAssignment + */ +public interface InjectAssignmentProvider { + /** + * Create a new provider to customize assignments. + * + * @param ctx code generation context + * @return a new assignment + */ + InjectAssignment create(CodegenContext ctx); +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectCodegenObserver.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectCodegenObserver.java new file mode 100644 index 00000000000..d62ba2d9acb --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectCodegenObserver.java @@ -0,0 +1,54 @@ +/* + * 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.codegen.spi; + +import java.util.Set; + +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.service.codegen.RegistryRoundContext; + +/** + * Processes events from inject extension. + */ +public interface InjectCodegenObserver { + + /** + * Called after a processing event that occurred in the codegen extension. + * + * @param roundContext context of the current processing round + * @param elements all elements of interest + */ + default void onProcessingEvent(RegistryRoundContext roundContext, Set elements) { + } + + /** + * Called for each injection point. + * In case the injection point is a field, the {@code element} and {@code argument} are the same instance. + * + * @param roundContext context of the current processing round + * @param service the service being processed + * @param element element that owns the injection point (constructor, method, field) + * @param argument element that is the injection point (constructor/method parameter, field) + */ + default void onInjectionPoint(RegistryRoundContext roundContext, + TypeInfo service, + TypedElementInfo element, + TypedElementInfo argument) { + + } +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectCodegenObserverProvider.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectCodegenObserverProvider.java new file mode 100644 index 00000000000..1d67c8bb8b9 --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/InjectCodegenObserverProvider.java @@ -0,0 +1,45 @@ +/* + * 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.codegen.spi; + +import java.util.Set; + +import io.helidon.codegen.Option; +import io.helidon.service.codegen.RegistryCodegenContext; + +/** + * A {@link java.util.ServiceLoader} provider interface for observers that will be + * called for code generation events. + */ +public interface InjectCodegenObserverProvider { + /** + * The provider can add supported options. + * + * @return options supported by this provider + */ + default Set> supportedOptions() { + return Set.of(); + } + + /** + * Create a new observer based on the Helidon Inject code generation context. + * + * @param context code generation context for this code generation session + * @return a new observer + */ + InjectCodegenObserver create(RegistryCodegenContext context); +} diff --git a/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/package-info.java b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/package-info.java new file mode 100644 index 00000000000..d371221a9cd --- /dev/null +++ b/service/inject/codegen/src/main/java/io/helidon/service/inject/codegen/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * SPI for code generation for Helidon Service Injection. + */ +package io.helidon.service.inject.codegen.spi; diff --git a/service/inject/codegen/src/main/java/module-info.java b/service/inject/codegen/src/main/java/module-info.java new file mode 100644 index 00000000000..7920f8a194a --- /dev/null +++ b/service/inject/codegen/src/main/java/module-info.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +/** + * Code generation for Helidon Service Registry. + */ +module io.helidon.service.inject.codegen { + requires transitive io.helidon.builder.api; + requires transitive io.helidon.codegen.classmodel; + requires transitive io.helidon.codegen; + requires transitive io.helidon.service.codegen; + requires jdk.jshell; + + exports io.helidon.service.inject.codegen; + exports io.helidon.service.inject.codegen.spi; + + uses io.helidon.service.inject.codegen.spi.InjectCodegenObserverProvider; + uses io.helidon.service.inject.codegen.spi.InjectAssignmentProvider; + + provides io.helidon.service.codegen.spi.RegistryCodegenExtensionProvider + with io.helidon.service.inject.codegen.InjectionExtensionProvider, + io.helidon.service.inject.codegen.EventObserverExtensionProvider; + + provides io.helidon.codegen.spi.AnnotationMapperProvider + with io.helidon.service.inject.codegen.MapNamedByTypeMapperProvider; + + provides io.helidon.service.inject.codegen.spi.InjectCodegenObserverProvider + with io.helidon.service.inject.codegen.EventEmitterObserverProvider; +} \ No newline at end of file diff --git a/service/inject/inject/etc/spotbugs/exclude.xml b/service/inject/inject/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..abb161e69c0 --- /dev/null +++ b/service/inject/inject/etc/spotbugs/exclude.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/service/inject/inject/pom.xml b/service/inject/inject/pom.xml new file mode 100644 index 00000000000..59f65966e6b --- /dev/null +++ b/service/inject/inject/pom.xml @@ -0,0 +1,163 @@ + + + + + + io.helidon.service.inject + helidon-service-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-inject + Helidon Service Inject + + Full injection support on top of service registry + + + + etc/spotbugs/exclude.xml + + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-configurable + + + io.helidon.common + helidon-common-config + + + io.helidon.common + helidon-common-types + + + io.helidon.builder + helidon-builder-api + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.common + helidon-common-mapper + + + io.helidon.service + helidon-service-metadata + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.common.features + helidon-common-features-api + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + + + + diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/Activators.java b/service/inject/inject/src/main/java/io/helidon/service/inject/Activators.java new file mode 100644 index 00000000000..b0aa902574a --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/Activators.java @@ -0,0 +1,883 @@ +/* + * 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.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.common.GenericType; +import io.helidon.common.LazyValue; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.service.inject.api.ActivationRequest; +import io.helidon.service.inject.api.ActivationResult; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.FactoryType; +import io.helidon.service.inject.api.GeneratedInjectService.PerInstanceDescriptor; +import io.helidon.service.inject.api.GeneratedInjectService.QualifiedFactoryDescriptor; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.InjectionPointFactory; +import io.helidon.service.inject.api.Injection.QualifiedFactory; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.InterceptionMetadata; +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.inject.api.ServiceInstance; +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +import static java.util.function.Predicate.not; + +/* + Developer note: when changing this, change ActivatorsPerLookup as well + */ + +/** + * Activator types for various types the users can implement, for real scopes (singleton, request scope). + * + * @see io.helidon.service.inject.ActivatorsPerLookup + */ +@SuppressWarnings("checkstyle:VisibilityModifier") // as long as all are inner classes, this is OK +final class Activators { + private Activators() { + } + + @SuppressWarnings("unchecked") + static Activator create(ServiceProvider provider, T instance) { + if (instance instanceof Supplier supplier) { + return new FixedSupplierActivator<>(provider, (Supplier) supplier); + } else { + return new Activators.FixedActivator<>(provider, instance); + } + } + + static Supplier> create(InjectServiceRegistryImpl registry, ServiceProvider provider) { + InjectServiceDescriptor descriptor = provider.descriptor(); + + if (descriptor.scope().equals(Injection.PerLookup.TYPE)) { + return switch (descriptor.factoryType()) { + case NONE -> new MissingDescribedActivator<>(provider); + case SERVICE -> { + if (descriptor instanceof PerInstanceDescriptor dbd) { + yield () -> new ActivatorsPerLookup.PerInstanceActivator<>(registry, provider, dbd); + } + yield () -> new ActivatorsPerLookup.SingleServiceActivator<>(provider); + } + case SUPPLIER -> () -> new ActivatorsPerLookup.SupplierActivator<>(provider); + case SERVICES -> () -> new ActivatorsPerLookup.ServicesProviderActivator<>(provider); + case INJECTION_POINT -> () -> new ActivatorsPerLookup.IpProviderActivator<>(provider); + case QUALIFIED -> () -> + new ActivatorsPerLookup.QualifiedProviderActivator<>(provider, + (QualifiedFactoryDescriptor) descriptor); + }; + } else { + return switch (descriptor.factoryType()) { + case NONE -> new MissingDescribedActivator<>(provider); + case SERVICE -> { + if (descriptor instanceof PerInstanceDescriptor dbd) { + yield () -> new PerInstanceActivator<>(registry, provider, dbd); + } + yield () -> new Activators.SingleServiceActivator<>(provider); + } + case SUPPLIER -> () -> new Activators.SupplierActivator<>(provider); + case SERVICES -> () -> new Activators.ServicesProviderActivator<>(provider); + case INJECTION_POINT -> () -> new Activators.IpProviderActivator<>(provider); + case QUALIFIED -> () -> + new Activators.QualifiedProviderActivator<>(provider, + (QualifiedFactoryDescriptor) descriptor); + }; + } + } + + abstract static class BaseActivator implements Activator { + final ServiceProvider provider; + private final ReadWriteLock instanceLock = new ReentrantReadWriteLock(); + + volatile Phase currentPhase = Phase.INIT; + + BaseActivator(ServiceProvider provider) { + this.provider = provider; + } + + // three states + // service provided an empty list (services provider etc.) + // service provided null, or activation failed + // service provided 1 or more instances + @Override + public Optional>> instances(Lookup lookup) { + /* + At the time we are looking up instances, we also resolve them to appropriate type + As this type represents a value within a scope, and not "instance per call", we can safely + store the result, unless it is lookup bound + */ + instanceLock.readLock().lock(); + try { + if (currentPhase == Phase.ACTIVE) { + return targetInstances(lookup); + } + } finally { + instanceLock.readLock().unlock(); + } + + instanceLock.writeLock().lock(); + try { + if (currentPhase != Phase.ACTIVE) { + ActivationResult res = activate(provider.activationRequest()); + if (res.failure()) { + return Optional.empty(); + } + } + return targetInstances(lookup); + } finally { + instanceLock.writeLock().unlock(); + } + } + + @Override + public InjectServiceDescriptor descriptor() { + return provider.descriptor(); + } + + @Override + public String description() { + return provider.descriptor().serviceType().classNameWithEnclosingNames() + + ":" + currentPhase; + } + + @Override + public ActivationResult activate(ActivationRequest request) { + // probably re-entering the same lock + instanceLock.writeLock().lock(); + try { + if (currentPhase == request.targetPhase()) { + // we are already there, just return success + return ActivationResult.builder() + .startingActivationPhase(currentPhase) + .finishingActivationPhase(currentPhase) + .targetActivationPhase(currentPhase) + .success(true) + .build(); + } + if (currentPhase.ordinal() > request.targetPhase().ordinal()) { + // we are already ahead, this is a problem + return ActivationResult.builder() + .startingActivationPhase(currentPhase) + .finishingActivationPhase(currentPhase) + .targetActivationPhase(request.targetPhase()) + .success(false) + .build(); + } + return doActivate(request); + } finally { + instanceLock.writeLock().unlock(); + } + } + + @Override + public ActivationResult deactivate() { + // probably re-entering the same lock + instanceLock.writeLock().lock(); + try { + ActivationResult.Builder response = ActivationResult.builder() + .success(true); + + if (!currentPhase.eligibleForDeactivation()) { + stateTransitionStart(response, Phase.DESTROYED); + return ActivationResult.builder() + .targetActivationPhase(Phase.DESTROYED) + .finishingActivationPhase(currentPhase) + .success(true) + .build(); + } + + response.startingActivationPhase(this.currentPhase); + stateTransitionStart(response, Phase.PRE_DESTROYING); + preDestroy(response); + stateTransitionStart(response, Phase.DESTROYED); + + return response.build(); + } finally { + instanceLock.writeLock().unlock(); + } + } + + @Override + public Phase phase() { + return currentPhase; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " for " + provider; + } + + protected void construct(ActivationResult.Builder response) { + } + + protected void inject(ActivationResult.Builder response) { + } + + protected void postConstruct(ActivationResult.Builder response) { + } + + protected void finishActivation(ActivationResult.Builder response) { + } + + protected void preDestroy(ActivationResult.Builder response) { + } + + protected void setTargetInstances() { + } + + protected Optional>> targetInstances(Lookup lookup) { + return targetInstances(); + } + + protected Optional>> targetInstances() { + return Optional.empty(); + } + + /** + * Check if a provider was requested as part of the lookup. + * This is the case if: + *

      + *
    • The lookup explicitly asks for a provider by its type
    • + *
    • The lookup looks for contract that is the same as this service type
    • + *
    • The lookup looks for service type that is the same as this service type
    • + *
    + * + * @param lookup requested lookup + * @param providerType type of provider this type implements + * @return whether the provider itself should be returned + */ + protected boolean requestedProvider(Lookup lookup, FactoryType providerType) { + if (lookup.factoryTypes().contains(providerType)) { + return true; + } + if (lookup.contracts().size() == 1 && lookup.contracts().contains(ResolvedType.create(descriptor().serviceType()))) { + return true; + } + if (lookup.serviceType().isPresent() && lookup.serviceType().get().equals(descriptor().serviceType())) { + return true; + } + + return false; + } + + private void stateTransitionStart(ActivationResult.Builder res, Phase phase) { + res.finishingActivationPhase(phase); + this.currentPhase = phase; + } + + private ActivationResult doActivate(ActivationRequest request) { + Phase initialPhase = this.currentPhase; + Phase startingPhase = request.startingPhase().orElse(initialPhase); + Phase targetPhase = request.targetPhase(); + this.currentPhase = startingPhase; + Phase finishingPhase = startingPhase; + + ActivationResult.Builder response = ActivationResult.builder() + .startingActivationPhase(initialPhase) + .finishingActivationPhase(startingPhase) + .targetActivationPhase(targetPhase) + .success(true); + + if (targetPhase.ordinal() > Phase.ACTIVATION_STARTING.ordinal() + && initialPhase == Phase.INIT) { + if (Phase.INIT == startingPhase + || Phase.ACTIVATION_STARTING == startingPhase + || Phase.DESTROYED == startingPhase) { + stateTransitionStart(response, Phase.ACTIVATION_STARTING); + } + } + + finishingPhase = response.finishingActivationPhase().orElse(finishingPhase); + if (response.targetActivationPhase().ordinal() >= Phase.CONSTRUCTING.ordinal()) { + stateTransitionStart(response, Phase.CONSTRUCTING); + construct(response); + } + + finishingPhase = response.finishingActivationPhase().orElse(finishingPhase); + if (response.targetActivationPhase().ordinal() >= Phase.INJECTING.ordinal() + && (Phase.CONSTRUCTING == finishingPhase)) { + stateTransitionStart(response, Phase.INJECTING); + inject(response); + } + + finishingPhase = response.finishingActivationPhase().orElse(finishingPhase); + if (response.targetActivationPhase().ordinal() >= Phase.POST_CONSTRUCTING.ordinal() + && (Phase.INJECTING == finishingPhase)) { + stateTransitionStart(response, Phase.POST_CONSTRUCTING); + postConstruct(response); + } + finishingPhase = response.finishingActivationPhase().orElse(finishingPhase); + if (response.targetActivationPhase().ordinal() >= Phase.ACTIVATION_FINISHING.ordinal() + && (Phase.POST_CONSTRUCTING == finishingPhase)) { + stateTransitionStart(response, Phase.ACTIVATION_FINISHING); + finishActivation(response); + } + finishingPhase = response.finishingActivationPhase().orElse(finishingPhase); + if (response.targetActivationPhase().ordinal() >= Phase.ACTIVE.ordinal() + && (Phase.ACTIVATION_FINISHING == finishingPhase)) { + stateTransitionStart(response, Phase.ACTIVE); + } + + if (startingPhase.ordinal() < Phase.CONSTRUCTING.ordinal() + && currentPhase.ordinal() >= Phase.CONSTRUCTING.ordinal()) { + setTargetInstances(); + } + + return response.build(); + } + } + + static class FixedActivator extends BaseActivator { + private final Optional>> instances; + + FixedActivator(ServiceProvider provider, T instance) { + super(provider); + + List> values = List.of(QualifiedInstance.create(instance, provider.serviceInfo().qualifiers())); + this.instances = Optional.of(values); + } + + @Override + protected Optional>> targetInstances() { + return instances; + } + } + + static class FixedSupplierActivator extends BaseActivator { + private final Supplier>>> instances; + + FixedSupplierActivator(ServiceProvider provider, Supplier instanceSupplier) { + super(provider); + + instances = LazyValue.create(() -> { + List> values = List.of(QualifiedInstance.create(instanceSupplier.get(), + provider.descriptor().qualifiers())); + return Optional.of(values); + }); + } + + @Override + protected Optional>> targetInstances() { + return instances.get(); + } + + } + + /** + * {@code MyService implements Contract}. + * Created for a service within each scope. + */ + static class SingleServiceActivator extends BaseActivator { + private final ReentrantLock lock = new ReentrantLock(); + protected InstanceHolder serviceInstance; + protected List> targetInstances; + + SingleServiceActivator(ServiceProvider provider) { + super(provider); + } + + @Override + protected Optional>> targetInstances() { + return Optional.ofNullable(targetInstances); + } + + @Override + protected void construct(ActivationResult.Builder response) { + if (lock.isHeldByCurrentThread()) { + throw new ServiceRegistryException("Cyclic dependency, attempting to obtain an instance of " + + provider.descriptor().serviceType().fqName() + + " while instantiating it"); + } + try { + lock.lock(); + this.serviceInstance = InstanceHolder.create(provider, provider.injectionPlan()); + this.serviceInstance.construct(); + } finally { + lock.unlock(); + } + } + + @Override + protected void setTargetInstances() { + if (serviceInstance != null) { + // lifecycle of the target instances is the same as of the service instance + // when service is created, we have the target... + this.targetInstances = List.of(QualifiedInstance.create(serviceInstance.get(), + provider.descriptor().qualifiers())); + } + } + + protected void inject(ActivationResult.Builder response) { + if (serviceInstance != null) { + serviceInstance.inject(); + } + } + + protected void postConstruct(ActivationResult.Builder response) { + if (serviceInstance != null) { + serviceInstance.postConstruct(); + } + } + + @Override + protected void preDestroy(ActivationResult.Builder response) { + if (serviceInstance != null) { + serviceInstance.preDestroy(); + } + serviceInstance = null; + targetInstances = null; + } + } + + /** + * {@code MyService implements Supplier}. + */ + static class SupplierActivator extends SingleServiceActivator { + SupplierActivator(ServiceProvider provider) { + super(provider); + } + + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (requestedProvider(lookup, FactoryType.SUPPLIER)) { + // the user requested the provider, not the provided + T instance = serviceInstance.get(); + return Optional.of(List.of(QualifiedInstance.create(instance, provider.descriptor().qualifiers()))); + } + + return super.targetInstances(lookup); + } + + @SuppressWarnings("unchecked") + @Override + protected void setTargetInstances() { + if (serviceInstance == null) { + return; + } + // the instance list is created just once, hardcoded to the instance we have just created + Supplier instanceSupplier = (Supplier) serviceInstance.get(); + this.targetInstances = List.of(QualifiedInstance.create(instanceSupplier.get(), + provider.descriptor().qualifiers())); + } + } + + /** + * {@code MyService implements QualifiedProvider}. + */ + static class QualifiedProviderActivator extends SingleServiceActivator { + static final GenericType OBJECT_GENERIC_TYPE = GenericType.create(Object.class); + + private final TypeName supportedQualifier; + private final Set supportedContracts; + private final boolean anyMatch; + + QualifiedProviderActivator(ServiceProvider provider, QualifiedFactoryDescriptor qpd) { + super(provider); + this.supportedQualifier = qpd.qualifierType(); + this.supportedContracts = provider.descriptor() + .contracts() + .stream() + .filter(it -> !QualifiedFactory.TYPE.equals(it.type())) + .collect(Collectors.toSet()); + this.anyMatch = this.supportedContracts.contains(ResolvedType.create(TypeNames.OBJECT)); + } + + @Override + protected void setTargetInstances() { + // target instances cannot be created, they are resolved on each lookup + } + + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (serviceInstance == null) { + return Optional.empty(); + } + + return lookup.qualifiers() + .stream() + .filter(it -> this.supportedQualifier.equals(it.typeName())) + .findFirst() + .flatMap(qualifier -> targetInstances(lookup, qualifier)); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Optional>> targetInstances(Lookup lookup, Qualifier qualifier) { + if (lookup.contracts().size() == 1) { + if (anyMatch || this.supportedContracts.containsAll(lookup.contracts())) { + Optional> genericType = lookup.injectionPoint() + .map(Ip::contractType); + GenericType contract = genericType + .or(lookup::contractType) + .orElse(OBJECT_GENERIC_TYPE); + + return Optional.of(targetInstances(lookup, qualifier, contract)); + } + } + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + private List> targetInstances(Lookup lookup, Qualifier qualifier, GenericType contract) { + var qProvider = (QualifiedFactory) serviceInstance.get(); + + return qProvider.list(qualifier, lookup, contract); + } + } + + /** + * {@code MyService implements InjectionPointProvider}. + */ + static class IpProviderActivator extends SingleServiceActivator { + IpProviderActivator(ServiceProvider provider) { + super(provider); + } + + @Override + protected void setTargetInstances() { + // target instances cannot be created, they are resolved on each lookup + } + + @SuppressWarnings("unchecked") + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (serviceInstance == null) { + return Optional.empty(); + } + var ipProvider = (InjectionPointFactory) serviceInstance.get(); + + if (requestedProvider(lookup, FactoryType.INJECTION_POINT)) { + // the user requested the provider, not the provided + T instance = (T) ipProvider; + return Optional.of(List.of(QualifiedInstance.create(instance, provider.descriptor().qualifiers()))); + } + + try { + return Optional.of(ipProvider.list(lookup)); + } catch (RuntimeException e) { + throw new ServiceRegistryException("Failed to list instances in InjectionPointProvider: " + + ipProvider.getClass().getName(), e); + } + } + } + + /** + * {@code MyService implements ServicesProvider}. + */ + static class ServicesProviderActivator extends SingleServiceActivator { + ServicesProviderActivator(ServiceProvider provider) { + super(provider); + } + + @Override + protected void construct(ActivationResult.Builder response) { + super.construct(response); + } + + @SuppressWarnings("unchecked") + @Override + protected void setTargetInstances() { + if (serviceInstance == null) { + return; + } + // the instance list is created just once, hardcoded to the instance we have just created + Injection.ServicesFactory instanceSupplier = (Injection.ServicesFactory) serviceInstance.get(); + targetInstances = instanceSupplier.services(); + } + + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (requestedProvider(lookup, FactoryType.SERVICES)) { + return Optional.of(List.of(QualifiedInstance.create(serviceInstance.get(), descriptor().qualifiers()))); + } + + if (targetInstances == null) { + return Optional.empty(); + } + + List> response = new ArrayList<>(); + for (QualifiedInstance instance : targetInstances) { + if (lookup.matchesQualifiers(instance.qualifiers())) { + response.add(instance); + } + } + + return Optional.of(List.copyOf(response)); + } + } + + /** + * Service annotated {@link io.helidon.service.inject.api.Injection.PerInstance}. + */ + static class PerInstanceActivator extends BaseActivator { + private final InjectServiceRegistryImpl registry; + private final ResolvedType createFor; + private final Lookup createForLookup; + private List> serviceInstances; + private List> targetInstances; + + PerInstanceActivator(InjectServiceRegistryImpl registry, ServiceProvider provider, PerInstanceDescriptor dbd) { + super(provider); + + this.registry = registry; + this.createFor = ResolvedType.create(dbd.createFor()); + this.createForLookup = Lookup.builder() + .addContract(createFor) + .build(); + } + + static Map> updatePlan(Map> injectionPlan, + ServiceInstance driver, + Qualifier name) { + + Set ips = Set.copyOf(injectionPlan.keySet()); + + Set contracts = driver.contracts(); + + Map> updatedPlan = new HashMap<>(injectionPlan); + + for (Dependency dependency : ips) { + Ip ip = Ip.create(dependency); + // injection point for the driving instance + if (contracts.contains(ResolvedType.create(ip.contract())) + && ip.qualifiers().isEmpty()) { + if (ServiceInstance.TYPE.equals(ip.typeName())) { + // if the injection point has the same contract, no qualifiers, then it is the driving instance + updatedPlan.put(ip, new IpPlan<>(() -> driver, injectionPlan.get(ip).descriptors())); + } else { + // if the injection point has the same contract, no qualifiers, then it is the driving instance + updatedPlan.put(ip, new IpPlan<>(driver, injectionPlan.get(ip).descriptors())); + } + } + // injection point for the service name + if (TypeNames.STRING.equals(ip.contract())) { + // @InstanceName String name + if (ip.qualifiers() + .stream() + .anyMatch(it -> Injection.InstanceName.TYPE.equals(it.typeName()))) { + updatedPlan.put(ip, + new IpPlan<>(() -> name.value().orElse(Injection.Named.DEFAULT_NAME), + injectionPlan.get(ip).descriptors())); + } + } + } + + return updatedPlan; + } + + @Override + protected void construct(ActivationResult.Builder response) { + // at this moment, we must resolve services that are driving this instance + + // we do not want to use lookup, as that is doing too much for us + List> drivingInstances = driversFromPlan(provider.injectionPlan(), createFor) + .stream() + .map(registry::serviceManager) + .flatMap(it -> it.activator() + .instances(createForLookup) + .stream() + .flatMap(List::stream) + .map(qi -> it.registryInstance(createForLookup, qi))) + .toList(); + + serviceInstances = drivingInstances.stream() + .map(it -> QualifiedServiceInstance.create(provider, it)) + .toList(); + for (QualifiedServiceInstance serviceInstance : serviceInstances) { + serviceInstance.serviceInstance().construct(); + } + } + + @Override + protected void inject(ActivationResult.Builder response) { + for (QualifiedServiceInstance instance : serviceInstances) { + instance.serviceInstance().inject(); + } + } + + @Override + protected void postConstruct(ActivationResult.Builder response) { + for (QualifiedServiceInstance instance : serviceInstances) { + instance.serviceInstance().postConstruct(); + } + } + + @Override + protected void setTargetInstances() { + if (serviceInstances != null) { + targetInstances = serviceInstances.stream() + .map(it -> QualifiedInstance.create(it.serviceInstance().get(), it.qualifiers())) + .toList(); + } + } + + @Override + protected void preDestroy(ActivationResult.Builder response) { + if (serviceInstances != null) { + serviceInstances.stream() + .map(QualifiedServiceInstance::serviceInstance) + .forEach(InstanceHolder::preDestroy); + } + serviceInstances = null; + targetInstances = null; + } + + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (targetInstances == null) { + return Optional.empty(); + } + List> response = new ArrayList<>(); + for (QualifiedInstance instance : targetInstances) { + if (lookup.matchesQualifiers(instance.qualifiers())) { + response.add(instance); + } + } + + return Optional.of(List.copyOf(response)); + } + + private List driversFromPlan(Map> ipSupplierMap, ResolvedType createFor) { + // I need the list of descriptors from the injection plan + for (Map.Entry> entry : ipSupplierMap.entrySet()) { + Dependency dependency = entry.getKey(); + Ip ip = Ip.create(dependency); + if (createFor.equals(ip.contract()) + && ip.qualifiers().size() == 1 + && ip.qualifiers().contains(Qualifier.WILDCARD_NAMED)) { + return List.of(entry.getValue().descriptors()); + } + } + // there is not + return registry.servicesByContract(createFor); + } + + private record QualifiedServiceInstance(InstanceHolder serviceInstance, + Set qualifiers) { + static QualifiedServiceInstance create(ServiceProvider provider, ServiceInstance driver) { + Set qualifiers = driver.qualifiers(); + Qualifier name = qualifiers.stream() + .filter(not(Qualifier.WILDCARD_NAMED::equals)) + .filter(it -> Injection.Named.TYPE.equals(it.typeName())) + .findFirst() + .orElse(Qualifier.DEFAULT_NAMED); + Set newQualifiers = provider.descriptor().qualifiers() + .stream() + .filter(not(Qualifier.WILDCARD_NAMED::equals)) + .collect(Collectors.toSet()); + newQualifiers.add(name); + + Map> injectionPlan = updatePlan(provider.injectionPlan(), driver, name); + + return new QualifiedServiceInstance<>(InstanceHolder.create(provider, injectionPlan), newQualifiers); + } + } + } + + static class InstanceHolder { + private final DependencyContext ctx; + private final InterceptionMetadata interceptionMetadata; + private final InjectServiceDescriptor source; + + private volatile T instance; + + private InstanceHolder(DependencyContext ctx, + InterceptionMetadata interceptionMetadata, + InjectServiceDescriptor source) { + this.ctx = ctx; + this.interceptionMetadata = interceptionMetadata; + this.source = source; + } + + static InstanceHolder create(ServiceProvider serviceProvider, Map> injectionPlan) { + // the same instance is returned for the lifetime of the service provider + return new InstanceHolder<>(InjectionContext.create(injectionPlan), + serviceProvider.interceptionMetadata(), + serviceProvider.descriptor()); + } + + T get() { + return instance; + } + + @SuppressWarnings("unchecked") + void construct() { + instance = (T) source.instantiate(ctx, interceptionMetadata); + } + + void inject() { + // using linked set, so we can see in debugging what was injected first + Set injected = new LinkedHashSet<>(); + source.inject(ctx, interceptionMetadata, injected, instance); + } + + void postConstruct() { + source.postConstruct(instance); + } + + void preDestroy() { + source.preDestroy(instance); + } + } + + private static class MissingDescribedActivator implements Supplier> { + private static final System.Logger LOGGER = System.getLogger(Activators.class.getName()); + private final String serviceType; + + private MissingDescribedActivator(ServiceProvider provider) { + this.serviceType = provider.serviceInfo().serviceType().fqName(); + + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, + "The registry knows of a descriptor that was generated on demand " + + "(@" + Injection.Describe.class.getName() + "), which expects an instance configured for " + + "it, either" + + " when creating the registry through configuration, or when creating a scope. " + + "Service that does not have an instance registered: " + + serviceType + + ", if there is an attempt on injecting this type, a runtime exception " + + "will be thrown"); + } + + } + + @Override + public Activator get() { + throw new ServiceRegistryException("Instance for " + serviceType + " must be provided explicitly " + + "either during startup, or when creating a scope." + + " Cannot provide an instance for a descriptor without a service."); + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ActivatorsPerLookup.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ActivatorsPerLookup.java new file mode 100644 index 00000000000..8f2c7b051e9 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ActivatorsPerLookup.java @@ -0,0 +1,368 @@ +/* + * 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.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.common.GenericType; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.service.inject.api.ActivationResult; +import io.helidon.service.inject.api.Activator.Phase; +import io.helidon.service.inject.api.FactoryType; +import io.helidon.service.inject.api.GeneratedInjectService; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedFactory; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Injection.ServicesFactory; +import io.helidon.service.inject.api.InterceptionMetadata; +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.inject.api.ServiceInstance; +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +import static io.helidon.service.inject.Activators.QualifiedProviderActivator.OBJECT_GENERIC_TYPE; +import static java.util.function.Predicate.not; + +/* + Developer note: when changing this, also change Activators + */ + +/** + * Activator types for various types the users can implement, for services without scope + * (with {@link io.helidon.service.inject.api.Injection.PerLookup} as its scope in descriptor). + *

    + * Activators in this type create an instance per injection, or per call to {@link java.util.function.Supplier#get()}. + * + * @see io.helidon.service.inject.Activators + */ +@SuppressWarnings("checkstyle:VisibilityModifier") // as long as all are inner classes, this is OK +final class ActivatorsPerLookup { + private ActivatorsPerLookup() { + } + + /** + * {@code MyService implements Contract}. + * Created for a service within each scope. + */ + static class SingleServiceActivator extends Activators.BaseActivator { + protected OnDemandInstance serviceInstance; + + SingleServiceActivator(ServiceProvider provider) { + super(provider); + } + + @Override + protected Optional>> targetInstances() { + if (serviceInstance == null) { + return Optional.empty(); + } + return Optional.of(List.of(QualifiedInstance.create(serviceInstance.get(currentPhase), + provider.descriptor().qualifiers()))); + } + + @Override + protected void construct(ActivationResult.Builder response) { + this.serviceInstance = new OnDemandInstance<>(InjectionContext.create(provider.injectionPlan()), + provider.interceptionMetadata(), + provider.descriptor()); + } + + @Override + protected void preDestroy(ActivationResult.Builder response) { + this.serviceInstance = null; + } + } + + /** + * {@code MyService implements Supplier}. + */ + static class SupplierActivator extends SingleServiceActivator { + SupplierActivator(ServiceProvider provider) { + super(provider); + } + + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (requestedProvider(lookup, FactoryType.SUPPLIER)) { + if (serviceInstance == null) { + return Optional.empty(); + } + // the user requested the provider, not the provided + T instance = serviceInstance.get(currentPhase); + return Optional.of(List.of(QualifiedInstance.create(instance, provider.descriptor().qualifiers()))); + } + return targetInstances(); + } + + @SuppressWarnings("unchecked") + @Override + protected Optional>> targetInstances() { + if (serviceInstance == null) { + return Optional.empty(); + } + Supplier instanceSupplier = (Supplier) serviceInstance.get(currentPhase); + return Optional.of(List.of(QualifiedInstance.create(instanceSupplier.get(), + provider.descriptor().qualifiers()))); + } + } + + /** + * {@code MyService implements QualifiedProvider}. + */ + static class QualifiedProviderActivator extends SingleServiceActivator { + private final TypeName supportedQualifier; + private final Set supportedContracts; + private final boolean anyMatch; + + QualifiedProviderActivator(ServiceProvider provider, GeneratedInjectService.QualifiedFactoryDescriptor qpd) { + super(provider); + this.supportedQualifier = qpd.qualifierType(); + this.supportedContracts = provider.descriptor().contracts() + .stream() + .filter(it -> !QualifiedFactory.TYPE.equals(it.type())) + .collect(Collectors.toSet()); + this.anyMatch = this.supportedContracts.contains(ResolvedType.create(TypeNames.OBJECT)); + } + + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (serviceInstance == null) { + return Optional.empty(); + } + + return lookup.qualifiers() + .stream() + .filter(it -> this.supportedQualifier.equals(it.typeName())) + .findFirst() + .flatMap(qualifier -> targetInstances(lookup, qualifier)); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Optional>> targetInstances(Lookup lookup, Qualifier qualifier) { + if (lookup.contracts().size() == 1) { + if (anyMatch || this.supportedContracts.containsAll(lookup.contracts())) { + Optional> genericType = lookup.injectionPoint() + .map(Ip::contractType); + GenericType contract = genericType + .or(lookup::contractType) + .orElse(OBJECT_GENERIC_TYPE); + + return Optional.of(targetInstances(lookup, qualifier, contract)); + } + } + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + private List> targetInstances(Lookup lookup, Qualifier qualifier, GenericType contract) { + var qProvider = (QualifiedFactory) serviceInstance.get(currentPhase); + + return qProvider.list(qualifier, lookup, contract); + } + } + + /** + * {@code MyService implements InjectionPointProvider}. + */ + static class IpProviderActivator extends SingleServiceActivator { + IpProviderActivator(ServiceProvider provider) { + super(provider); + } + + @SuppressWarnings("unchecked") + @Override + protected Optional>> targetInstances(Lookup lookup) { + if (serviceInstance == null) { + return Optional.empty(); + } + var ipProvider = (Injection.InjectionPointFactory) serviceInstance.get(currentPhase); + + if (requestedProvider(lookup, FactoryType.INJECTION_POINT)) { + // the user requested the provider, not the provided + T instance = (T) ipProvider; + return Optional.of(List.of(QualifiedInstance.create(instance, provider.descriptor().qualifiers()))); + } + + try { + return Optional.of(ipProvider.list(lookup)); + } catch (RuntimeException e) { + throw new ServiceRegistryException("Failed to list instances in InjectionPointProvider: " + + ipProvider.getClass().getName(), e); + } + } + } + + /** + * {@code MyService implements ServicesProvider}. + */ + static class ServicesProviderActivator extends SingleServiceActivator { + ServicesProviderActivator(ServiceProvider provider) { + super(provider); + } + + @SuppressWarnings("unchecked") + @Override + protected Optional>> targetInstances(Lookup lookup) { + ServicesFactory instanceSupplier = (Injection.ServicesFactory) serviceInstance.get(currentPhase); + + if (requestedProvider(lookup, FactoryType.SERVICES)) { + return Optional.of(List.of(QualifiedInstance.create((T) instanceSupplier, descriptor().qualifiers()))); + } + + List> response = new ArrayList<>(); + for (QualifiedInstance instance : instanceSupplier.services()) { + if (lookup.matchesQualifiers(instance.qualifiers())) { + response.add(instance); + } + } + + return Optional.of(List.copyOf(response)); + } + } + + /** + * Service annotated {@link io.helidon.service.inject.api.Injection.PerInstance}. + */ + static class PerInstanceActivator extends Activators.BaseActivator { + private final InjectServiceRegistryImpl registry; + private final ResolvedType createFor; + + private List> serviceInstances; + + PerInstanceActivator(InjectServiceRegistryImpl registry, + ServiceProvider provider, + GeneratedInjectService.PerInstanceDescriptor dbd) { + super(provider); + + this.registry = registry; + this.createFor = ResolvedType.create(dbd.createFor()); + } + + @Override + protected void construct(ActivationResult.Builder response) { + // at this moment, we must resolve services that are driving this instance + List services = registry.servicesByContract(createFor); + + serviceInstances = services.stream() + .map(registry::serviceManager) + .flatMap(it -> it.activator() + .instances(Lookup.EMPTY) + .stream() + .flatMap(List::stream) + .map(qi -> it.registryInstance(Lookup.EMPTY, qi))) + .map(it -> QualifiedOnDemandInstance.create(provider, it)) + .collect(Collectors.toList()); + } + + @Override + protected Optional>> targetInstances() { + return Optional.of(serviceInstances.stream() + .map(it -> QualifiedInstance.create(it.serviceInstance() + .get(currentPhase), + it.qualifiers())) + .toList()); + } + + @Override + protected void preDestroy(ActivationResult.Builder response) { + this.serviceInstances = null; + } + } + + private record QualifiedOnDemandInstance(OnDemandInstance serviceInstance, + Set qualifiers) { + static QualifiedOnDemandInstance create(ServiceProvider provider, + ServiceInstance driver) { + Set qualifiers = driver.qualifiers(); + Qualifier name = qualifiers.stream() + .filter(not(Qualifier.WILDCARD_NAMED::equals)) + .filter(it -> Injection.Named.TYPE.equals(it.typeName())) + .findFirst() + .orElse(Qualifier.DEFAULT_NAMED); + Set newQualifiers = provider.descriptor().qualifiers() + .stream() + .filter(not(Qualifier.WILDCARD_NAMED::equals)) + .collect(Collectors.toSet()); + newQualifiers.add(name); + + Map> injectionPlan = Activators.PerInstanceActivator.updatePlan(provider.injectionPlan(), + driver, + name); + + return new QualifiedOnDemandInstance<>(new OnDemandInstance<>(InjectionContext.create(injectionPlan), + provider.interceptionMetadata(), + provider.descriptor()), + newQualifiers); + } + } + + static class OnDemandInstance { + private final ReentrantLock lock = new ReentrantLock(); + private final DependencyContext ctx; + private final InterceptionMetadata interceptionMetadata; + private final InjectServiceDescriptor source; + + OnDemandInstance(DependencyContext ctx, + InterceptionMetadata interceptionMetadata, + InjectServiceDescriptor source) { + this.ctx = ctx; + this.interceptionMetadata = interceptionMetadata; + this.source = source; + } + + @SuppressWarnings("unchecked") + T get(Phase phase) { + if (lock.isHeldByCurrentThread()) { + throw new ServiceRegistryException("Cyclic dependency, attempting to obtain an instance of " + + source.serviceType().fqName() + + " while instantiating it"); + } + try { + lock.lock(); + if (phase.ordinal() >= Phase.CONSTRUCTING.ordinal()) { + T instance = (T) source.instantiate(ctx, interceptionMetadata); + if (phase.ordinal() >= Phase.INJECTING.ordinal()) { + Set injected = new LinkedHashSet<>(); + source.inject(ctx, interceptionMetadata, injected, instance); + } + if (phase.ordinal() >= Phase.POST_CONSTRUCTING.ordinal()) { + source.postConstruct(instance); + } + return instance; + } + } finally { + lock.unlock(); + } + throw new ServiceRegistryException("An instance was requested even though lifecycle is limited to phase: " + phase); + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/Binding.java b/service/inject/inject/src/main/java/io/helidon/service/inject/Binding.java new file mode 100644 index 00000000000..6151718818d --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/Binding.java @@ -0,0 +1,51 @@ +/* + * 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 io.helidon.common.types.TypeName; +import io.helidon.service.registry.Service; + +/** + * A binding instance, if available at runtime, will be expected to provide a plan for all service provider's injection + * points. + *

    + * Implementations of this contract are normally code generated, although then can be programmatically written by the developer + * for special cases. + *

    + * Binding instances MUST NOT have injection points. + */ +@Service.Contract +public interface Binding { + /** + * Type name of this interface. + */ + TypeName TYPE = TypeName.create(Binding.class); + + /** + * Name of this application binding. + * + * @return binding name + */ + String name(); + + /** + * Configure injection points and dependencies in this application. + * + * @param binder the binder used to register the service provider injection plans + */ + void configure(InjectionPlanBinder binder); +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ConfigProvider.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ConfigProvider.java new file mode 100644 index 00000000000..b2fbc023440 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ConfigProvider.java @@ -0,0 +1,34 @@ +/* + * 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; + +import java.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.common.config.Config; +import io.helidon.service.registry.Service; + +@Service.Provider +@Weight(0.1) // a very low weight, as this just provides an empty config +class ConfigProvider implements Supplier { + private final Config config = Config.empty(); + + @Override + public Config get() { + return config; + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ConfigValueProvider.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ConfigValueProvider.java new file mode 100644 index 00000000000..124dea7c238 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ConfigValueProvider.java @@ -0,0 +1,118 @@ +/* + * 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; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.helidon.common.GenericType; +import io.helidon.common.config.Config; +import io.helidon.common.config.ConfigException; +import io.helidon.common.config.ConfigValue; +import io.helidon.common.mapper.MapperManager; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.Configuration; +import io.helidon.service.inject.api.Injection; +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.registry.ServiceRegistry; + +@Injection.Singleton +class ConfigValueProvider implements Injection.QualifiedFactory { + private final ServiceRegistry registry; + private final Supplier config; + + ConfigValueProvider(ServiceRegistry registry, Supplier config) { + this.registry = registry; + this.config = config; + } + + @Override + public Optional> first(Qualifier qualifier, Lookup lookup, GenericType type) { + String value = qualifier.stringValue() + .orElseThrow(() -> new IllegalStateException("Annotation " + + Configuration.Value.class.getName() + + " must have a value defined, yet received it without " + + "value: " + qualifier)); + + // if it contains :, then it separates default value from key + String defaultValue = null; + String key; + + if (value.contains(":")) { + int index = value.indexOf(':'); + + key = value.substring(0, index); + defaultValue = value.substring(index + 1); + } else { + if (value.isEmpty()) { + Ip ip = lookup.injectionPoint() + .orElseThrow(() -> new IllegalStateException("Configuration.Value does not specify a value " + + "(configuration key), yet injection point is " + + "not provided. Cannot infer key.")); + key = ip.service().fqName() + "." + ip.name(); + } else { + key = value; + } + } + + Config configInstance = config.get(); + ConfigValue configValue = configInstance.get(key) + .as(type.rawType()); + + Object result; + if (configValue.isEmpty()) { + result = defaultValue(configInstance, qualifier, type, defaultValue); + } else { + result = configValue.get(); + } + + return Optional.of(Injection.QualifiedInstance.create(result, qualifier)); + } + + @SuppressWarnings("unchecked") + private Object defaultValue(Config config, + Qualifier qualifier, + GenericType type, + String defaultValue) { + if (defaultValue != null) { + return MapperManager.global() + .map(defaultValue, GenericType.STRING, type, "config"); + } + + // there may be a provider + Optional typeName = qualifier.typeValue("defaultProvider"); + if (typeName.isEmpty()) { + return null; + } + TypeName defaultProvider = typeName.get(); + if (defaultProvider.equals(Configuration.Value.NoProvider.TYPE)) { + // no custom provider + return null; + } + Object provider = registry.first(defaultProvider) + .orElseThrow(() -> new ConfigException("Default value provider: " + defaultProvider.fqName() + + " must be available through service registry," + + " maybe annotate it with @Service.Provider?")); + + // provider implements Function + Function providerAsFunction = (Function) provider; + return providerAsFunction.apply(config); + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/Contracts.java b/service/inject/inject/src/main/java/io/helidon/service/inject/Contracts.java new file mode 100644 index 00000000000..939f9fc81be --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/Contracts.java @@ -0,0 +1,73 @@ +/* + * 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.util.Set; + +import io.helidon.common.types.ResolvedType; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; + +/* +Management of contracts, to return correct contracts for services created from other services, +such as a InjectionPointProvider, ServicesProvider, or Supplier + */ +final class Contracts { + private Contracts() { + } + + static ContractLookup create(InjectServiceInfo descriptor) { + Set contracts = descriptor.contracts(); + + return switch (descriptor.factoryType()) { + case NONE, SERVICE -> new FixedContracts(contracts); + default -> new ProviderContracts(contracts, descriptor.factoryContracts()); + }; + } + + interface ContractLookup { + Set contracts(Lookup lookup); + } + + private static final class FixedContracts implements ContractLookup { + private final Set contracts; + + FixedContracts(Set contracts) { + this.contracts = contracts; + } + + @Override + public Set contracts(Lookup lookup) { + return contracts; + } + } + + private static final class ProviderContracts implements ContractLookup { + private final Set contracts; + private final Set factoryContracts; + + ProviderContracts(Set contracts, Set factoryContracts) { + this.contracts = contracts; + this.factoryContracts = factoryContracts; + } + + @Override + public Set contracts(Lookup lookup) { + return contracts; + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/CoreWrappers.java b/service/inject/inject/src/main/java/io/helidon/service/inject/CoreWrappers.java new file mode 100644 index 00000000000..5157774b59c --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/CoreWrappers.java @@ -0,0 +1,142 @@ +/* + * 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.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.FactoryType; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.Ip; +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceInfo; + +class CoreWrappers { + private CoreWrappers() { + } + + static InjectServiceDescriptor create(ServiceDescriptor serviceInfo) { + if (serviceInfo instanceof InjectServiceDescriptor inj) { + return inj; + } + return new CoreDescriptor<>(serviceInfo); + } + + static class CoreServiceInfo implements InjectServiceInfo { + private final ServiceInfo delegate; + private final TypeName scope; + private final FactoryType providerType; + + private CoreServiceInfo(ServiceInfo delegate) { + this.delegate = delegate; + this.scope = scope(delegate); + this.providerType = delegate.factoryContracts().isEmpty() + ? FactoryType.SERVICE + : FactoryType.SUPPLIER; + } + + @Override + public double weight() { + return delegate.weight(); + } + + @Override + public TypeName scope() { + return scope; + } + + @Override + public TypeName serviceType() { + return delegate.serviceType(); + } + + @Override + public TypeName descriptorType() { + return delegate.descriptorType(); + } + + @Override + public Set contracts() { + return delegate.contracts(); + } + + @Override + public boolean isAbstract() { + return delegate.isAbstract(); + } + + @Override + public FactoryType factoryType() { + return providerType; + } + + private static TypeName scope(ServiceInfo delegate) { + // if the core service is a supplier, we expect to get a new instance each time + // otherwise it is a de-facto singleton + return delegate.factoryContracts().isEmpty() + ? Injection.Singleton.TYPE + : Injection.PerLookup.TYPE; + } + } + + static final class CoreDescriptor extends CoreServiceInfo implements InjectServiceDescriptor { + private final ServiceDescriptor delegate; + private final List injectionPoints; + + CoreDescriptor(ServiceDescriptor delegate) { + super(delegate); + + this.delegate = delegate; + + this.injectionPoints = delegate.dependencies() + .stream() + .map(it -> Ip.builder() + .from(it) + .elementKind(ElementKind.CONSTRUCTOR) + .build()) + .collect(Collectors.toList()); + } + + @Override + public List dependencies() { + return injectionPoints; + } + + @Override + public Object instantiate(DependencyContext ctx) { + return delegate.instantiate(ctx); + } + + @Override + public Object instantiate(DependencyContext ctx, InterceptionMetadata interceptionMetadata) { + return delegate.instantiate(ctx); + } + + @Override + public io.helidon.service.registry.ServiceInfo coreInfo() { + return delegate; + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/EventManagerImpl.java b/service/inject/inject/src/main/java/io/helidon/service/inject/EventManagerImpl.java new file mode 100644 index 00000000000..90d526f0aa2 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/EventManagerImpl.java @@ -0,0 +1,184 @@ +/* + * 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.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.helidon.common.types.ResolvedType; +import io.helidon.service.inject.api.EventDispatchException; +import io.helidon.service.inject.api.EventManager; +import io.helidon.service.inject.api.GeneratedInjectService.EventObserverRegistration; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.registry.Service; + +@Injection.Singleton +class EventManagerImpl implements EventManager { + private static final System.Logger LOGGER = System.getLogger(EventManager.class.getName()); + + private final Supplier> registrations; + private final Map>> listeners = new HashMap<>(); + private final Map>> asyncListeners = new HashMap<>(); + private final ReadWriteLock listenersLock = new ReentrantReadWriteLock(); + private final ExecutorService executor; + + @Injection.Inject + EventManagerImpl(Supplier> registrations, + @Injection.NamedByType(EventManager.class) Optional executorService) { + this.registrations = registrations; + this.executor = executorService.orElseGet(() -> Executors.newThreadPerTaskExecutor( + Thread.ofVirtual() + .name("inject-event-manager-", 0) + .factory())); + } + + @Override + public void register(ResolvedType eventType, Consumer eventConsumer, Set qualifiers) { + listenersLock.writeLock().lock(); + try { + listeners.computeIfAbsent(new RegistrationKey(eventType, qualifiers), + k -> new ArrayList<>()) + .add(eventConsumer); + } finally { + listenersLock.writeLock().unlock(); + } + } + + @Override + public void registerAsync(ResolvedType eventType, Consumer eventConsumer, Set qualifiers) { + listenersLock.writeLock().lock(); + try { + asyncListeners.computeIfAbsent(new RegistrationKey(eventType, qualifiers), + k -> new ArrayList<>()) + .add(eventConsumer); + } finally { + listenersLock.writeLock().unlock(); + } + } + + @Override + public void emit(ResolvedType eventObjectType, Object eventObject, Set qualifiers) { + // first get all consumers + listenersLock.readLock().lock(); + + List> consumers; + List> asyncConsumers; + try { + consumers = listeners.get(new RegistrationKey(eventObjectType, qualifiers)); + asyncConsumers = asyncListeners.get(new RegistrationKey(eventObjectType, qualifiers)); + } finally { + listenersLock.readLock().unlock(); + } + + // async consumers should not block anything, just fire and forget + if (asyncConsumers != null) { + fireAndForget(asyncConsumers, eventObject); + } + // consumers block the execution + if (consumers != null) { + fire(consumers, eventObject); + } + } + + @Override + public CompletionStage emitAsync(ResolvedType eventObjectType, T eventObject, Set qualifiers) { + // first get all consumers + listenersLock.readLock().lock(); + + List> consumers; + List> asyncConsumers; + try { + consumers = listeners.get(new RegistrationKey(eventObjectType, qualifiers)); + asyncConsumers = asyncListeners.get(new RegistrationKey(eventObjectType, qualifiers)); + } finally { + listenersLock.readLock().unlock(); + } + + // async consumers should not block anything, just fire and forget + if (asyncConsumers != null) { + fireAndForget(asyncConsumers, eventObject); + } + // consumers block the execution + if (consumers != null) { + // we do care about results of this (it may throw only EventDispatchException or an error + return CompletableFuture.supplyAsync(() -> { + fire(consumers, eventObject); + return eventObject; + }, + executor); + } + return CompletableFuture.completedFuture(eventObject); + } + + @Service.PostConstruct + void init() { + var registrationList = registrations.get(); + registrationList.forEach(reg -> reg.register(this)); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void fire(List> consumers, Object eventObject) { + List thrown = new ArrayList<>(); + for (Consumer consumer : consumers) { + try { + consumer.accept(eventObject); + } catch (Exception e) { + thrown.add(e); + } + } + if (thrown.isEmpty()) { + return; + } + + var exception = new EventDispatchException("Event dispatching failed, see suppressed exceptions", thrown.getFirst()); + for (int i = 1; i < thrown.size(); i++) { + exception.addSuppressed(thrown.get(i)); + } + + throw exception; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void fireAndForget(List> asyncConsumers, Object eventObject) { + for (Consumer asyncConsumer : asyncConsumers) { + executor.submit(() -> { + try { + asyncConsumer.accept(eventObject); + } catch (Exception e) { + LOGGER.log(System.Logger.Level.WARNING, "Asynchronous event dispatch failed.", e); + } + }); + } + } + + private record RegistrationKey(ResolvedType eventObject, + Set qualifiers) { + } +} 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 new file mode 100644 index 00000000000..e021bb27c4d --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectConfigBlueprint.java @@ -0,0 +1,88 @@ +/* + * 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.util.List; +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.configurable.LruCache; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.registry.ServiceRegistryConfig; + +/** + * Helidon Inject configuration options. + */ +@Prototype.Blueprint +@Prototype.Configured("registry") +interface InjectConfigBlueprint extends ServiceRegistryConfig { + /** + * LRU cache to use for caching lookup. Only needed if {@link #lookupCacheEnabled()} is set to {@code true}. + * This allows customization of the LRU cache. + * + * @return LRU cache to use for lookup caching, or empty to use the default instance (or when not used at all) + */ + @Option.Configured + Optional>> lookupCache(); + + /** + * Flag indicating whether service lookups + * (i.e., via {@link io.helidon.service.inject.api.InjectRegistry#first(io.helidon.service.inject.api.Lookup)}) are cached. + * + * @return the flag indicating whether service lookups are cached, defaults to {@code false} + */ + @Option.Configured + boolean lookupCacheEnabled(); + + /** + * Flag indicating whether runtime interception is enabled. + * If set to {@code false}, methods will be invoked without any interceptors, even if interceptors are available. + * + * @return whether to intercept calls at runtime, defaults to {@code true} + */ + @Option.Configured + @Option.DefaultBoolean(true) + boolean interceptionEnabled(); + + /** + * In certain conditions Injection services should be initialized but not started (i.e., avoiding calls to + * {@code PostConstruct} + * etc.). This can be used in special cases where the normal Injection startup should limit lifecycle up to a given phase. + * + * @return the phase to stop at during lifecycle + */ + @Option.Configured + @Option.Default("ACTIVE") + Activator.Phase limitRuntimePhase(); + + /** + * Flag indicating whether compile-time generated {@link Binding}'s + * should be used at Injection's startup initialization. + * Even if set to {@code true}, this is effective only if an {@link io.helidon.service.inject.Binding} + * was generated using Helidon Service Maven Plugin. + * + * @return the flag indicating whether the provider is permitted to use binding generated code from compile-time, + * defaults to {@code true} + * @see io.helidon.service.inject.Binding + */ + @Option.Configured + @Option.DefaultBoolean(true) + boolean useBinding(); +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectRegistryManager.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectRegistryManager.java new file mode 100644 index 00000000000..91f0d78e0d0 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectRegistryManager.java @@ -0,0 +1,452 @@ +/* + * 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.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +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.InjectRegistry__ServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.InterceptionMetadata__ServiceDescriptor; +import io.helidon.service.inject.api.Scopes__ServiceDescriptor; +import io.helidon.service.metadata.DescriptorMetadata; +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.DescriptorHandler; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceDiscovery; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceLoader__ServiceDescriptor; +import io.helidon.service.registry.ServiceRegistryException; +import io.helidon.service.registry.ServiceRegistryManager; +import io.helidon.service.registry.VirtualDescriptor; + +/** + * Manager is responsible for managing the state of a {@link io.helidon.service.inject.api.InjectRegistry}. + * Each manager instances owns a single service registry. + *

    + * To use a singleton service across application, either pass it through parameters, or use + * {@link io.helidon.service.registry.GlobalServiceRegistry}. + */ +public class InjectRegistryManager implements ServiceRegistryManager { + static final Comparator SERVICE_INFO_COMPARATOR = Comparator + .comparingDouble(InjectServiceInfo::weight) + .reversed() + .thenComparing((f, s) -> { + if (f.qualifiers().isEmpty() && s.qualifiers().isEmpty()) { + return 0; + } + if (f.qualifiers().isEmpty()) { + return -1; + } + if (s.qualifiers().isEmpty()) { + return 1; + } + return 0; + }) + .thenComparing(InjectServiceInfo::serviceType); + private static final System.Logger LOGGER = System.getLogger(InjectRegistryManager.class.getName()); + private final ReentrantReadWriteLock lifecycleLock = new ReentrantReadWriteLock(); + private final InjectConfig config; + private final ServiceDiscovery discovery; + + private InjectServiceRegistryImpl registry; + + InjectRegistryManager(InjectConfig config, ServiceDiscovery serviceDiscovery) { + this.config = config; + this.discovery = serviceDiscovery; + } + + /** + * Create a new inject registry manager with default configuration. + * + * @return a new inject registry manager + */ + public static InjectRegistryManager create() { + return create(InjectConfig.create()); + } + + /** + * Create a new inject registry manager with custom configuration. + * + * @param config configuration to use + * @return a new configured inject registry manager + */ + public static InjectRegistryManager create(InjectConfig config) { + // we provide the service, this + return new InjectRegistryManager(config, + config.discoverServices() + ? ServiceDiscovery.create() + : ServiceDiscovery.noop()); + } + + @SuppressWarnings({"unchecked"}) + @Override + public InjectRegistry registry() { + Lock readLock = lifecycleLock.readLock(); + try { + readLock.lock(); + if (registry != null) { + return registry; + } + } finally { + readLock.unlock(); + } + + Lock writeLock = lifecycleLock.writeLock(); + try { + writeLock.lock(); + if (registry != null) { + return registry; + } + // map of the service descriptor instance to Described (we need to have a single instance always used) + Map descriptorToDescribed = new IdentityHashMap<>(); + + // scope handlers (scope type to service) + Map scopeHandlers = new HashMap<>(); + // service descriptor singleton instance to its explicit value + Map explicitInstances = new IdentityHashMap<>(); + // implementation type to its manager + Map servicesByType = new HashMap<>(); + // implemented contracts to their manager(s) + Map> servicesByContract = new HashMap<>(); + // map of qualifier type to a service info that can provide instances for it + Map> qualifiedProvidersByQualifier = new HashMap<>(); + // map of a qualifier type and contract to a service info + Map> typedQualifiedProviders = + new HashMap<>(); + + List> bindings = new ArrayList<>(); + + config.serviceInstances() + .forEach((desc, instance) -> { + var descriptor = desc; + if (descriptor instanceof VirtualDescriptor) { + // maybe we have a real descriptor for this type + descriptor = virtualDescriptor(config, discovery, descriptor); + } + Described described = toDescribed(descriptorToDescribed, descriptor); + bind(bindings, + scopeHandlers, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders, + described); + explicitInstances.putIfAbsent(descriptor, instance); + }); + + for (var descriptor : config.serviceDescriptors()) { + bind(bindings, + scopeHandlers, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders, + toDescribed(descriptorToDescribed, descriptor)); + } + + boolean logUnsupported = LOGGER.isLoggable(System.Logger.Level.TRACE); + + for (var descriptorMeta : discovery.allMetadata()) { + String registryType = descriptorMeta.registryType(); + if (!(DescriptorMetadata.REGISTRY_TYPE_CORE.equals(registryType) || "inject".equals(registryType))) { + // we support only core and inject + if (logUnsupported) { + LOGGER.log(System.Logger.Level.TRACE, + "Ignoring service of type \"" + descriptorMeta.registryType() + "\": " + descriptorMeta); + } + continue; + } + + ServiceDescriptor descriptor = descriptorMeta.descriptor(); + if (contains(descriptor.contracts(), Binding.TYPE)) { + bindings.add((ServiceDescriptor) descriptor); + // applications are not bound to the registry + } else { + Described described = toDescribed(descriptorToDescribed, descriptor); + + bind(bindings, + scopeHandlers, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders, + described); + } + } + + // add service registry information (service registry cannot be overridden in any way) + InjectServiceDescriptor scopesDescriptor = Scopes__ServiceDescriptor.INSTANCE; + Described scopedDescribed = new Described(scopesDescriptor, scopesDescriptor, false); + descriptorToDescribed.put(scopesDescriptor, scopedDescribed); + + InjectServiceDescriptor registryDescriptor = InjectRegistry__ServiceDescriptor.INSTANCE; + Described registryDescribed = new Described(registryDescriptor, registryDescriptor, false); + descriptorToDescribed.put(scopesDescriptor, registryDescribed); + bind(bindings, + scopeHandlers, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders, + registryDescribed); + // add injection metadata information + InjectServiceDescriptor interceptDescriptor = InterceptionMetadata__ServiceDescriptor.INSTANCE; + Described described = new Described(interceptDescriptor, interceptDescriptor, false); + descriptorToDescribed.put(interceptDescriptor, described); + bind(bindings, + scopeHandlers, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders, + described); + + registry = new InjectServiceRegistryImpl(config, + descriptorToDescribed, + scopeHandlers, + explicitInstances, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders); + + // now check if we have an application, and if so, apply it + if (config.useBinding()) { + for (ServiceDescriptor binding : bindings) { + // applications cannot have dependencies + Binding bindingInstance = (Binding) binding.instantiate(DependencyContext.create(Map.of())); + bindingInstance.configure(new ApplicationPlanBinder(bindingInstance, registry)); + } + } + + // and if application was not bound using binding(s), we need to create the bindings now + registry.ensureInjectionPlans(); + + return registry; + } finally { + writeLock.unlock(); + } + } + + @Override + public void shutdown() { + Lock lock = lifecycleLock.writeLock(); + try { + lock.lock(); + if (registry == null) { + // registry was never requested, + return; + } + + registry.close(); + registry = null; + } finally { + lock.unlock(); + } + } + + @SuppressWarnings("rawtypes") + private static ServiceDescriptor virtualDescriptor(InjectConfig config, + ServiceDiscovery discovery, + ServiceDescriptor descriptor) { + TypeName serviceType = descriptor.serviceType(); + var fromConfig = config.serviceDescriptors() + .stream() + .filter(registered -> registered.serviceType().equals(serviceType)) + .findFirst(); + if (fromConfig.isPresent()) { + return fromConfig.get(); + } + + return discovery.allMetadata() + .stream() + .filter(handler -> contains(handler.contracts(), serviceType)) + .map(DescriptorHandler::descriptor) + .filter(desc -> desc.serviceType().equals(serviceType)) + .findFirst() + .map(it -> (ServiceDescriptor) it) + .orElse(descriptor); + } + + private Described toDescribed(Map descriptorToDescribed, + ServiceDescriptor descriptor) { + return descriptorToDescribed.computeIfAbsent(descriptor, it -> { + if (descriptor instanceof InjectServiceDescriptor injectDescriptor) { + return new Described(descriptor, injectDescriptor, false); + } else { + return new Described(descriptor, CoreWrappers.create(descriptor), true); + } + }); + } + + @SuppressWarnings("unchecked") + private void bind(List> bindings, + Map scopeHandlers, + Map servicesByType, + Map> servicesByContract, + Map> qualifiedProvidersByQualifier, + Map> typedQualifiedProviders, + Described described) { + + InjectServiceDescriptor descriptor = described.injectDescriptor(); + + if (contains(descriptor.contracts(), Binding.TYPE)) { + bindings.add((ServiceDescriptor) described.coreDescriptor()); + // application is not bound to the registry + return; + } + + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + if (descriptor.coreInfo() instanceof ServiceLoader__ServiceDescriptor sl) { + LOGGER.log(System.Logger.Level.TRACE, + "Binding service loader descriptor: " + sl + ")"); + } else { + LOGGER.log(System.Logger.Level.TRACE, "Binding service descriptor: " + descriptor.descriptorType() + .fqName()); + } + } + + // by service type + servicesByType.putIfAbsent(descriptor.serviceType(), descriptor); + // service type is a contract as well (to make lookup easier) + servicesByContract.computeIfAbsent(ResolvedType.create(descriptor.serviceType()), + it -> new TreeSet<>(SERVICE_INFO_COMPARATOR)) + .add(descriptor); + + Set contracts = descriptor.contracts(); + // by contract + for (ResolvedType contract : contracts) { + servicesByContract.computeIfAbsent(contract, + it -> new TreeSet<>(SERVICE_INFO_COMPARATOR)) + .add(descriptor); + } + + // scope handlers have a very specific meaning + if (contains(contracts, Injection.ScopeHandler.TYPE)) { + if (!Injection.Singleton.TYPE.equals(descriptor.scope())) { + throw new ServiceRegistryException("Services that provide ScopeHandler contract MUST be in Singleton scope, but " + + descriptor.serviceType().fqName() + " is in " + + descriptor.scope().fqName() + " scope."); + } + if (descriptor instanceof GeneratedInjectService.ScopeHandlerDescriptor shd) { + scopeHandlers.putIfAbsent(shd.handledScope(), descriptor); + } else { + throw new ServiceRegistryException("Service descriptors of services that implement ScopeHandler MUST" + + " implement ScopeHandlerDescriptor. Service " + + descriptor.descriptorType().fqName() + " does not."); + } + } + + if (descriptor.factoryType() == FactoryType.QUALIFIED) { + // a special kind of service that matches ANY qualifier instance of a specific type, and also may match + // a specific contract, or ANY contract + if (descriptor instanceof GeneratedInjectService.QualifiedFactoryDescriptor qpd) { + TypeName qualifierType = qpd.qualifierType(); + if (contains(contracts, TypeNames.OBJECT)) { + // matches any contract + qualifiedProvidersByQualifier.computeIfAbsent(qualifierType, it -> new TreeSet<>(SERVICE_INFO_COMPARATOR)) + .add(descriptor); + } else { + // contract specific + Set realContracts = contracts.stream() + .map(ResolvedType::type) + .filter(Predicate.not(Injection.QualifiedFactory.TYPE::equals)) + .collect(Collectors.toSet()); + for (TypeName realContract : realContracts) { + TypedQualifiedProviderKey key = new TypedQualifiedProviderKey(qualifierType, + ResolvedType.create(realContract)); + typedQualifiedProviders.computeIfAbsent(key, it -> new TreeSet<>(SERVICE_INFO_COMPARATOR)) + .add(descriptor); + } + } + } else { + throw new ServiceRegistryException("Service descriptors of services that implement QualifiedProvider MUST" + + " implement QualifiedProviderDescriptor. Service " + + descriptor.descriptorType().fqName() + " does not."); + } + } + } + + private static boolean contains(Set contracts, TypeName type) { + return contracts.stream().anyMatch(it -> it.type().equals(type)); + } + + record TypedQualifiedProviderKey(TypeName qualifier, ResolvedType contract) { + } + + /** + * @param coreDescriptor instance that is generated and used to register injection points etc. + * @param injectDescriptor instance that satisfies the inject descriptor API + * @param isCore if false, both are the same instance + */ + record Described(ServiceDescriptor coreDescriptor, + InjectServiceDescriptor injectDescriptor, + boolean isCore) { + } + + private static class ApplicationPlanBinder implements InjectionPlanBinder { + private static final System.Logger LOGGER = System.getLogger(ApplicationPlanBinder.class.getName()); + + private final Binding appInstance; + private final InjectServiceRegistryImpl registry; + + private ApplicationPlanBinder(Binding appInstance, InjectServiceRegistryImpl registry) { + this.appInstance = appInstance; + this.registry = registry; + } + + @Override + public Binder bindTo(ServiceInfo descriptor) { + ServiceManager serviceManager = registry.serviceManager(descriptor); + + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "binding injection plan to " + serviceManager); + } + + return serviceManager.servicePlanBinder(); + } + + @Override + public void interceptors(ServiceInfo... descriptors) { + registry.interceptors(descriptors); + } + + @Override + public String toString() { + return "Service binder for application: " + appInstance.name(); + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectRegistryManagerProvider.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectRegistryManagerProvider.java new file mode 100644 index 00000000000..af5abd840fd --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectRegistryManagerProvider.java @@ -0,0 +1,57 @@ +/* + * 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.util.function.Supplier; + +import io.helidon.service.registry.ServiceDiscovery; +import io.helidon.service.registry.ServiceRegistryConfig; +import io.helidon.service.registry.ServiceRegistryManager; +import io.helidon.service.registry.spi.ServiceRegistryManagerProvider; + +/** + * {@link java.util.ServiceLoader} provider implementation for + * {@link io.helidon.service.registry.spi.ServiceRegistryManagerProvider} to provide a service registry + * with injection and interception support. + */ +public class InjectRegistryManagerProvider implements ServiceRegistryManagerProvider { + /** + * Required public constructor. + * + * @deprecated required for Java {@link java.util.ServiceLoader} + */ + @Deprecated + public InjectRegistryManagerProvider() { + } + + @Override + public ServiceRegistryManager create(ServiceRegistryConfig config, + ServiceDiscovery serviceDiscovery, + Supplier coreRegistryManager) { + InjectConfig injectConfig; + if (config instanceof InjectConfig ic) { + injectConfig = ic; + } else { + injectConfig = InjectConfig.builder() + // we need to add appropriate configured options from config (if present) + .update(it -> config.config().ifPresent(it::config)) + .from(config) + .build(); + } + return new InjectRegistryManager(injectConfig, serviceDiscovery); + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectServiceRegistryImpl.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectServiceRegistryImpl.java new file mode 100644 index 00000000000..36b6cc1f2ca --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectServiceRegistryImpl.java @@ -0,0 +1,649 @@ +/* + * 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.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +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.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.common.LazyValue; +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; +import io.helidon.common.configurable.LruCache; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.service.inject.InjectRegistryManager.TypedQualifiedProviderKey; +import io.helidon.service.inject.ServiceSupplies.ServiceSupplyList; +import io.helidon.service.inject.api.ActivationRequest; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InjectRegistry__ServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.InstanceName__ServiceDescriptor; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.InterceptionMetadata__ServiceDescriptor; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.inject.api.Scope; +import io.helidon.service.inject.api.ScopeNotActiveException; +import io.helidon.service.inject.api.ScopedRegistry; +import io.helidon.service.inject.api.Scopes; +import io.helidon.service.inject.api.Scopes__ServiceDescriptor; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +import static io.helidon.service.inject.InjectRegistryManager.SERVICE_INFO_COMPARATOR; +import static io.helidon.service.inject.LookupTrace.traceLookup; + +/** + * Full-blown service registry with injection and interception support. + *

    + * This implementation re-implements even the core registry, as we want the services to be capable of interoperating + * (i.e. core services can receive inject services and vice-versa). + */ +class InjectServiceRegistryImpl implements InjectRegistry, Scopes { + private static final AtomicInteger COUNTER = new AtomicInteger(); + + private final String id = String.valueOf(COUNTER.incrementAndGet()); + + private final ReadWriteLock stateLock = new ReentrantReadWriteLock(); + private final Lock stateReadLock = stateLock.readLock(); + private final Lock stateWriteLock = stateLock.writeLock(); + + // map of scope annotation to service info + private final Map scopeHandlerServices; + // map of service implementation class to service info + private final Map servicesByType; + // map of provided contracts to service info(s) + private final Map> servicesByContract; + // map of qualifier annotations to service info(s) + private final Map> qualifiedProvidersByQualifier; + // map of qualifier annotations and resolved type combination to service info(s) + private final Map> typedQualifiedProviders; + + private final RegistryCounter lookupCounter; + private final RegistryCounter lookupScanCounter; + private final RegistryCounter cacheLookupCounter; + private final RegistryCounter cacheHitCounter; + private final boolean cacheEnabled; + private final LruCache> cache; + + private final LazyValue singletonScope = LazyValue + .create(() -> createScope(Injection.Singleton.TYPE, Optional::empty, id, Map.of())); + + private final LazyValue perLookupScope = LazyValue + .create(() -> createScope(Injection.PerLookup.TYPE, Optional::empty, id, Map.of())); + private final Map scopeHandlerInstances = new HashMap<>(); + private final Lock scopeHandlerInstancesLock = new ReentrantLock(); + private final boolean interceptionEnabled; + private final InterceptionMetadata interceptionMetadata; + + // runtime fields (to obtain actual service instances) + // service descriptor to its manager + private final Map> servicesByDescriptor = new IdentityHashMap<>(); + private final ActivationRequest activationRequest; + private Map> interceptors; + + @SuppressWarnings("unchecked") + InjectServiceRegistryImpl(InjectConfig config, + Map descriptorToDescribed, + Map scopeHandlers, + Map explicitInstances, + Map servicesByType, + Map> servicesByContract, + Map> qualifiedProvidersByQualifier, + Map> typedQualifiedProviders) { + + this.interceptionEnabled = config.interceptionEnabled(); + // this is a bit tricky - we are leaking our instance that is not yet finished, so it must + // not be accessed in the constructor! + this.interceptionMetadata = interceptionEnabled + ? InterceptionMetadataImpl.create(this) + : InterceptionMetadataImpl.noop(); + + // these must be bound here, as the instance exists now + // (and we do not want to allow post-constructor binding) + explicitInstances.put(Scopes__ServiceDescriptor.INSTANCE, this); + explicitInstances.put(InjectRegistry__ServiceDescriptor.INSTANCE, this); + explicitInstances.put(InterceptionMetadata__ServiceDescriptor.INSTANCE, interceptionMetadata); + + this.cacheEnabled = config.lookupCacheEnabled(); + this.cache = cacheEnabled ? config.lookupCache().orElseGet(LruCache::create) : null; + + // no-op counters (registry needs config, too early + this.lookupCounter = new RegistryCounter(); + this.lookupScanCounter = new RegistryCounter(); + if (cacheEnabled) { + this.cacheLookupCounter = new RegistryCounter(); + this.cacheHitCounter = new RegistryCounter(); + } else { + this.cacheLookupCounter = null; + this.cacheHitCounter = null; + } + + this.scopeHandlerServices = scopeHandlers; + this.servicesByType = servicesByType; + this.servicesByContract = servicesByContract; + this.qualifiedProvidersByQualifier = qualifiedProvidersByQualifier; + this.typedQualifiedProviders = typedQualifiedProviders; + this.activationRequest = ActivationRequest.builder() + .targetPhase(config.limitRuntimePhase()) + .build(); + + /* + For each known service descriptor, create an appropriate service manager + */ + descriptorToDescribed.forEach((descriptor, described) -> { + InjectServiceDescriptor injectDescriptor = described.injectDescriptor(); + + Object instance = explicitInstances.get(descriptor); + ServiceProvider provider = new ServiceProvider<>( + this, + (InjectServiceDescriptor) described.injectDescriptor()); + + if (instance != null) { + Activator activator = Activators.create(provider, instance); + servicesByDescriptor.put(descriptor, + new ServiceManager<>(scopeSupplier(injectDescriptor), provider, () -> activator)); + } else { + servicesByDescriptor.put(descriptor, + new ServiceManager<>(scopeSupplier(injectDescriptor), + provider, + Activators.create(this, provider))); + } + }); + + // make sure config is initialized as it should be + if (config.limitRuntimePhase().ordinal() >= Activator.Phase.ACTIVE.ordinal()) { + Config registryConfig = first(Config.class).orElseGet(GlobalConfig::config); + GlobalConfig.config(() -> registryConfig, true); + + // Set-up metrics using metric registry + MeterRegistry meterRegistry = Metrics.globalRegistry(); + this.lookupCounter.consumer = meterRegistry + .getOrCreate(Counter.builder("io.helidon.inject.lookups") + .description("Number of lookups in the service registry") + .scope(Meter.Scope.VENDOR))::increment; + this.lookupScanCounter.consumer = meterRegistry + .getOrCreate(Counter.builder("io.helidon.inject.scanLookups") + .description("Number of lookups that require registry scan") + .scope(Meter.Scope.VENDOR))::increment; + if (cacheEnabled) { + this.cacheLookupCounter.consumer = meterRegistry + .getOrCreate(Counter.builder("io.helidon.inject.cacheLookups") + .description("Number of lookups in cache in the service registry") + .scope(Meter.Scope.VENDOR))::increment; + this.cacheHitCounter.consumer = meterRegistry + .getOrCreate(Counter.builder("io.helidon.inject.cacheHits") + .description("Number of cache hits in the service registry") + .scope(Meter.Scope.VENDOR))::increment; + } + } + } + + @SuppressWarnings("unchecked") + @Override + public Optional get(ServiceInfo serviceInfo) { + ServiceManager serviceManager = servicesByDescriptor.get(serviceInfo); + if (serviceManager == null) { + return Optional.empty(); + } + return (Optional) new ServiceSupplies.ServiceSupplyOptional<>(Lookup.EMPTY, List.of(serviceManager(serviceInfo))) + .get(); + } + + @Override + public List allServices(TypeName contract) { + return lookupServices(Lookup.create(contract)) + .stream() + .map(InjectServiceInfo::coreInfo) + .collect(Collectors.toList()); + } + + @Override + public T get(Lookup lookup) { + return this.supply(lookup).get(); + } + + @Override + public Optional first(Lookup lookup) { + return this.supplyFirst(lookup).get(); + } + + @Override + public List all(Lookup lookup) { + return this.supplyAll(lookup).get(); + } + + @Override + public Supplier supply(Lookup lookup) { + List> managers = lookupManagers(lookup); + + if (managers.isEmpty()) { + throw new ServiceRegistryException("There is no service in registry that matches this lookup: " + lookup); + } + return new ServiceSupplies.ServiceSupply<>(lookup, managers); + } + + @Override + public Supplier> supplyFirst(Lookup lookup) { + List> managers = lookupManagers(lookup); + + if (managers.isEmpty()) { + return Optional::empty; + } + return new ServiceSupplies.ServiceSupplyOptional<>(lookup, managers); + } + + @Override + public Supplier> supplyAll(Lookup lookup) { + List> managers = lookupManagers(lookup); + + if (managers.isEmpty()) { + return List::of; + } + return new ServiceSupplyList<>(lookup, managers); + } + + @Override + public T get(TypeName contract) { + return get(Lookup.create(contract)); + } + + @Override + public Optional first(TypeName contract) { + return first(Lookup.create(contract)); + } + + @Override + public List all(TypeName contract) { + return all(Lookup.create(contract)); + } + + @Override + public Supplier supply(TypeName contract) { + return supply(Lookup.create(contract)); + } + + @Override + public Supplier> supplyFirst(TypeName contract) { + return supplyFirst(Lookup.create(contract)); + } + + @Override + public Supplier> supplyAll(TypeName contract) { + return supplyAll(Lookup.create(contract)); + } + + @Override + public Scope createScope(TypeName scopeType, String id, Map, Object> initialBindings) { + return createScope(scopeType, + scopeHandler(scopeType), + id, + initialBindings); + } + + @Override + public List lookupServices(Lookup lookup) { + try { + stateReadLock.lock(); + // a very special lookup + if (lookup.qualifiers().contains(Qualifier.CREATE_FOR_NAME)) { + if (lookup.qualifiers().size() != 1) { + throw new ServiceRegistryException("Invalid injection lookup. @" + + Injection.InstanceName.class.getName() + + " must be the only qualifier used."); + } + if (!lookup.contracts().contains(ResolvedType.create(TypeNames.STRING))) { + throw new ServiceRegistryException("Invalid injection lookup. @" + + Injection.InstanceName.class.getName() + + " must use String contract."); + } + if (lookup.contracts().size() != 1) { + throw new ServiceRegistryException("Invalid injection lookup. @" + + Injection.InstanceName.class.getName() + + " must use String as the only contract."); + } + return List.of(InstanceName__ServiceDescriptor.INSTANCE); + } + + lookupCounter.increment(); + + traceLookup(lookup, "start: {0}", lookup); + + if (cacheEnabled) { + List cacheResult = cache.get(lookup) + .orElse(null); + cacheLookupCounter.increment(); + if (cacheResult != null) { + traceLookup(lookup, "from cache", cacheResult); + cacheHitCounter.increment(); + return cacheResult; + } + } + + List result = new ArrayList<>(); + + if (lookup.serviceType().isPresent()) { + // when a specific service type is requested, we go for it + InjectServiceInfo exact = servicesByType.get(lookup.serviceType().get()); + if (exact != null) { + traceLookup(lookup, "by service type", result); + result.add(exact); + return result; + } + } + + if (1 == lookup.contracts().size()) { + // a single contract is requested, we are ready for this ("indexed by contract") + ResolvedType theOnlyContractRequested = lookup.contracts().iterator().next(); + Set subsetOfMatches = servicesByContract.get(theOnlyContractRequested); + if (subsetOfMatches != null) { + // the subset is ordered, cannot use parallel, also no need to re-order + subsetOfMatches.stream() + .filter(lookup::matches) + .forEach(result::add); + if (!result.isEmpty()) { + traceLookup(lookup, "by single contract", result); + return result; + } + } + } + + // table scan :-( + lookupScanCounter.increment(); + servicesByType.values() + .stream() + .filter(lookup::matches) + .sorted(SERVICE_INFO_COMPARATOR) + .forEach(result::add); + traceLookup(lookup, "from full table scan", result); + + if (result.isEmpty() && !lookup.qualifiers().isEmpty()) { + // check qualified providers + if (lookup.contracts().size() == 1) { + ResolvedType contract = lookup.contracts().iterator().next(); + for (Qualifier qualifier : lookup.qualifiers()) { + TypeName qualifierType = qualifier.typeName(); + Set found = typedQualifiedProviders.get(new TypedQualifiedProviderKey(qualifierType, + contract)); + if (found != null) { + traceLookup(lookup, "from typed qualified providers", found); + result.addAll(found); + } + found = qualifiedProvidersByQualifier.get(qualifierType); + if (found != null) { + traceLookup(lookup, "from typed qualified providers", found); + result.addAll(found); + } + } + } + } + + if (cacheEnabled) { + cache.put(lookup, result); + } + + traceLookup(lookup, "full result", result); + return result; + } finally { + stateReadLock.unlock(); + } + } + + void ensureInjectionPlans() { + servicesByDescriptor.values() + .forEach(ServiceManager::ensureInjectionPlan); + } + + void close() { + singletonScope.get() + .close(); + } + + List servicesByContract(ResolvedType contract) { + Set serviceInfos = servicesByContract.get(contract); + if (serviceInfos == null) { + return List.of(); + } + return serviceInfos.stream() + .map(InjectServiceInfo::coreInfo) + .collect(Collectors.toList()); + } + + List> lookupManagers(Lookup lookup) { + List> result = new ArrayList<>(); + + for (InjectServiceInfo service : lookupServices(lookup)) { + result.add(serviceManager(service.coreInfo())); + } + + return result; + } + + InterceptionMetadata interceptionMetadata() { + return interceptionMetadata; + } + + ActivationRequest activationRequest() { + return activationRequest; + } + + @SuppressWarnings("unchecked") + ServiceManager serviceManager(ServiceInfo info) { + ServiceManager result = (ServiceManager) servicesByDescriptor.get(info); + if (result == null) { + throw new ServiceRegistryException("Attempt to use service info not managed by this registry: " + info); + } + return result; + } + + void interceptors(ServiceInfo... serviceInfos) { + if (!interceptionEnabled) { + return; + } + try { + stateWriteLock.lock(); + if (this.interceptors == null) { + this.interceptors = new LinkedHashMap<>(); + } + Set ordered = new TreeSet<>(SERVICE_INFO_COMPARATOR); + for (ServiceInfo serviceInfo : serviceInfos) { + ServiceManager serviceManager = this.serviceManager(serviceInfo); + ordered.add(serviceManager.injectDescriptor()); + } + + // there may be more than one application, we need to add to existing + for (InjectServiceInfo injectServiceInfo : ordered) { + this.interceptors.computeIfAbsent(injectServiceInfo.coreInfo(), + this::serviceManager); + } + } finally { + stateWriteLock.unlock(); + } + } + + List> interceptors() { + try { + stateReadLock.lock(); + if (interceptors != null) { + return List.copyOf(interceptors.values()); + } + } finally { + stateReadLock.unlock(); + } + try { + stateWriteLock.lock(); + if (interceptors == null) { + // we must preserve the order of services, as they are weight ordered! + this.interceptors = new LinkedHashMap<>(); + List> serviceManagers = + lookupManagers(Lookup.builder() + .addContract(Interception.Interceptor.class) + .addQualifier(Qualifier.WILDCARD_NAMED) + .build()); + for (ServiceManager serviceManager : serviceManagers) { + this.interceptors.put(serviceManager.descriptor(), serviceManager); + } + } + return List.copyOf(interceptors.values()); + } finally { + stateWriteLock.unlock(); + } + } + + private Scope createScope(TypeName scopeType, + Injection.ScopeHandler scopeHandler, + String id, + Map, Object> initialBindings) { + var registry = new ScopedRegistryImpl(this, scopeType, id, initialBindings); + var scope = new ScopeImpl(scopeType, scopeHandler, registry); + scopeHandler.activate(scope); + return scope; + } + + private Supplier scopeSupplier(InjectServiceInfo descriptor) { + TypeName scope = descriptor.scope(); + if (Injection.Singleton.TYPE.equals(scope)) { + return singletonScope; + } else if (Injection.PerLookup.TYPE.equals(scope)) { + return perLookupScope; + } else { + // must be a lazy value, as the scope handler may not be available at the time this method is called + LazyValue scopeHandler = LazyValue.create(() -> scopeHandler(scope)); + return () -> scopeHandler.get() + .currentScope() // must be called each time, as we must use the currently active scope, not a cached one + .orElseThrow(() -> new ScopeNotActiveException("Scope not active for service: " + + descriptor.serviceType().fqName(), + scope)); + } + } + + private Injection.ScopeHandler scopeHandler(TypeName scope) { + scopeHandlerInstancesLock.lock(); + try { + return scopeHandlerInstances.computeIfAbsent(scope, it -> { + InjectServiceInfo serviceInfo = scopeHandlerServices.get(scope); + if (serviceInfo == null) { + throw new ServiceRegistryException("There is no scope handler service registered for scope: " + + scope.fqName()); + } + ServiceManager serviceManager = servicesByDescriptor.get(serviceInfo.coreInfo()); + return (Injection.ScopeHandler) serviceManager.activator() + .instances(Lookup.EMPTY) + .orElseThrow(() -> new ServiceRegistryException("Scope handler service did not return any instance for: " + + scope.fqName())) + .getFirst() // List.getFirst() - Qualified instance + .get(); + }); + } finally { + scopeHandlerInstancesLock.unlock(); + } + } + + private static class RegistryCounter implements Counter { + private volatile Consumer consumer; + + RegistryCounter() { + consumer = it -> { + }; + } + + @Override + public void increment() { + increment(1); + } + + @Override + public void increment(long amount) { + consumer.accept(amount); + } + + @Override + public long count() { + throw new UnsupportedOperationException("This is not a full counter"); + } + + @Override + public Id id() { + throw new UnsupportedOperationException("This is not a full counter"); + } + + @Override + public Optional baseUnit() { + throw new UnsupportedOperationException("This is not a full counter"); + } + + @Override + public Optional description() { + throw new UnsupportedOperationException("This is not a full counter"); + } + + @Override + public Type type() { + throw new UnsupportedOperationException("This is not a full counter"); + } + + @Override + public Optional scope() { + throw new UnsupportedOperationException("This is not a full counter"); + } + + @Override + public R unwrap(Class c) { + throw new UnsupportedOperationException("This is not a full counter"); + } + } + + private record ScopeImpl(TypeName scopeType, + Injection.ScopeHandler handler, + ScopedRegistry registry) implements Scope { + + @Override + public void close() { + handler.deactivate(this); + } + + @Override + public String toString() { + return "Scope for " + scopeType.fqName(); + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionContext.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionContext.java new file mode 100644 index 00000000000..6424d128bd3 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionContext.java @@ -0,0 +1,48 @@ +/* + * 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.util.Map; +import java.util.NoSuchElementException; + +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.DependencyContext; + +class InjectionContext implements DependencyContext { + private final Map> injectionPlan; + + InjectionContext(Map> injectionPlan) { + this.injectionPlan = injectionPlan; + } + + static DependencyContext create(Map> injectionPlan) { + return new InjectionContext(injectionPlan); + } + + @SuppressWarnings("unchecked") + @Override + public T dependency(Dependency dependency) { + IpPlan ipPlan = injectionPlan.get(dependency); + if (ipPlan == null) { + throw new NoSuchElementException("Cannot resolve injection id " + dependency + " for service " + + dependency.service().fqName() + + ", this dependency was not declared in " + + "the service descriptor"); + } + return (T) ipPlan.get(); + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionPlanBinder.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionPlanBinder.java new file mode 100644 index 00000000000..47537a53783 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InjectionPlanBinder.java @@ -0,0 +1,185 @@ +/* + * 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; + +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.ServiceInfo; + +/** + * Responsible for registering the injection plan to the services in the service registry. + *

    + * IMPORTANT: all methods must be called with {@link io.helidon.service.registry.ServiceDescriptor} singleton + * instances for {@link io.helidon.service.registry.ServiceInfo} parameter, as the registry depends on instance + * equality. All generated code is done this way. + */ +public interface InjectionPlanBinder { + + /** + * Bind an injection plan to a service provider instance. + * + * @param descriptor the service to receive the injection plan. + * @return the binder to use for binding the injection plan to the service provider + */ + Binder bindTo(io.helidon.service.registry.ServiceInfo descriptor); + + /** + * Bind all discovered interceptors. + * + * @param descriptors interceptor services + */ + void interceptors(io.helidon.service.registry.ServiceInfo... descriptors); + + /** + * The binder builder for the service plan. + * The caller must be aware of cardinality and type (whether to inject {@link java.util.function.Supplier} or instance) + * of injections. + * + * @see io.helidon.service.inject.api.Ip + */ + interface Binder { + + /** + * Binds a single service to the injection point identified by the id. + * The injection point expects a single service instance. + * + * @param dependency the injection point identity + * @param descriptor the service descriptor to bind to this identity + * @return the binder builder + */ + Binder bind(Dependency dependency, + ServiceInfo descriptor); + + /** + * Bind to an optional field, with zero or one services. + * The injection point expects an {@link java.util.Optional} of service instance. + * + * @param dependency injection point identity + * @param descriptors the service descriptor to bind (zero or one) + * @return the binder builder + */ + Binder bindOptional(Dependency dependency, + ServiceInfo... descriptors); + + /** + * Binds to a list field, with zero or more services. + * The injection point expects a {@link java.util.List} of service instances. + * + * @param dependency the injection point identity + * @param descriptors service descriptors to bind to this identity (zero or more) + * @return the binder builder + */ + Binder bindList(Dependency dependency, + ServiceInfo... descriptors); + + /** + * Binds to a supplier field. + * The injection point expects a {@link java.util.function.Supplier} of service. + * + * @param dependency the injection point identity + * @param descriptor the service descriptor to bind to this identity. + * @return the binder builder + */ + Binder bindSupplier(Dependency dependency, + ServiceInfo descriptor); + + /** + * Bind to a supplier of optional field. + * The injection point expects a {@link java.util.function.Supplier} of {@link java.util.Optional} of service. + * + * @param dependency injection point identity + * @param descriptor the service descriptor to bind (zero or one) + * @return the binder builder + */ + Binder bindSupplierOfOptional(Dependency dependency, + ServiceInfo... descriptor); + + /** + * Bind to an optional supplier field. + * The injection point expects a {@link java.util.function.Supplier} of {@link java.util.Optional} of service. + * + * @param dependency injection point identity + * @param descriptor the service descriptor to bind (zero or one) + * @return the binder builder + */ + Binder bindOptionalOfSupplier(Dependency dependency, + ServiceInfo... descriptor); + + /** + * Bind to a supplier of list. + * The injection point expects a {@link java.util.function.Supplier} of {@link java.util.List} of services. + * + * @param dependency the injection point identity + * @param descriptors service descriptor to bind to this identity (zero or more) + * @return the binder builder + */ + Binder bindSupplierOfList(Dependency dependency, + ServiceInfo... descriptors); + + /** + * Bind to a list of suppliers. + * The injection point expects a {@link java.util.List} of {@link java.util.function.Supplier Suppliers} of service. + * + * @param dependency the injection point identity + * @param descriptors service descriptor to bind to this identity (zero or more) + * @return the binder builder + */ + Binder bindListOfSuppliers(Dependency dependency, + ServiceInfo... descriptors); + + /** + * Represents a null bind. + * + * @param dependency the injection point identity + * @return the binder builder + */ + Binder bindNull(Dependency dependency); + + /** + * Bind service instance. + * + * @param dependency the injection point identity + * @param descriptor the service descriptor to bind + * @return the binder builder + */ + Binder bindServiceInstance(Dependency dependency, ServiceInfo descriptor); + + /** + * Bind to a list of service instances. + * + * @param dependency the injection point identity + * @param descriptors the service descriptors to bind (zero or more) + * @return the binder builder + */ + Binder bindServiceInstanceList(Dependency dependency, ServiceInfo... descriptors); + + /** + * Bind to an optional of service instance. + * + * @param dependency the injection point identity + * @param descriptor the service descriptor to bind (zero or one) + * @return the binder builder + */ + Binder bindOptionalOfServiceInstance(Dependency dependency, ServiceInfo... descriptor); + + /** + * Commits the bindings for this service provider. + */ + void commit(); + + } + +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/InterceptionMetadataImpl.java b/service/inject/inject/src/main/java/io/helidon/service/inject/InterceptionMetadataImpl.java new file mode 100644 index 00000000000..a6daff7e5d4 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/InterceptionMetadataImpl.java @@ -0,0 +1,180 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.service.inject.ServiceSupplies.ServiceSupply; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; +import io.helidon.service.inject.api.InterceptionInvoker; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; + +class InterceptionMetadataImpl implements InterceptionMetadata { + private final InjectServiceRegistryImpl registry; + + private InterceptionMetadataImpl(InjectServiceRegistryImpl registry) { + this.registry = registry; + } + + static InterceptionMetadata create(InjectServiceRegistryImpl registry) { + return new InterceptionMetadataImpl(registry); + } + + static InterceptionMetadata noop() { + return new NoopMetadata(); + } + + @Override + public InterceptionInvoker createInvoker(InjectServiceInfo descriptor, + Set typeQualifiers, + List typeAnnotations, + TypedElementInfo element, + InterceptionInvoker targetInvoker, + Set> checkedExceptions) { + Objects.requireNonNull(descriptor); + Objects.requireNonNull(typeQualifiers); + Objects.requireNonNull(typeAnnotations); + Objects.requireNonNull(element); + Objects.requireNonNull(targetInvoker); + Objects.requireNonNull(checkedExceptions); + + var interceptors = interceptors(typeAnnotations, element); + if (interceptors.isEmpty()) { + return targetInvoker; + } else { + return params -> Invocation.invokeInterception(InterceptionContext.builder() + .serviceInfo(descriptor) + .typeAnnotations(typeAnnotations) + .elementInfo(element) + .build(), + interceptors, + targetInvoker, + params, + checkedExceptions); + } + } + + @Override + public InterceptionInvoker createInvoker(Object serviceInstance, + InjectServiceInfo descriptor, + Set typeQualifiers, + List typeAnnotations, + TypedElementInfo element, + InterceptionInvoker targetInvoker, + Set> checkedExceptions) { + Objects.requireNonNull(serviceInstance); + Objects.requireNonNull(descriptor); + Objects.requireNonNull(typeQualifiers); + Objects.requireNonNull(typeAnnotations); + Objects.requireNonNull(element); + Objects.requireNonNull(targetInvoker); + Objects.requireNonNull(checkedExceptions); + + var interceptors = interceptors(typeAnnotations, element); + if (interceptors.isEmpty()) { + return targetInvoker; + } else { + return params -> Invocation.invokeInterception(InterceptionContext.builder() + .serviceInstance(serviceInstance) + .serviceInfo(descriptor) + .typeAnnotations(typeAnnotations) + .elementInfo(element) + .build(), + interceptors, + targetInvoker, + params, + checkedExceptions); + } + } + + private List> interceptors(List typeAnnotations, + TypedElementInfo element) { + // need to find all interceptors for the providers (ordered by weight) + List> allInterceptors = registry.interceptors(); + + List> result = new ArrayList<>(); + + for (ServiceManager interceptor : allInterceptors) { + if (applicable(typeAnnotations, interceptor.injectDescriptor())) { + result.add(new ServiceSupply<>(Lookup.EMPTY, List.of(interceptor))); + continue; + } + if (applicable(element.annotations(), interceptor.injectDescriptor())) { + result.add(new ServiceSupply<>(Lookup.EMPTY, List.of(interceptor))); + } + } + + return result; + } + + private boolean applicable(List typeAnnotations, InjectServiceInfo serviceInfo) { + for (Annotation typeAnnotation : typeAnnotations) { + if (serviceInfo.qualifiers().contains(Qualifier.createNamed(typeAnnotation.typeName().fqName()))) { + return true; + } + } + return false; + } + + private static class NoopMetadata implements InterceptionMetadata { + @Override + public InterceptionInvoker createInvoker(InjectServiceInfo descriptor, + Set typeQualifiers, + List typeAnnotations, + TypedElementInfo element, + InterceptionInvoker targetInvoker, + Set> checkedExceptions) { + Objects.requireNonNull(descriptor); + Objects.requireNonNull(typeQualifiers); + Objects.requireNonNull(typeAnnotations); + Objects.requireNonNull(element); + Objects.requireNonNull(targetInvoker); + Objects.requireNonNull(checkedExceptions); + + return targetInvoker; + } + + @Override + public InterceptionInvoker createInvoker(Object serviceInstance, + InjectServiceInfo descriptor, + Set typeQualifiers, + List typeAnnotations, + TypedElementInfo element, + InterceptionInvoker targetInvoker, + Set> checkedExceptions) { + Objects.requireNonNull(serviceInstance); + Objects.requireNonNull(descriptor); + Objects.requireNonNull(typeQualifiers); + Objects.requireNonNull(typeAnnotations); + Objects.requireNonNull(element); + Objects.requireNonNull(targetInvoker); + Objects.requireNonNull(checkedExceptions); + + return targetInvoker; + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/Invocation.java b/service/inject/inject/src/main/java/io/helidon/service/inject/Invocation.java new file mode 100644 index 00000000000..63add61c4cf --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/Invocation.java @@ -0,0 +1,163 @@ +/* + * 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.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; +import io.helidon.service.inject.api.InterceptionException; +import io.helidon.service.inject.api.InterceptionInvoker; + +/** + * Handles the invocation of {@link io.helidon.service.inject.api.Interception.Interceptor} methods. + * Note that upon a successful call to the + * {@link io.helidon.service.inject.api.Interception.Interceptor.Chain#proceed(Object[])} + * or to the ultimate target, the invocation will be prevented from being executed again. + * + * @param the invocation type + * @see io.helidon.service.inject.api.InterceptionContext + */ +class Invocation implements Interception.Interceptor.Chain { + private final InterceptionContext ctx; + private final List> interceptors; + private final Set> checkedExceptions; + private int interceptorPos; + private InterceptionInvoker call; + + private Invocation(InterceptionContext ctx, + List> interceptors, + InterceptionInvoker call, + Set> checkedExceptions) { + this.ctx = ctx; + this.call = call; + this.interceptors = List.copyOf(interceptors); + this.checkedExceptions = checkedExceptions; + } + + /** + * Creates an instance of {@link io.helidon.service.inject.Invocation} and invokes it in this context. + * + * @param ctx the invocation context + * @param call the call to the base service provider's method + * @param args the call arguments + * @param checkedExceptions expected exception types + * @param the type returned from the method element + * @return the invocation instance + * @throws io.helidon.service.inject.api.InterceptionException if there are errors during invocation chain processing + * @throws Exception any checked exception declared by the method itself + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + static V invokeInterception(InterceptionContext ctx, + List> interceptors, + InterceptionInvoker call, + Object[] args, + Set> checkedExceptions) throws Exception { + Objects.requireNonNull(ctx); + Objects.requireNonNull(call); + Objects.requireNonNull(args); + Objects.requireNonNull(checkedExceptions); + + if (interceptors.isEmpty()) { + try { + return call.invoke(args); + } catch (Throwable t) { + if (shouldThrow(checkedExceptions, t.getClass())) { + throw t; + } + throw new InterceptionException("Error in interceptor chain processing", t, true); + } + } else { + return (V) new Invocation(ctx, interceptors, call, checkedExceptions).proceed(args); + } + } + + @Override + public String toString() { + return String.valueOf(ctx.elementInfo()); + } + + @Override + public V proceed(Object... args) throws Exception { + if (this.call == null) { + throw new InterceptionException("Duplicate invocation, or unknown call type: " + this, true); + } + + if (interceptorPos < interceptors.size()) { + Supplier interceptorProvider = interceptors.get(interceptorPos); + Interception.Interceptor interceptor = interceptorProvider.get(); + interceptorPos++; + try { + return interceptor.proceed(ctx, this, args); + } catch (RuntimeException e) { + interceptorPos--; + throw e; + } catch (Throwable t) { + interceptorPos--; + + if (shouldThrow(checkedExceptions, t.getClass())) { + throw t; + } + + String message = "Error in interceptor chain processing"; + boolean called = call == null; + + throw new InterceptionException(message, t, called); + } + } + + InterceptionInvoker call = this.call; + this.call = null; + + try { + return call.invoke(args); + } catch (InterceptionException e) { + if (e.targetWasCalled()) { + // allow the call to happen again + this.call = call; + } + throw e; + } catch (RuntimeException e) { + this.call = call; + throw e; + } catch (Throwable t) { + // allow the call to happen again + this.call = call; + if (shouldThrow(checkedExceptions, t.getClass())) { + // do not wrap, declared checked exception + throw t; + } + // wrap, unexpected exception/throwable + throw new InterceptionException("Error in interceptor chain processing", t, true); + } + } + + private static boolean shouldThrow(Set> checked, Class t) { + if (checked.contains(t)) { + return true; + } + for (Class aClass : checked) { + if (aClass.isAssignableFrom(t)) { + return true; + } + } + return false; + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/IpPlan.java b/service/inject/inject/src/main/java/io/helidon/service/inject/IpPlan.java new file mode 100644 index 00000000000..64a6e436eb1 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/IpPlan.java @@ -0,0 +1,36 @@ +/* + * 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.util.function.Supplier; + +import io.helidon.service.registry.ServiceInfo; + +/** + * Injection point plan of injection. + * + * @param valueSupplier supplier of the value + * @param descriptors descriptor(s) used to obtain the value(s) in the supplier + * @param type of the value + */ +record IpPlan(Supplier valueSupplier, + ServiceInfo... descriptors) implements Supplier { + @Override + public T get() { + return valueSupplier.get(); + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/LookupTrace.java b/service/inject/inject/src/main/java/io/helidon/service/inject/LookupTrace.java new file mode 100644 index 00000000000..89499f92822 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/LookupTrace.java @@ -0,0 +1,110 @@ +/* + * 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.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.inject.api.ServiceInstance; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.TRACE; + +final class LookupTrace { + private static final System.Logger LOGGER = System.getLogger(LookupTrace.class.getName()); + + private LookupTrace() { + } + + static void traceLookup(Lookup lookup, String message, Object... args) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, prefix(lookup) + message, args); + } + } + + static void traceLookupInstance(Lookup lookup, + ServiceManager manager, + List> instances) { + if (LOGGER.isLoggable(TRACE)) { + String serviceType = manager.descriptor().serviceType().fqName(); + if (instances.isEmpty()) { + LOGGER.log(TRACE, prefix(lookup) + "service {0} added 0 instances", + serviceType); + return; + } + + for (ServiceInstance instance : instances) { + LOGGER.log(TRACE, prefix(lookup) + "service {0} adding instance: {1}", + serviceType, + instanceInfo(instance)); + } + } + } + + static void traceLookupInstances(Lookup lookup, + List> instances) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, prefix(lookup) + "sorted instances by weight and service:{0}", + instances.stream() + .map(it -> it.serviceType().fqName() + " [" + it.weight() + "]") + .collect(Collectors.joining(", "))); + } + } + + static void traceLookup(Lookup lookup, String message, List services) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "{0}matching service providers {1}: {2}", + prefix(lookup), + message, + services.stream() + .map(InjectServiceInfo::serviceType) + .map(TypeName::fqName) + .collect(Collectors.toUnmodifiableList())); + } + } + + private static String instanceInfo(ServiceInstance instance) { + double weight = instance.weight(); + TypeName scope = instance.scope(); + Set qualifiers = instance.qualifiers(); + Object o = instance.get(); + + return "weight(" + weight + "), " + + "scope(" + scope(scope) + "), " + + "qualifiers(" + qualifiers.stream() + .map(Qualifier::typeName) + .map(TypeName::fqName) + .collect(Collectors.joining(", ")) + "), " + + "instance(" + o + ")"; + } + + private static String scope(TypeName typeName) { + if (typeName.packageName().startsWith("io.helidon.service.inject")) { + return typeName.classNameWithEnclosingNames(); + } + return typeName.fqName(); + } + + private static String prefix(Lookup lookup) { + return "[" + System.identityHashCode(lookup) + "] "; + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/PerRequestScopeHandler.java b/service/inject/inject/src/main/java/io/helidon/service/inject/PerRequestScopeHandler.java new file mode 100644 index 00000000000..0cce53fd9ae --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/PerRequestScopeHandler.java @@ -0,0 +1,64 @@ +/* + * 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.util.Optional; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Scope; + +@Injection.Singleton +@Injection.NamedByType(Injection.PerRequest.class) +class PerRequestScopeHandler implements Injection.ScopeHandler { + private static final ThreadLocal REQUEST_SCOPES = new ThreadLocal<>(); + + @Override + public Optional currentScope() { + return Optional.ofNullable(REQUEST_SCOPES.get()).map(ScopeInfo::scope); + } + + @Override + public void activate(Scope scope) { + ScopeInfo currentScope = REQUEST_SCOPES.get(); + if (currentScope != null) { + throw new IllegalStateException("Attempt to re-create request scope. Already exists for this request: " + + currentScope.scope); + } + REQUEST_SCOPES.set(new ScopeInfo(scope, Thread.currentThread())); + scope.registry().activate(); + } + + @Override + public void deactivate(Scope scope) { + ScopeInfo currentScope = REQUEST_SCOPES.get(); + if (currentScope == null) { + throw new IllegalStateException("Current scope already de-activated: " + scope); + } + if (currentScope.scope != scope) { + throw new IllegalStateException("Memory leak! Attempting to close request scope in a different thread." + + " Expected scope: " + scope + + ", thread scope: " + currentScope + + ", thread that started the scope: " + currentScope.thread + + ", current thread: " + Thread.currentThread()); + } + REQUEST_SCOPES.remove(); + scope.registry().deactivate(); + } + + private record ScopeInfo(Scope scope, Thread thread) { + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ScopedRegistryImpl.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ScopedRegistryImpl.java new file mode 100644 index 00000000000..dae5cbab816 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ScopedRegistryImpl.java @@ -0,0 +1,176 @@ +/* + * 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.ArrayList; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.ActivationResult; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.ScopeNotActiveException; +import io.helidon.service.inject.api.ScopedRegistry; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +/** + * Services for a specific scope. + * This type is owned by Helidon Injection, and cannot be customized. + * When a scope is properly accessible through its {@link io.helidon.service.inject.api.Injection.ScopeHandler}, + * {@link #activate()} + * must be invoked by its control, to make sure all eager services are correctly activated. + */ +class ScopedRegistryImpl implements ScopedRegistry { + private static final System.Logger LOGGER = System.getLogger(ScopedRegistryImpl.class.getName()); + + private final ReadWriteLock serviceProvidersLock = new ReentrantReadWriteLock(); + private final Map> activators = new IdentityHashMap<>(); + + private final TypeName scope; + private final String id; + private boolean active = false; + + @SuppressWarnings({"rawtypes", "unchecked"}) + ScopedRegistryImpl(InjectServiceRegistryImpl registry, + TypeName scope, + String id, + Map, Object> initialBindings) { + this.scope = scope; + this.id = id; + + for (Map.Entry, Object> entry : initialBindings.entrySet()) { + InjectServiceDescriptor key = CoreWrappers.create(entry.getKey()); + ServiceProvider provider = new ServiceProvider<>(registry, + key + ); + Object value = entry.getValue(); + Activator fixedService; + + fixedService = Activators.create(provider, value); + + activators.put(key, fixedService); + } + } + + /** + * Activate this scope This method must be called just once, + * at the time the scope is active and instances can be created within it. + */ + public void activate() { + active = true; + } + + @Override + public void deactivate() { + try { + serviceProvidersLock.writeLock().lock(); + if (!active) { + return; + } + + List> toShutdown = activators.values() + .stream() + .filter(it -> it.phase().eligibleForDeactivation()) + .sorted(shutdownComparator()) + .toList(); + + List exceptions = new ArrayList<>(); + + for (Activator managedService : toShutdown) { + try { + ActivationResult activationResult = managedService.deactivate(); + if (activationResult.failure() && LOGGER.isLoggable(Level.DEBUG)) { + if (activationResult.error().isPresent()) { + LOGGER.log(Level.DEBUG, + "[" + id + "] Failed to deactivate " + managedService.description(), + activationResult.error().get()); + exceptions.add(activationResult.error().get()); + } else { + LOGGER.log(Level.DEBUG, + "[" + id + "] Failed to deactivate " + managedService.description()); + exceptions.add(new ServiceRegistryException("Failed to deactivate " + managedService.description() + + ", no exception received.")); + } + } + } catch (Exception e) { + if (LOGGER.isLoggable(Level.DEBUG)) { + LOGGER.log(Level.DEBUG, "[" + id + "] Failed to deactivate service provider: " + managedService, e); + } + exceptions.add(new ServiceRegistryException("Failed to deactivate " + managedService.description(), e)); + } + } + + active = false; + + if (exceptions.isEmpty()) { + return; + } + ServiceRegistryException failure = new ServiceRegistryException("Deactivation failed"); + exceptions.forEach(failure::addSuppressed); + throw failure; + } finally { + serviceProvidersLock.writeLock().unlock(); + } + } + + @SuppressWarnings("unchecked") + @Override + public Activator activator(ServiceInfo descriptor, Supplier> activatorSupplier) { + try { + serviceProvidersLock.readLock().lock(); + checkActive(); + Activator activator = activators.get(descriptor); + if (activator != null) { + return (Activator) activator; + } + } finally { + serviceProvidersLock.readLock().unlock(); + } + + // failed to get instance, now let's obtain a write lock and do it again + try { + serviceProvidersLock.writeLock().lock(); + checkActive(); + return (Activator) activators.computeIfAbsent(descriptor, + desc -> activatorSupplier.get()); + } finally { + serviceProvidersLock.writeLock().unlock(); + } + } + + private static Comparator> shutdownComparator() { + return Comparator + .>comparingDouble(it -> it.descriptor().runLevel().orElse(Injection.RunLevel.NORMAL)) + .thenComparing(it -> it.descriptor().weight()); + } + + private void checkActive() { + if (!active) { + throw new ScopeNotActiveException("Injection scope " + scope.fqName() + "[" + id + "] is not active.", scope); + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceManager.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceManager.java new file mode 100644 index 00000000000..eeb136eae64 --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceManager.java @@ -0,0 +1,137 @@ +/* + * 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.util.Set; +import java.util.function.Supplier; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.inject.api.Scope; +import io.helidon.service.inject.api.ServiceInstance; +import io.helidon.service.registry.ServiceInfo; + +/* +Manager of a single service. There is one instance per service provider (and per service descriptor). + */ +class ServiceManager { + private final ServiceProvider provider; + private final Supplier> activatorSupplier; + private final Supplier scopeSupplier; + + ServiceManager(Supplier scopeSupplier, + ServiceProvider provider, + Supplier> activatorSupplier) { + this.scopeSupplier = scopeSupplier; + this.provider = provider; + this.activatorSupplier = activatorSupplier; + } + + @Override + public String toString() { + return provider.descriptor().serviceType().classNameWithEnclosingNames(); + } + + void ensureInjectionPlan() { + provider.injectionPlan(); + } + + ServiceInstance registryInstance(Lookup lookup, QualifiedInstance instance) { + return new ServiceInstanceImpl<>(provider.descriptor(), + provider.contracts(lookup), + instance); + } + + InjectionPlanBinder.Binder servicePlanBinder() { + return provider.servicePlanBinder(); + } + + ServiceInfo descriptor() { + return provider.descriptor().coreInfo(); + } + + InjectServiceInfo injectDescriptor() { + return provider.descriptor(); + } + + /* + Get service activator for the scope it is in (always works for singleton, may fail for other) + this provides an instance of an activator that is bound to a scope instance + */ + Activator activator() { + return scopeSupplier + .get() + .registry() + .activator(provider.descriptor().coreInfo(), + activatorSupplier); + } + + private static final class ServiceInstanceImpl implements ServiceInstance { + private final InjectServiceDescriptor descriptor; + private final QualifiedInstance qualifiedInstance; + private final Set contracts; + + private ServiceInstanceImpl(InjectServiceDescriptor descriptor, + Set contracts, + QualifiedInstance qualifiedInstance) { + this.descriptor = descriptor; + this.contracts = contracts; + this.qualifiedInstance = qualifiedInstance; + } + + @Override + public T get() { + return qualifiedInstance.get(); + } + + @Override + public Set qualifiers() { + return qualifiedInstance.qualifiers(); + } + + @Override + public Set contracts() { + return contracts; + } + + @Override + public TypeName scope() { + return descriptor.scope(); + } + + @Override + public double weight() { + return descriptor.weight(); + } + + @Override + public TypeName serviceType() { + return descriptor.serviceType(); + } + + @Override + public String toString() { + return "Instance of " + descriptor.serviceType().fqName() + ": " + qualifiedInstance; + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ServicePlanBinder.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ServicePlanBinder.java new file mode 100644 index 00000000000..8103429b1dc --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ServicePlanBinder.java @@ -0,0 +1,209 @@ +/* + * 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.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.helidon.service.inject.ServiceSupplies.ServiceInstanceSupply; +import io.helidon.service.inject.ServiceSupplies.ServiceInstanceSupplyList; +import io.helidon.service.inject.ServiceSupplies.ServiceInstanceSupplyOptional; +import io.helidon.service.inject.ServiceSupplies.ServiceSupply; +import io.helidon.service.inject.ServiceSupplies.ServiceSupplyList; +import io.helidon.service.inject.ServiceSupplies.ServiceSupplyOptional; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.InstanceName__ServiceDescriptor; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +class ServicePlanBinder implements InjectionPlanBinder.Binder { + private final Map> injectionPlan = new LinkedHashMap<>(); + + private final InjectServiceDescriptor self; + private final Consumer>> injectionPlanConsumer; + private final InjectServiceRegistryImpl registry; + + private ServicePlanBinder(InjectServiceRegistryImpl registry, + InjectServiceDescriptor self, + Consumer>> injectionPlanConsumer) { + this.registry = registry; + this.self = self; + this.injectionPlanConsumer = injectionPlanConsumer; + } + + static InjectionPlanBinder.Binder create(InjectServiceRegistryImpl registry, + InjectServiceDescriptor descriptor, + Consumer>> planConsumer) { + return new ServicePlanBinder(registry, descriptor, planConsumer); + } + + @Override + public InjectionPlanBinder.Binder bind(Dependency dependency, ServiceInfo descriptor) { + if (descriptor == InstanceName__ServiceDescriptor.INSTANCE) { + injectionPlan.put(dependency, new IpPlan<>(new InstanceNameFailingSupplier(dependency), descriptor)); + } else { + ServiceSupply supply = new ServiceSupply<>(Lookup.create(dependency), + List.of(registry.serviceManager(descriptor))); + + injectionPlan.put(dependency, new IpPlan<>(supply, descriptor)); + } + return this; + } + + @Override + public InjectionPlanBinder.Binder bindServiceInstance(Dependency dependency, ServiceInfo descriptor) { + var supply = new ServiceInstanceSupply<>(Lookup.create(dependency), List.of(registry.serviceManager(descriptor))); + injectionPlan.put(dependency, new IpPlan<>(supply, descriptor)); + + return this; + } + + @Override + public InjectionPlanBinder.Binder bindOptional(Dependency dependency, ServiceInfo... descriptors) { + ServiceSupplyOptional supply = new ServiceSupplyOptional<>(Lookup.create(dependency), + toManagers(descriptors)); + + injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindOptionalOfServiceInstance(Dependency dependency, ServiceInfo... descriptors) { + var supply = new ServiceInstanceSupplyOptional<>(Lookup.create(dependency), + toManagers(descriptors)); + + injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindList(Dependency dependency, ServiceInfo... descriptors) { + ServiceSupplyList supply = new ServiceSupplyList<>(Lookup.create(dependency), + toManagers(descriptors)); + + injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindServiceInstanceList(Dependency dependency, ServiceInfo... descriptors) { + var supply = new ServiceInstanceSupplyList<>(Lookup.create(dependency), + toManagers(descriptors)); + + injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindSupplier(Dependency dependency, ServiceInfo descriptor) { + ServiceSupply supply = new ServiceSupply<>(Lookup.create(dependency), + toManagers(descriptor)); + + injectionPlan.put(dependency, new IpPlan<>(() -> supply, descriptor)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindSupplierOfOptional(Dependency dependency, ServiceInfo... descriptors) { + ServiceSupplyOptional supply = new ServiceSupplyOptional<>(Lookup.create(dependency), + toManagers(descriptors)); + + injectionPlan.put(dependency, new IpPlan<>(() -> supply, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindSupplierOfList(Dependency dependency, ServiceInfo... descriptors) { + ServiceSupplyList supply = new ServiceSupplyList<>(Lookup.create(dependency), + toManagers(descriptors)); + + injectionPlan.put(dependency, new IpPlan<>(() -> supply, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindOptionalOfSupplier(Dependency dependency, ServiceInfo... descriptors) { + // we must resolve this right now, so we just use the first descriptor, and hope the user did not inject + // this in a wrong scope + ServiceSupply supply = new ServiceSupply<>(Lookup.create(dependency), + toManagers(descriptors[0])); + injectionPlan.put(dependency, new IpPlan<>(() -> Optional.of(supply), descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindListOfSuppliers(Dependency dependency, ServiceInfo... descriptors) { + Lookup lookup = Lookup.create(dependency); + // we must resolve the list right now (one for each descriptor) + List> supplies = Stream.of(descriptors) + .map(this::toManagers) + .map(it -> new ServiceSupply<>(lookup, it)) + .toList(); + + injectionPlan.put(dependency, new IpPlan<>(() -> supplies, descriptors)); + return this; + } + + @Override + public InjectionPlanBinder.Binder bindNull(Dependency dependency) { + injectionPlan.put(dependency, new IpPlan<>(() -> null)); + return this; + } + + @Override + public void commit() { + injectionPlanConsumer.accept(Map.copyOf(injectionPlan)); + } + + @Override + public String toString() { + return "Service plan binder for " + self.serviceType(); + } + + private List> toManagers(ServiceInfo... descriptors) { + List> result = new ArrayList<>(); + for (ServiceInfo descriptor : descriptors) { + result.add(registry.serviceManager(descriptor)); + } + return result; + } + + private static final class InstanceNameFailingSupplier implements Supplier { + private final Dependency dependency; + + private InstanceNameFailingSupplier(Dependency dependency) { + this.dependency = dependency; + } + + @Override + public Object get() { + throw new ServiceRegistryException( + "@" + Injection.InstanceName.class.getName() + + "should have been resolved to correct name during lookup for " + + dependency); + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceProvider.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceProvider.java new file mode 100644 index 00000000000..acca78b1fae --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceProvider.java @@ -0,0 +1,219 @@ +/* + * 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.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.ActivationRequest; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.ServiceInstance; +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +/** + * Takes care of a single service descriptor. + * + * @param type of the provided service + */ +class ServiceProvider { + private final InjectServiceRegistryImpl registry; + private final InjectServiceInfo serviceInfo; + private final InjectServiceDescriptor descriptor; + + private final ActivationRequest activationRequest; + private final InterceptionMetadata interceptionMetadata; + private final Contracts.ContractLookup contracts; + private volatile Map> injectionPlan = null; + + ServiceProvider(InjectServiceRegistryImpl serviceRegistry, + InjectServiceDescriptor descriptor) { + + Objects.requireNonNull(serviceRegistry); + Objects.requireNonNull(descriptor); + + this.registry = serviceRegistry; + this.interceptionMetadata = registry.interceptionMetadata(); + this.activationRequest = registry.activationRequest(); + this.serviceInfo = descriptor; + this.descriptor = descriptor; + + this.contracts = Contracts.create(descriptor); + } + + @Override + public String toString() { + return "ServiceProvider for " + serviceInfo.serviceType().fqName(); + } + + InjectServiceInfo serviceInfo() { + return serviceInfo; + } + + InjectServiceDescriptor descriptor() { + return descriptor; + } + + InjectionPlanBinder.Binder servicePlanBinder() { + return ServicePlanBinder.create(registry, descriptor, it -> this.injectionPlan = it); + } + + Map> injectionPlan() { + Map> usedIp = injectionPlan; + if (usedIp == null) { + // no application, we have to create injection plan from current services + usedIp = createInjectionPlan(); + this.injectionPlan = usedIp; + } + return usedIp; + } + + InterceptionMetadata interceptionMetadata() { + return interceptionMetadata; + } + + Set contracts(Lookup lookup) { + return contracts.contracts(lookup); + } + + ActivationRequest activationRequest() { + return activationRequest; + } + + private Map> createInjectionPlan() { + // for core services, we must use Dependency, for inject services, we must use Ip + List dependencies = descriptor.coreInfo().dependencies(); + + if (dependencies.isEmpty()) { + return Map.of(); + } + + AtomicReference>> injectionPlan = new AtomicReference<>(); + + InjectionPlanBinder.Binder binder = ServicePlanBinder.create(registry, + descriptor, + injectionPlan::set); + for (Dependency injectionPoint : dependencies) { + planForIp(binder, injectionPoint); + } + + binder.commit(); + + return injectionPlan.get(); + } + + private void planForIp(InjectionPlanBinder.Binder injectionPlan, + Dependency injectionPoint) { + /* + very similar code is used in ApplicationCreator.buildTimeBinding + make sure this is kept in sync! + */ + Lookup lookup = Lookup.create(injectionPoint); + + if (descriptor.contracts().containsAll(lookup.contracts()) + && descriptor.qualifiers().equals(lookup.qualifiers())) { + // injection point 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 + lookup = Lookup.builder(lookup) + .weight(descriptor.weight()) + .build(); + } + + List discovered = registry.lookupServices(lookup) + .stream() + .filter(it -> it != descriptor) + .map(InjectServiceInfo::coreInfo) + .toList(); + + /* + Very similar code is used for build time code generation in ApplicationCreator.buildTimeBinding + make sure this is kept in sync! + */ + TypeName ipType = injectionPoint.typeName(); + + // now there are a few options - optional, list, and single instance + if (ipType.isList()) { + ServiceInfo[] descriptors = discovered.toArray(new ServiceInfo[0]); + TypeName typeOfList = ipType.typeArguments().getFirst(); + if (typeOfList.isSupplier()) { + // inject List> + injectionPlan.bindListOfSuppliers(injectionPoint, descriptors); + } else if (typeOfList.equals(ServiceInstance.TYPE)) { + injectionPlan.bindServiceInstanceList(injectionPoint, descriptors); + } else { + // inject List + injectionPlan.bindList(injectionPoint, descriptors); + } + } else if (ipType.isOptional()) { + // inject Optional + if (discovered.isEmpty()) { + injectionPlan.bindOptional(injectionPoint); + } else { + TypeName typeOfOptional = ipType.typeArguments().getFirst(); + if (typeOfOptional.isSupplier()) { + injectionPlan.bindOptionalOfSupplier(injectionPoint, discovered.getFirst()); + } else if (typeOfOptional.equals(ServiceInstance.TYPE)) { + injectionPlan.bindOptionalOfServiceInstance(injectionPoint, discovered.getFirst()); + } else { + injectionPlan.bindOptional(injectionPoint, discovered.getFirst()); + } + } + } else if (ipType.isSupplier()) { + // one of the supplier options + TypeName typeOfSupplier = ipType.typeArguments().getFirst(); + if (typeOfSupplier.isOptional()) { + // inject Supplier> + injectionPlan.bindSupplierOfOptional(injectionPoint, discovered.toArray(new ServiceInfo[0])); + } else if (typeOfSupplier.isList()) { + // inject Supplier> + injectionPlan.bindSupplierOfList(injectionPoint, discovered.toArray(new ServiceInfo[0])); + } else { + // inject Supplier + if (discovered.isEmpty()) { + // null binding is not supported at runtime + throw new ServiceRegistryException(injectionPoint.service().fqName() + + ": expected to resolve a service matching injection point " + + injectionPoint); + } + injectionPlan.bindSupplier(injectionPoint, discovered.getFirst()); + } + } else { + // inject Contract + if (discovered.isEmpty()) { + // null binding is not supported at runtime + throw new ServiceRegistryException(injectionPoint.service().fqName() + + ": expected to resolve a service matching injection point " + + injectionPoint); + } + if (ipType.equals(ServiceInstance.TYPE)) { + injectionPlan.bindServiceInstance(injectionPoint, discovered.getFirst()); + } else { + injectionPlan.bind(injectionPoint, discovered.getFirst()); + } + } + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceSupplies.java b/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceSupplies.java new file mode 100644 index 00000000000..8034d61b36f --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/ServiceSupplies.java @@ -0,0 +1,247 @@ +/* + * 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.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.inject.api.ServiceInstance; +import io.helidon.service.registry.ServiceInfo; +import io.helidon.service.registry.ServiceRegistryException; + +import static io.helidon.service.inject.LookupTrace.traceLookup; +import static io.helidon.service.inject.LookupTrace.traceLookupInstance; +import static io.helidon.service.inject.LookupTrace.traceLookupInstances; + +final class ServiceSupplies { + private ServiceSupplies() { + } + + private static List> explodeFilterAndSort(Lookup lookup, + List> serviceManagers) { + // this method is called when we resolve instances, so we can safely assume any scope is active + traceLookup(lookup, "explode, filter, and sort"); + List> result = new ArrayList<>(); + + for (ServiceManager serviceManager : serviceManagers) { + List> thisManager = new ArrayList<>(); + serviceManager.activator() + .instances(lookup) + .stream() + .flatMap(List::stream) + .map(it -> serviceManager.registryInstance(lookup, it)) + .forEach(thisManager::add); + + traceLookupInstance(lookup, serviceManager, thisManager); + + result.addAll(thisManager); + } + + result.sort(RegistryInstanceComparator.instance()); + + traceLookupInstances(lookup, result); + + return List.copyOf(result); + } + + private static class ServiceSupplyBase { + private final Lookup lookup; + private final List> managers; + + private ServiceSupplyBase(Lookup lookup, List> managers) { + this.managers = managers; + this.lookup = lookup; + } + + @Override + public String toString() { + return managers.stream() + .map(ServiceManager::descriptor) + .map(ServiceInfo::serviceType) + .map(TypeName::fqName) + .collect(Collectors.joining(", ")); + } + } + + static class ServiceInstanceSupply extends ServiceSupplyBase implements Supplier> { + private final Supplier> value; + + ServiceInstanceSupply(Lookup lookup, List> managers) { + super(lookup, managers); + Supplier> supplier; + + supplier = () -> explodeFilterAndSort(lookup, managers) + .stream() + .findFirst() + .orElseThrow(() -> new ServiceRegistryException( + "Neither of matching services could provide a value. Descriptors: " + managers + ", " + + "lookup: " + super.lookup)); + + this.value = supplier; + } + + @Override + public ServiceInstance get() { + return value.get(); + } + } + + static class ServiceSupply extends ServiceSupplyBase implements Supplier { + private final Supplier value; + + // supply a single instance at runtime based on the manager + ServiceSupply(Lookup lookup, List> managers) { + super(lookup, managers); + + Supplier supplier; + + supplier = () -> explodeFilterAndSort(lookup, managers) + .stream() + .findFirst() + .map(ServiceInstance::get) + .orElseThrow(() -> new ServiceRegistryException( + "Neither of matching services could provide a value. Descriptors: " + managers + ", " + + "lookup: " + super.lookup)); + + this.value = supplier; + } + + @Override + public T get() { + return value.get(); + } + } + + static class ServiceSupplyOptional extends ServiceSupplyBase implements Supplier> { + // supply a single instance at runtime based on the manager + ServiceSupplyOptional(Lookup lookup, List> managers) { + super(lookup, managers); + } + + @Override + public Optional get() { + Optional> first = explodeFilterAndSort(super.lookup, super.managers) + .stream() + .findFirst(); + return first.map(Supplier::get); + } + } + + static class ServiceInstanceSupplyOptional extends ServiceSupplyBase implements Supplier>> { + // supply a single instance at runtime based on the manager + ServiceInstanceSupplyOptional(Lookup lookup, List> managers) { + super(lookup, managers); + } + + @Override + public Optional> get() { + return explodeFilterAndSort(super.lookup, super.managers) + .stream() + .findFirst(); + } + } + + static class ServiceSupplyList extends ServiceSupplyBase implements Supplier> { + // supply a single instance at runtime based on the manager + ServiceSupplyList(Lookup lookup, List> managers) { + super(lookup, managers); + } + + @Override + public List get() { + Stream> stream = explodeFilterAndSort(super.lookup, super.managers) + .stream(); + + return stream.map(Supplier::get) + .toList(); + } + } + + static class ServiceInstanceSupplyList extends ServiceSupplyBase implements Supplier>> { + // supply a single instance at runtime based on the manager + ServiceInstanceSupplyList(Lookup lookup, List> managers) { + super(lookup, managers); + } + + @Override + public List> get() { + return explodeFilterAndSort(super.lookup, super.managers) + .stream() + .collect(Collectors.toUnmodifiableList()); + } + } + + private static class RegistryInstanceComparator implements Comparator> { + private static final RegistryInstanceComparator INSTANCE = new RegistryInstanceComparator(); + + private RegistryInstanceComparator() { + } + + /** + * Returns a service provider comparator. + * + * @return the service provider comparator + */ + static RegistryInstanceComparator instance() { + return INSTANCE; + } + + @Override + public int compare(ServiceInstance p1, + ServiceInstance p2) { + if (p1 == p2) { + return 0; + } + + // unqualified instances always first (even if lower weight) + if (p1.qualifiers().isEmpty() && !p2.qualifiers().isEmpty()) { + return -1; + } + + if (p2.qualifiers().isEmpty() && !p1.qualifiers().isEmpty()) { + return 1; + } + + // @default name before any other name + if (p1.qualifiers().contains(Qualifier.DEFAULT_NAMED) && !p2.qualifiers().contains(Qualifier.DEFAULT_NAMED)) { + return -1; + } + + if (p2.qualifiers().contains(Qualifier.DEFAULT_NAMED) && !p1.qualifiers().contains(Qualifier.DEFAULT_NAMED)) { + return 1; + } + + // weights + int comp = Double.compare(p2.weight(), p1.weight()); + if (comp != 0) { + return comp; + } + + // last by name + return p1.serviceType().compareTo(p2.serviceType()); + } + + } +} diff --git a/service/inject/inject/src/main/java/io/helidon/service/inject/package-info.java b/service/inject/inject/src/main/java/io/helidon/service/inject/package-info.java new file mode 100644 index 00000000000..5ba826bcc8a --- /dev/null +++ b/service/inject/inject/src/main/java/io/helidon/service/inject/package-info.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. + */ + +/** + * Support for service registry with full injection and interception. + * + * @see io.helidon.service.inject.api.InjectRegistry + * @see io.helidon.service.inject.InjectRegistryManager + * @see io.helidon.service.inject.api.Injection + */ +package io.helidon.service.inject; diff --git a/service/inject/inject/src/main/java/module-info.java b/service/inject/inject/src/main/java/module-info.java new file mode 100644 index 00000000000..f3b6ee0cf16 --- /dev/null +++ b/service/inject/inject/src/main/java/module-info.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.Preview; + +/** + * Service registry with injection support API. + */ +@Feature(value = "Injection", + description = "Injection, interception and config bean support for Service Registry", + path = {"Registry", "Injection"}, + since = "4.2.0") +@Preview +module io.helidon.service.inject { + requires static io.helidon.common.features.api; + + requires io.helidon.metrics.api; + requires io.helidon.service.metadata; + + requires transitive io.helidon.service.inject.api; + requires transitive io.helidon.service.registry; + requires transitive io.helidon.common.config; + requires transitive io.helidon.builder.api; + requires transitive io.helidon.common.types; + requires transitive io.helidon.common.configurable; + + exports io.helidon.service.inject; + + provides io.helidon.service.registry.spi.ServiceRegistryManagerProvider + with io.helidon.service.inject.InjectRegistryManagerProvider; +} \ No newline at end of file diff --git a/service/inject/pom.xml b/service/inject/pom.xml new file mode 100644 index 00000000000..a246ccb2276 --- /dev/null +++ b/service/inject/pom.xml @@ -0,0 +1,44 @@ + + + + + + io.helidon.service + helidon-service-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.service.inject + helidon-service-inject-project + Helidon Service Inject Project + pom + + Full injection support on top of service registry, and API to declare injection in services + + + + codegen + api + inject + + diff --git a/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadata.java b/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadata.java index cd665b6553e..49323167adc 100644 --- a/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadata.java +++ b/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadata.java @@ -17,7 +17,9 @@ package io.helidon.service.metadata; import java.util.Set; +import java.util.stream.Collectors; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.metadata.hson.Hson; @@ -38,9 +40,36 @@ public interface DescriptorMetadata { * @param weight weight of the service descriptor * @param contracts contracts the service implements * @return a new descriptor metadata instance + * @deprecated use {@link #create(String, io.helidon.common.types.TypeName, double, java.util.Set, java.util.Set)} instead */ + @Deprecated(forRemoval = true, since = "4.2.0") static DescriptorMetadata create(String registryType, TypeName descriptor, double weight, Set contracts) { - return new DescriptorMetadataImpl(registryType, weight, descriptor, contracts); + return new DescriptorMetadataImpl( + registryType, + weight, + descriptor, + contracts.stream() + .map(ResolvedType::create) + .collect(Collectors.toUnmodifiableSet()), + Set.of()); + } + + /** + * Create a new instance from descriptor information, i.e. when code generating the descriptor metadata. + * + * @param registryType type of registry, such as {@link #REGISTRY_TYPE_CORE} + * @param descriptor type of the service descriptor (the generated file from {@code helidon-service-codegen}) + * @param weight weight of the service descriptor + * @param contracts contracts the service implements + * @param factoryContracts factory contracts the service instance implements + * @return a new descriptor metadata instance + */ + static DescriptorMetadata create(String registryType, + TypeName descriptor, + double weight, + Set contracts, + Set factoryContracts) { + return new DescriptorMetadataImpl(registryType, weight, descriptor, contracts, factoryContracts); } /** @@ -62,7 +91,14 @@ static DescriptorMetadata create(String registryType, TypeName descriptor, doubl * * @return contracts the service implements/provides. */ - Set contracts(); + Set contracts(); + + /** + * Contracts of the factory service, if this describes a factory, empty otherwise. + * + * @return factory contracts + */ + Set factoryContracts(); /** * Weight of the service. diff --git a/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadataImpl.java b/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadataImpl.java index 19ed3141492..7a0084837db 100644 --- a/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadataImpl.java +++ b/service/metadata/src/main/java/io/helidon/service/metadata/DescriptorMetadataImpl.java @@ -21,16 +21,23 @@ import java.util.stream.Collectors; import io.helidon.common.Weighted; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.metadata.hson.Hson; record DescriptorMetadataImpl(String registryType, double weight, TypeName descriptorType, - Set contracts) implements DescriptorMetadata { + Set contracts, + Set factoryContracts) implements DescriptorMetadata { private static final int CURRENT_DESCRIPTOR_VERSION = 1; private static final int DEFAULT_DESCRIPTOR_VERSION = 1; + private static final String HSON_TYPE = "type"; + private static final String HSON_WEIGHT = "weight"; + private static final String HSON_DESCRIPTOR = "descriptor"; + private static final String HSON_CONTRACTS = "contracts"; + private static final String HSON_FACTORY_CONTRACTS = "factoryContracts"; static DescriptorMetadata create(String moduleName, String location, Hson.Struct service) { int version = service.intValue("version", DEFAULT_DESCRIPTOR_VERSION); @@ -40,24 +47,30 @@ static DescriptorMetadata create(String moduleName, String location, Hson.Struct + " loaded from \"" + location + "\", " + "expected version: \"" + CURRENT_DESCRIPTOR_VERSION + "\"," + " descriptor (if available): " - + service.stringValue("descriptor", "N/A")); + + service.stringValue(HSON_DESCRIPTOR, "N/A")); } - String type = service.stringValue("type", REGISTRY_TYPE_CORE); - TypeName descriptor = service.stringValue("descriptor") + String type = service.stringValue(HSON_TYPE, REGISTRY_TYPE_CORE); + TypeName descriptor = service.stringValue(HSON_DESCRIPTOR) .map(TypeName::create) .orElseThrow(() -> new IllegalStateException("Could not parse service metadata " + " for module \"" + moduleName + "\"" + " loaded from \"" + location + "\", " + "missing \"descriptor\" value")); - double weight = service.doubleValue("weight", Weighted.DEFAULT_WEIGHT); - Set contracts = service.stringArray("contracts") + double weight = service.doubleValue(HSON_WEIGHT, Weighted.DEFAULT_WEIGHT); + Set contracts = service.stringArray(HSON_CONTRACTS) .orElseGet(List::of) .stream() - .map(TypeName::create) + .map(ResolvedType::create) + .collect(Collectors.toUnmodifiableSet()); + + Set factoryContracts = service.stringArray(HSON_FACTORY_CONTRACTS) + .orElseGet(List::of) + .stream() + .map(ResolvedType::create) .collect(Collectors.toSet()); - return new DescriptorMetadataImpl(type, weight, descriptor, contracts); + return new DescriptorMetadataImpl(type, weight, descriptor, contracts, factoryContracts); } @Override @@ -65,16 +78,22 @@ public Hson.Struct toHson() { var builder = Hson.structBuilder(); if (!registryType.equals(REGISTRY_TYPE_CORE)) { - builder.set("type", registryType); + builder.set(HSON_TYPE, registryType); } if (weight != Weighted.DEFAULT_WEIGHT) { - builder.set("weight", weight); + builder.set(HSON_WEIGHT, weight); } - builder.set("descriptor", descriptorType.fqName()); - builder.setStrings("contracts", contracts.stream() - .map(TypeName::fqName) + builder.set(HSON_DESCRIPTOR, descriptorType.fqName()); + builder.setStrings(HSON_CONTRACTS, contracts.stream() + .map(ResolvedType::resolvedName) .sorted(String.CASE_INSENSITIVE_ORDER) .collect(Collectors.toUnmodifiableList())); + if (!factoryContracts.isEmpty()) { + builder.setStrings(HSON_FACTORY_CONTRACTS, factoryContracts.stream() + .map(ResolvedType::resolvedName) + .sorted(String.CASE_INSENSITIVE_ORDER) + .collect(Collectors.toUnmodifiableList())); + } return builder.build(); } diff --git a/service/pom.xml b/service/pom.xml index db67a8faa3f..72de3cd13e2 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -44,6 +44,7 @@ metadata codegen registry + inject diff --git a/service/registry/pom.xml b/service/registry/pom.xml index 500631ccdd8..3d79f5592f5 100644 --- a/service/registry/pom.xml +++ b/service/registry/pom.xml @@ -97,6 +97,11 @@ helidon-common-features-processor ${helidon.version} + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + @@ -120,6 +125,11 @@ helidon-common-features-processor ${helidon.version} + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + 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 1ed9cfde51b..e7635d0681b 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 @@ -33,11 +33,11 @@ import io.helidon.common.LazyValue; import io.helidon.common.Weighted; import io.helidon.common.Weights; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.metadata.hson.Hson; import io.helidon.service.metadata.DescriptorMetadata; import io.helidon.service.metadata.Descriptors; -import io.helidon.service.registry.GeneratedService.Descriptor; import static io.helidon.service.metadata.Descriptors.SERVICE_REGISTRY_LOCATION; import static java.nio.charset.StandardCharsets.UTF_8; @@ -106,12 +106,16 @@ private static Class toClass(TypeName className) { } } - private static Descriptor getDescriptorInstance(TypeName descriptorType) { + private static ServiceDescriptor getDescriptorInstance(TypeName descriptorType) { Class clazz = toClass(descriptorType); try { Field field = clazz.getField("INSTANCE"); - return (Descriptor) field.get(null); + Object descriptorInstance = field.get(null); + if (descriptorInstance instanceof ServiceDescriptor sd) { + return sd; + } + return (ServiceDescriptor) field.get(null); } catch (ReflectiveOperationException e) { throw new ServiceRegistryException("Could not obtain the instance of service descriptor " + descriptorType.fqName(), @@ -159,11 +163,12 @@ private static DescriptorHandlerImpl createServiceProviderDescriptor(TypeName pr weight)); } - Descriptor descriptor = ServiceLoader__ServiceDescriptor.create(providerType, provider, weight); + ServiceDescriptor descriptor = ServiceLoader__ServiceDescriptor.create(providerType, provider, weight); return new DescriptorHandlerImpl(DescriptorMetadata.create("core", descriptor.descriptorType(), weight, - descriptor.contracts()), + descriptor.contracts(), + descriptor.factoryContracts()), LazyValue.create(descriptor)); } @@ -196,14 +201,14 @@ boolean isComment() { } private record DescriptorHandlerImpl(DescriptorMetadata metadata, - LazyValue> descriptorSupplier) implements DescriptorHandler { + LazyValue> descriptorSupplier) implements DescriptorHandler { DescriptorHandlerImpl(DescriptorMetadata metadata) { this(metadata, LazyValue.create(() -> getDescriptorInstance(metadata.descriptorType()))); } @Override - public Descriptor descriptor() { + public ServiceDescriptor descriptor() { return descriptorSupplier.get(); } @@ -218,10 +223,15 @@ public TypeName descriptorType() { } @Override - public Set contracts() { + public Set contracts() { return metadata.contracts(); } + @Override + public Set factoryContracts() { + return metadata.factoryContracts(); + } + @Override public double weight() { return metadata.weight(); diff --git a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java index b6b13b517aa..e45ea60acc1 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java +++ b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java @@ -31,9 +31,8 @@ import java.util.stream.Collectors; import io.helidon.common.LazyValue; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypeNames; -import io.helidon.service.registry.GeneratedService.Descriptor; /** * Basic implementation of the service registry with simple dependency support. @@ -45,14 +44,14 @@ class CoreServiceRegistry implements ServiceRegistry { Comparator.comparing(ServiceProvider::weight).reversed() .thenComparing(ServiceProvider::descriptorType); - private final Map> providersByContract; + private final Map> providersByContract; private final Map providersByService; private final List allProviders; @SuppressWarnings({"rawtypes", "unchecked"}) CoreServiceRegistry(ServiceRegistryConfig config, ServiceDiscovery serviceDiscovery) { List allProviders = new ArrayList<>(); - Map> providers = new HashMap<>(); + Map> providers = new HashMap<>(); Map providersByService = new IdentityHashMap<>(); // each just once @@ -74,7 +73,7 @@ class CoreServiceRegistry implements ServiceRegistry { }); // add configured descriptors - for (Descriptor descriptor : config.serviceDescriptors()) { + for (ServiceDescriptor descriptor : config.serviceDescriptors()) { BoundDescriptor bd = new BoundDescriptor(this, descriptor, LazyValue.create(() -> { var instance = instance(descriptor); instance.ifPresent(descriptor::postConstruct); @@ -170,7 +169,7 @@ public Optional get(ServiceInfo serviceInfo) { @Override public List allServices(TypeName contract) { - return Optional.ofNullable(providersByContract.get(contract)) + return Optional.ofNullable(providersByContract.get(ResolvedType.create(contract))) .orElseGet(List::of) .stream() .map(ServiceProvider::descriptor) @@ -182,10 +181,10 @@ void shutdown() { allProviders.forEach(ServiceProvider::close); } - private static void addContracts(Map> providers, - Set contracts, + private static void addContracts(Map> providers, + Set contracts, ServiceProvider provider) { - for (TypeName contract : contracts) { + for (ResolvedType contract : contracts) { providers.computeIfAbsent(contract, it -> new ArrayList<>()) .add(provider); } @@ -194,29 +193,22 @@ private static void addContracts(Map> providers, @SuppressWarnings({"rawtypes", "unchecked"}) private ServiceAndInstance instanceSupplier(DescriptorHandler descriptorMeta) { LazyValue> serviceInstance = LazyValue.create(() -> { - Descriptor descriptor = descriptorMeta.descriptor(); + ServiceDescriptor descriptor = descriptorMeta.descriptor(); var instance = instance(descriptor); instance.ifPresent(descriptor::postConstruct); return instance; }); - if (descriptorMeta.contracts().contains(TypeNames.SUPPLIER)) { + if (descriptorMeta.factoryContracts().isEmpty()) { + return new ServiceAndInstance(serviceInstance); + } else { return new ServiceAndInstance(serviceInstance, () -> instanceFromSupplier(descriptorMeta.descriptor(), serviceInstance)); - } else { - return new ServiceAndInstance(serviceInstance); - } - } - - private record ServiceAndInstance(LazyValue> serviceSupplier, - Supplier> instanceSupplier) { - ServiceAndInstance(LazyValue> serviceSupplier) { - this(serviceSupplier, serviceSupplier); } } private List allProviders(TypeName contract) { - List serviceProviders = providersByContract.get(contract); + List serviceProviders = providersByContract.get(ResolvedType.create(contract)); if (serviceProviders == null) { return List.of(); } @@ -224,7 +216,8 @@ private List allProviders(TypeName contract) { return List.copyOf(serviceProviders); } - private Optional instanceFromSupplier(Descriptor descriptor, LazyValue> serviceInstanceSupplier) { + private Optional instanceFromSupplier(ServiceDescriptor descriptor, + LazyValue> serviceInstanceSupplier) { Optional serviceInstance = serviceInstanceSupplier.get(); if (serviceInstance.isEmpty()) { return Optional.empty(); @@ -243,14 +236,14 @@ private Optional instanceFromSupplier(Descriptor descriptor, LazyValu } } - private Optional instance(Descriptor descriptor) { + private Optional instance(ServiceDescriptor descriptor) { var dependencyContext = collectDependencies(descriptor); Object serviceInstance = descriptor.instantiate(dependencyContext); return Optional.of(serviceInstance); } - private DependencyContext collectDependencies(Descriptor descriptor) { + private DependencyContext collectDependencies(ServiceDescriptor descriptor) { List dependencies = descriptor.dependencies(); Map collectedDependencies = new HashMap<>(); @@ -289,7 +282,7 @@ private Object dependencyNoSupplier(TypeName dependencyType, TypeName contract) } private interface ServiceProvider { - Descriptor descriptor(); + ServiceDescriptor descriptor(); Optional instance(); @@ -300,7 +293,14 @@ private interface ServiceProvider { void close(); } - private record BoundInstance(Descriptor descriptor, Optional instance) implements ServiceProvider { + private record ServiceAndInstance(LazyValue> serviceSupplier, + Supplier> instanceSupplier) { + ServiceAndInstance(LazyValue> serviceSupplier) { + this(serviceSupplier, serviceSupplier); + } + } + + private record BoundInstance(ServiceDescriptor descriptor, Optional instance) implements ServiceProvider { @Override public double weight() { return descriptor.weight(); @@ -318,12 +318,12 @@ public void close() { } private record BoundDescriptor(CoreServiceRegistry registry, - Descriptor descriptor, + ServiceDescriptor descriptor, LazyValue> lazyInstance, ReentrantLock lock) implements ServiceProvider { private BoundDescriptor(CoreServiceRegistry registry, - Descriptor descriptor, + ServiceDescriptor descriptor, LazyValue> lazyInstance) { this(registry, descriptor, lazyInstance, new ReentrantLock()); } @@ -360,7 +360,7 @@ public TypeName descriptorType() { @Override public void close() { if (lazyInstance.isLoaded()) { - lazyInstance.get().ifPresent(it -> ((Descriptor) descriptor).preDestroy(it)); + lazyInstance.get().ifPresent(it -> ((ServiceDescriptor) descriptor).preDestroy(it)); } } } @@ -377,7 +377,7 @@ private DiscoveredDescriptor(CoreServiceRegistry registry, } @Override - public Descriptor descriptor() { + public ServiceDescriptor descriptor() { return metadata.descriptor(); } @@ -415,7 +415,7 @@ public TypeName descriptorType() { public void close() { var serviceSupplier = instances.serviceSupplier(); if (serviceSupplier.isLoaded()) { - serviceSupplier.get().ifPresent(it -> ((Descriptor) metadata.descriptor()).preDestroy(it)); + serviceSupplier.get().ifPresent(it -> ((ServiceDescriptor) metadata.descriptor()).preDestroy(it)); } } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/DescriptorHandler.java b/service/registry/src/main/java/io/helidon/service/registry/DescriptorHandler.java index d861de61666..06a97bbd827 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/DescriptorHandler.java +++ b/service/registry/src/main/java/io/helidon/service/registry/DescriptorHandler.java @@ -26,5 +26,5 @@ public interface DescriptorHandler extends io.helidon.service.metadata.Descripto * * @return the descriptor */ - GeneratedService.Descriptor descriptor(); + ServiceDescriptor descriptor(); } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ExistingInstanceDescriptor.java b/service/registry/src/main/java/io/helidon/service/registry/ExistingInstanceDescriptor.java new file mode 100644 index 00000000000..b92d4692bbd --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/ExistingInstanceDescriptor.java @@ -0,0 +1,122 @@ +/* + * 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.registry; + +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; + +/** + * A special case service descriptor allowing registration of service instances that do not have + * a code generated service descriptor, such as for testing. + *

    + * Note that these instances cannot be used for creating code generated binding, as they do not exist as classes. + * + * @param type of the instance + */ +public final class ExistingInstanceDescriptor implements ServiceDescriptor { + private static final TypeName DESCRIPTOR_TYPE = TypeName.create(ExistingInstanceDescriptor.class); + private final T instance; + private final TypeName serviceType; + private final Set contracts; + private final double weight; + + private ExistingInstanceDescriptor(T instance, + TypeName serviceType, + Set contracts, + double weight) { + this.instance = instance; + this.serviceType = serviceType; + this.contracts = contracts; + this.weight = weight; + } + + /** + * Create a new instance. + * The only place this can be used at is with + * {@link + * io.helidon.service.registry.ServiceRegistryConfig.Builder#addServiceDescriptor(io.helidon.service.registry.ServiceDescriptor)}. + * + * @param instance service instance to use + * @param contracts contracts of the service (the ones we want service registry to use) + * @param weight weight of the service + * @param type of the service + * @return a new service descriptor for the provided information + */ + public static ExistingInstanceDescriptor create(T instance, + Collection> contracts, + double weight) { + TypeName serviceType = TypeName.create(instance.getClass()); + Set contractSet = contracts.stream() + .map(ResolvedType::create) + .collect(Collectors.toSet()); + + return new ExistingInstanceDescriptor<>(instance, serviceType, contractSet, weight); + } + + @Override + public TypeName serviceType() { + return serviceType; + } + + @Override + public TypeName descriptorType() { + return DESCRIPTOR_TYPE; + } + + @Override + public Set contracts() { + return contracts; + } + + @Override + public Object instantiate(DependencyContext ctx) { + return instance; + } + + @Override + public double weight() { + return weight; + } + + @Override + public String toString() { + return contracts + " (" + weight + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ExistingInstanceDescriptor that)) { + return false; + } + return Double.compare(weight, that.weight) == 0 + && instance == that.instance + && Objects.equals(contracts, that.contracts); + } + + @Override + public int hashCode() { + return Objects.hash(instance, contracts, weight); + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java b/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java index 7061c0dd63a..85a68e2f86c 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java +++ b/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java @@ -193,7 +193,7 @@ private static ConfiguredService configuredService(Config serviceConfig, boolean "Service provider configuration defined as a list must have a single node that is the type, " + "with children containing the provider configuration. Failed on: " + serviceConfig.key()); } - usedConfig = configs.get(0); + usedConfig = configs.getFirst(); name = usedConfig.name(); type = usedConfig.get(KEY_SERVICE_TYPE).asString().orElse(name); enabled = usedConfig.get(KEY_SERVICE_ENABLED).asBoolean().orElse(enabled); @@ -326,42 +326,6 @@ private static ConfiguredService configuredService(Config serviceConfig, boolean return result; } - /** - * A descriptor of a service. In addition to providing service metadata, this also allows instantiation - * of the service instance, with dependent services as parameters. - * - * @param type of the described service - */ - public interface Descriptor extends ServiceInfo { - /** - * Create a new service instance. - * - * @param ctx dependency context with all dependencies of this service - * @return a new instance, must be of the type T or a subclass - */ - // we cannot return T, as it does not allow us to correctly handle inheritance - default Object instantiate(DependencyContext ctx) { - throw new IllegalStateException("Cannot instantiate type " + serviceType().fqName() + ", as it is either abstract," - + " or an interface."); - } - - /** - * Invoke {@link io.helidon.service.registry.Service.PostConstruct} annotated method(s). - * - * @param instance instance to use - */ - default void postConstruct(T instance) { - } - - /** - * Invoke {@link io.helidon.service.registry.Service.PreDestroy} annotated method(s). - * - * @param instance instance to use - */ - default void preDestroy(T instance) { - } - } - private record TypeAndName(String type, String name) { } diff --git a/service/registry/src/main/java/io/helidon/service/registry/Service.java b/service/registry/src/main/java/io/helidon/service/registry/Service.java index 8f3d29eeafd..2c42ae30cce 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/Service.java +++ b/service/registry/src/main/java/io/helidon/service/registry/Service.java @@ -18,6 +18,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -59,12 +60,15 @@ private Service() { *

  • Implementing a {@link java.util.function.Supplier} of the contract; when using supplier, service registry * supports the capability to return {@link java.util.Optional} in case the service cannot provide a value; such * a service will be ignored and only other implementations (with lower weight) would be used. Supplier will be - * called each time the dependency is used, or each time a method on registry is called to request an instance
  • + * called each time the dependency is used, or each time a method on registry is called to request an instance. If the + * provided instance should be singleton-like as well, use {@link io.helidon.common.LazyValue} or + * similar approach to create it once and return the same instance every time * */ @Documented @Retention(RetentionPolicy.CLASS) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Inherited public @interface Provider { /** * Type name of this annotation. @@ -113,12 +117,14 @@ private Service() { * consider using {@link ExternalContracts} instead - this annotation can be placed on the * implementation class implementing the given {@code Contract} interface(s). *

    - * Default behavior of the service registry is to only provide support lookup based on contracts. + * Default behavior of the service registry is to assume any super type and implemented interface is a contract. This can + * be changed through annotation processor/codegen configuration. */ @Documented @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface Contract { + // contract should not be @Inherited, as we do not want types implementing a contract inherit this trait } /** @@ -162,7 +168,7 @@ private Service() { /** * Type of service registry that should read this descriptor. Defaults to * {@value DescriptorHandler#REGISTRY_TYPE_CORE}, so the descriptor must only implement - * {@link io.helidon.service.registry.GeneratedService.Descriptor}. + * {@link io.helidon.service.registry.ServiceDescriptor}. * * @return type of registry this descriptor supports */ diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceDescriptor.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceDescriptor.java new file mode 100644 index 00000000000..e066bebff64 --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceDescriptor.java @@ -0,0 +1,53 @@ +/* + * 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.registry; + +/** + * A descriptor of a service. In addition to providing service metadata, this also allows instantiation + * of the service instance, with dependent services as parameters. + * + * @param type of the described service + */ +public interface ServiceDescriptor extends ServiceInfo { + /** + * Create a new service instance. + * + * @param ctx dependency context with all dependencies of this service + * @return a new instance, must be of the type T or a subclass + */ + // we cannot return T, as it does not allow us to correctly handle inheritance + default Object instantiate(DependencyContext ctx) { + throw new IllegalStateException("Cannot instantiate type " + serviceType().fqName() + ", as it is either abstract," + + " or an interface."); + } + + /** + * Invoke {@link io.helidon.service.registry.Service.PostConstruct} annotated method(s). + * + * @param instance instance to use + */ + default void postConstruct(T instance) { + } + + /** + * Invoke {@link io.helidon.service.registry.Service.PreDestroy} annotated method(s). + * + * @param instance instance to use + */ + default void preDestroy(T instance) { + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceInfo.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceInfo.java index 50ffcf50e83..8fed8b99701 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceInfo.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceInfo.java @@ -20,6 +20,7 @@ import java.util.Set; import io.helidon.common.Weighted; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; /** @@ -41,11 +42,21 @@ public interface ServiceInfo extends Weighted { TypeName descriptorType(); /** - * Set of contracts the described service implements. + * Set of contracts the described service implements or provides through a factory method. * * @return set of contracts */ - default Set contracts() { + default Set contracts() { + return Set.of(); + } + + /** + * Set of contracts the described service implements directly. If the service is not a factory, + * this set is empty. + * + * @return set of factory contracts + */ + default Set factoryContracts() { return Set.of(); } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceLoader__ServiceDescriptor.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceLoader__ServiceDescriptor.java index 56bb3769c04..8117fc5e977 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceLoader__ServiceDescriptor.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceLoader__ServiceDescriptor.java @@ -21,15 +21,17 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import io.helidon.common.LazyValue; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; /** * Service descriptor to enable dependency on services loaded via {@link java.util.ServiceLoader}. */ @SuppressWarnings("checkstyle:TypeName") // matches pattern of generated descriptors -public abstract class ServiceLoader__ServiceDescriptor implements GeneratedService.Descriptor { +public abstract class ServiceLoader__ServiceDescriptor implements ServiceDescriptor { private static final TypeName DESCRIPTOR_TYPE = TypeName.create(ServiceLoader__ServiceDescriptor.class); // we must use instance comparison, so we must make sure we give the same instance for the same combination @@ -47,9 +49,9 @@ private ServiceLoader__ServiceDescriptor() { * @param weight weight of the provider * @return new descriptor */ - public static GeneratedService.Descriptor create(TypeName providerInterface, - ServiceLoader.Provider provider, - double weight) { + public static ServiceDescriptor create(TypeName providerInterface, + ServiceLoader.Provider provider, + double weight) { LOCK.lock(); try { TypeName providerImpl = TypeName.create(provider.type()); @@ -66,14 +68,39 @@ public static GeneratedService.Descriptor create(TypeName providerInterf } } + /** + * Create a new instance for a specific provider interface and implementation. + * This method is used from generated code. + * + * @param providerInterface provider interface type + * @param implType implementation class + * @param instanceSupplier supplier of a new instance (so we do not use reflection) + * @param weight weight of the provider implementation + * @param type of the implementation + * @return a new service descriptor + */ + public static ServiceDescriptor create(TypeName providerInterface, + Class implType, + Supplier instanceSupplier, + double weight) { + return ServiceLoader__ServiceDescriptor.create(providerInterface, new ProviderImpl(implType, instanceSupplier), weight); + } + @Override public TypeName descriptorType() { return DESCRIPTOR_TYPE; } + /** + * Type name of the provider interface this service implementation implements. + * + * @return provider interface type + */ + public abstract TypeName providerInterface(); + private static class ServiceProviderDescriptor extends ServiceLoader__ServiceDescriptor { private final TypeName providerInterface; - private final Set contracts; + private final Set contracts; private final TypeName providerImpl; private final double weight; private final LazyValue instance; @@ -83,7 +110,7 @@ private ServiceProviderDescriptor(TypeName providerInterface, ServiceLoader.Provider provider, double weight) { this.providerInterface = providerInterface; - this.contracts = Set.of(providerInterface); + this.contracts = Set.of(ResolvedType.create(providerInterface)); this.providerImpl = providerImpl; this.weight = weight; this.instance = LazyValue.create(provider); @@ -102,7 +129,7 @@ public TypeName serviceType() { } @Override - public Set contracts() { + public Set contracts() { return contracts; } @@ -116,8 +143,32 @@ public double weight() { return weight; } + @Override + public TypeName providerInterface() { + return providerInterface; + } } private record ProviderKey(TypeName providerInterface, TypeName providerImpl) { } + + private static class ProviderImpl implements ServiceLoader.Provider { + private final Class implType; + private final Supplier instanceSupplier; + + private ProviderImpl(Class implType, Supplier instanceSupplier) { + this.implType = implType; + this.instanceSupplier = instanceSupplier; + } + + @Override + public Class type() { + return implType; + } + + @Override + public Object get() { + return instanceSupplier.get(); + } + } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java index 607f3bc8a4a..85565221179 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java @@ -23,7 +23,6 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; import io.helidon.common.config.Config; -import io.helidon.service.registry.GeneratedService.Descriptor; /** * Helidon service registry configuration. @@ -65,7 +64,7 @@ interface ServiceRegistryConfigBlueprint { * @return services to register */ @Option.Singular - List> serviceDescriptors(); + List> serviceDescriptors(); /** * Manually register initial bindings for some of the services in the registry. @@ -74,7 +73,7 @@ interface ServiceRegistryConfigBlueprint { */ @Option.Singular @Option.SameGeneric - Map, Object> serviceInstances(); + Map, Object> serviceInstances(); /** * Config instance used to configure this registry configuration. diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigSupport.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigSupport.java index 15e9af6a88f..9d738b3d125 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigSupport.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigSupport.java @@ -16,11 +16,7 @@ package io.helidon.service.registry; -import java.util.Objects; -import java.util.Set; - import io.helidon.builder.api.Prototype; -import io.helidon.common.Weighted; import io.helidon.common.types.TypeName; class ServiceRegistryConfigSupport { @@ -32,8 +28,13 @@ private CustomMethods() { } /** - * Put an instance of a contract outside of service described services. - * This will create a "virtual" service descriptor that will not be valid for metadata operations. + * Put an instance of a contract. In case there is a descriptor that matches the contract + * (i.e. the service type is the provided contract), the instance will be assigned that descriptor. + * The instance would be outside of service described services otherwise, creating a + * "virtual" service descriptor that will not be valid for metadata operations. + *

    + * If there is no descriptor for the contract, you will not be able to use our Maven plugin to code generate bindings and + * main classes. * * @param builder ignored * @param contract contract to add a specific instance for @@ -47,8 +48,13 @@ static void putContractInstance(ServiceRegistryConfig.BuilderBase builder, } /** - * Put an instance of a contract outside of service described services. - * This will create a "virtual" service descriptor that will not be valid for metadata operations. + * Put an instance of a contract. In case there is a descriptor that matches the contract + * (i.e. the service type is the provided contract), the instance will be assigned that descriptor. + * The instance would be outside of service described services otherwise, creating a + * "virtual" service descriptor that will not be valid for metadata operations. + *

    + * If there is no descriptor for the contract, you will not be able to use our Maven plugin to code generate bindings and + * main classes. * * @param builder ignored * @param contract contract to add a specific instance for @@ -61,55 +67,4 @@ static void putContractInstance(ServiceRegistryConfig.BuilderBase builder, putContractInstance(builder, TypeName.create(contract), instance); } } - - private static class VirtualDescriptor implements GeneratedService.Descriptor { - private static final TypeName TYPE = TypeName.create(VirtualDescriptor.class); - private final Set contracts; - private final TypeName serviceType; - private final TypeName descriptorType; - - private VirtualDescriptor(TypeName contract) { - this.contracts = Set.of(contract); - this.serviceType = contract; - this.descriptorType = TypeName.builder(TYPE) - .className(TYPE.className() + "_" + contract.className() + "__VirtualDescriptor") - .build(); - } - - @Override - public TypeName serviceType() { - return serviceType; - } - - @Override - public TypeName descriptorType() { - return descriptorType; - } - - @Override - public Set contracts() { - return contracts; - } - - @Override - public double weight() { - return Weighted.DEFAULT_WEIGHT + 1000; - } - - @Override - public int hashCode() { - return Objects.hash(serviceType); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof VirtualDescriptor that)) { - return false; - } - return Objects.equals(serviceType, that.serviceType); - } - } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistry__ServiceDescriptor.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistry__ServiceDescriptor.java index e69b6b09808..826d37299f7 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistry__ServiceDescriptor.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistry__ServiceDescriptor.java @@ -18,20 +18,21 @@ import java.util.Set; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; /** * Service descriptor to enable dependency on {@link io.helidon.service.registry.ServiceRegistry}. */ @SuppressWarnings("checkstyle:TypeName") // matches pattern of generated descriptors -public class ServiceRegistry__ServiceDescriptor implements GeneratedService.Descriptor { +public class ServiceRegistry__ServiceDescriptor implements ServiceDescriptor { /** * Singleton instance to be referenced when building applications. */ public static final ServiceRegistry__ServiceDescriptor INSTANCE = new ServiceRegistry__ServiceDescriptor(); private static final TypeName DESCRIPTOR_TYPE = TypeName.create(ServiceRegistry__ServiceDescriptor.class); - private static final Set CONTRACTS = Set.of(ServiceRegistry.TYPE); + private static final Set CONTRACTS = Set.of(ResolvedType.create(ServiceRegistry.TYPE)); private ServiceRegistry__ServiceDescriptor() { } @@ -47,7 +48,7 @@ public TypeName descriptorType() { } @Override - public Set contracts() { + public Set contracts() { return CONTRACTS; } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/VirtualDescriptor.java b/service/registry/src/main/java/io/helidon/service/registry/VirtualDescriptor.java new file mode 100644 index 00000000000..014e49939d4 --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/VirtualDescriptor.java @@ -0,0 +1,78 @@ +/* + * 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.registry; + +import java.util.Objects; +import java.util.Set; + +import io.helidon.common.Weighted; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; + +/** + * A virtual descriptor is not backed by a generated descriptor. + */ +public class VirtualDescriptor implements ServiceDescriptor { + private static final TypeName TYPE = TypeName.create(VirtualDescriptor.class); + private final Set contracts; + private final TypeName serviceType; + private final TypeName descriptorType; + + VirtualDescriptor(TypeName contract) { + this.contracts = Set.of(ResolvedType.create(contract)); + this.serviceType = contract; + this.descriptorType = TypeName.builder(TYPE) + .className(TYPE.className() + "_" + contract.className() + "__VirtualDescriptor") + .build(); + } + + @Override + public TypeName serviceType() { + return serviceType; + } + + @Override + public TypeName descriptorType() { + return descriptorType; + } + + @Override + public Set contracts() { + return contracts; + } + + @Override + public double weight() { + return Weighted.DEFAULT_WEIGHT + 1000; + } + + @Override + public int hashCode() { + return Objects.hash(serviceType); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VirtualDescriptor that)) { + return false; + } + return Objects.equals(serviceType, that.serviceType); + } +} diff --git a/service/registry/src/main/java/module-info.java b/service/registry/src/main/java/module-info.java index c387787882f..3ef1a44825e 100644 --- a/service/registry/src/main/java/module-info.java +++ b/service/registry/src/main/java/module-info.java @@ -21,7 +21,7 @@ /** * Core service registry, supporting {@link io.helidon.service.registry.Service.Provider}. */ -@Feature(value = "registry", +@Feature(value = "Registry", description = "Service Registry", in = HelidonFlavor.SE, path = "Registry" diff --git a/service/tests/codegen/pom.xml b/service/tests/codegen/pom.xml index 13a4c39c24b..f0ac014576d 100644 --- a/service/tests/codegen/pom.xml +++ b/service/tests/codegen/pom.xml @@ -40,6 +40,14 @@ io.helidon.service helidon-service-registry + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + io.helidon.config helidon-config-metadata diff --git a/service/tests/codegen/src/main/java/module-info.java b/service/tests/codegen/src/main/java/module-info.java index 6dfc02fed59..ad35b9206ee 100644 --- a/service/tests/codegen/src/main/java/module-info.java +++ b/service/tests/codegen/src/main/java/module-info.java @@ -16,6 +16,8 @@ module io.helidon.service.tests.codegen { requires io.helidon.service.registry; + requires io.helidon.service.inject; + requires io.helidon.service.inject.api; requires io.helidon.service.codegen; requires io.helidon.config.metadata; } \ No newline at end of file diff --git a/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java b/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java index 3e6bf64c623..89c0c9d35dd 100644 --- a/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java +++ b/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java @@ -23,12 +23,16 @@ import java.util.Map; import java.util.Set; +import io.helidon.builder.api.Prototype; +import io.helidon.common.Generated; import io.helidon.common.types.TypeName; import io.helidon.service.codegen.ServiceCodegenTypes; import io.helidon.service.registry.Dependency; import io.helidon.service.registry.DependencyContext; import io.helidon.service.registry.GeneratedService; import io.helidon.service.registry.Service; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceInfo; import org.hamcrest.CoreMatchers; import org.hamcrest.collection.IsEmptyCollection; @@ -40,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.fail; class ServiceCodegenTypesTest { + @SuppressWarnings("removal") @Test void testTypes() { // it is really important to test ALL constants on the class, so let's use reflection @@ -67,11 +72,16 @@ void testTypes() { checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_CONTRACT", Service.Contract.class); checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_EXTERNAL_CONTRACTS", Service.ExternalContracts.class); checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_DESCRIPTOR", Service.Descriptor.class); - checkField(toCheck, checked, fields, "SERVICE_DESCRIPTOR", GeneratedService.Descriptor.class); checkField(toCheck, checked, fields, "SERVICE_DEPENDENCY", Dependency.class); checkField(toCheck, checked, fields, "SERVICE_DEPENDENCY_CONTEXT", DependencyContext.class); + checkField(toCheck, checked, fields, "SERVICE_DESCRIPTOR", ServiceDescriptor.class); - assertThat(toCheck, IsEmptyCollection.empty()); + checkField(toCheck, checked, fields, "BUILDER_BLUEPRINT", Prototype.Blueprint.class); + checkField(toCheck, checked, fields, "GENERATED_ANNOTATION", Generated.class); + + assertThat("If the collection is not empty, please add appropriate checkField line to this test", + toCheck, + IsEmptyCollection.empty()); } private void checkField(Set namesToCheck, diff --git a/service/tests/codegen/src/test/java/module-info.java b/service/tests/codegen/src/test/java/module-info.java index 49940eb0f05..ab0295c357e 100644 --- a/service/tests/codegen/src/test/java/module-info.java +++ b/service/tests/codegen/src/test/java/module-info.java @@ -18,11 +18,14 @@ exports io.helidon.service.tests.codegen; requires io.helidon.service.registry; + requires io.helidon.service.inject; + requires io.helidon.service.inject.api; requires io.helidon.service.codegen; requires io.helidon.config.metadata; requires hamcrest.all; requires org.junit.jupiter.api; + requires io.helidon.service.metadata; opens io.helidon.service.tests.codegen to org.junit.platform.commons; } \ No newline at end of file diff --git a/service/tests/inject/codegen/pom.xml b/service/tests/inject/codegen/pom.xml new file mode 100644 index 00000000000..6b9d124bafd --- /dev/null +++ b/service/tests/inject/codegen/pom.xml @@ -0,0 +1,64 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-codegen + Helidon Service Tests Inject Codegen + Tests for injection codegen + + + + io.helidon.service.inject + helidon-service-inject-codegen + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + 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 new file mode 100644 index 00000000000..c221515bca2 --- /dev/null +++ b/service/tests/inject/codegen/src/test/java/io/helidon/service/tests/inject/codegen/InjectCodegenTypesTest.java @@ -0,0 +1,159 @@ +/* + * 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.codegen; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.helidon.common.types.TypeName; +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.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionException; +import io.helidon.service.inject.api.InterceptionInvoker; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.Ip; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.inject.api.ServiceInstance; +import io.helidon.service.inject.codegen.InjectCodegenTypes; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.collection.IsEmptyCollection; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +class InjectCodegenTypesTest { + @Test + void testTypes() { + // it is really important to test ALL constants on the class, so let's use reflection + Field[] declaredFields = InjectCodegenTypes.class.getDeclaredFields(); + + Set toCheck = new HashSet<>(); + Set checked = new HashSet<>(); + Map fields = new HashMap<>(); + + for (Field declaredField : declaredFields) { + String name = declaredField.getName(); + + assertThat(name + " must be a TypeName", declaredField.getType(), CoreMatchers.sameInstance(TypeName.class)); + assertThat(name + " must be static", Modifier.isStatic(declaredField.getModifiers()), is(true)); + assertThat(name + " must be public", Modifier.isPublic(declaredField.getModifiers()), is(true)); + assertThat(name + " must be final", Modifier.isFinal(declaredField.getModifiers()), is(true)); + + toCheck.add(name); + fields.put(name, declaredField); + } + + // api.Injection.* + checkField(toCheck, checked, fields, "INJECTION_INJECT", Injection.Inject.class); + checkField(toCheck, checked, fields, "INJECTION_SINGLETON", Injection.Singleton.class); + checkField(toCheck, checked, fields, "INJECTION_NAMED", Injection.Named.class); + checkField(toCheck, checked, fields, "INJECTION_NAMED_BY_TYPE", Injection.NamedByType.class); + checkField(toCheck, checked, fields, "INJECTION_QUALIFIER", Injection.Qualifier.class); + checkField(toCheck, checked, fields, "INJECTION_DESCRIBE", Injection.Describe.class); + checkField(toCheck, checked, fields, "INJECTION_SCOPE", Injection.Scope.class); + checkField(toCheck, checked, fields, "INJECTION_PER_LOOKUP", Injection.PerLookup.class); + checkField(toCheck, checked, fields, "INJECTION_PER_INSTANCE", Injection.PerInstance.class); + checkField(toCheck, checked, fields, "INJECTION_RUN_LEVEL", Injection.RunLevel.class); + checkField(toCheck, checked, fields, "INJECTION_POINT_FACTORY", Injection.InjectionPointFactory.class); + 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); + + // api.Interception.* + checkField(toCheck, checked, fields, "INTERCEPTION_INTERCEPTED", Interception.Intercepted.class); + checkField(toCheck, checked, fields, "INTERCEPTION_DELEGATE", Interception.Delegate.class); + checkField(toCheck, checked, fields, "INTERCEPTION_EXTERNAL_DELEGATE", Interception.ExternalDelegate.class); + + // api.* except for interception types + checkField(toCheck, checked, fields, "INJECT_FACTORY_TYPE", FactoryType.class); + checkField(toCheck, checked, fields, "INJECT_QUALIFIER", Qualifier.class); + 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); + + // api.* interception types + checkField(toCheck, checked, fields, "INTERCEPT_EXCEPTION", InterceptionException.class); + checkField(toCheck, checked, fields, "INTERCEPT_METADATA", InterceptionMetadata.class); + checkField(toCheck, checked, fields, "INTERCEPT_INVOKER", InterceptionInvoker.class); + + // api.* event types + checkField(toCheck, checked, fields, "EVENT_OBSERVER", Event.Observer.class); + checkField(toCheck, checked, fields, "EVENT_OBSERVER_ASYNC", Event.AsyncObserver.class); + checkField(toCheck, checked, fields, "EVENT_EMITTER", Event.Emitter.class); + checkField(toCheck, checked, fields, "EVENT_MANAGER", EventManager.class); + + + // generated inject service types + checkField(toCheck, checked, fields, "INJECT_G_PER_INSTANCE_DESCRIPTOR", + GeneratedInjectService.PerInstanceDescriptor.class); + checkField(toCheck, checked, fields, "INJECT_G_QUALIFIED_FACTORY_DESCRIPTOR", + GeneratedInjectService.QualifiedFactoryDescriptor.class); + checkField(toCheck, checked, fields, "INJECT_G_SCOPE_HANDLER_DESCRIPTOR", + GeneratedInjectService.ScopeHandlerDescriptor.class); + checkField(toCheck, checked, fields, "INJECT_G_IP_SUPPORT", + GeneratedInjectService.IpSupport.class); + checkField(toCheck, checked, fields, "INJECT_G_EVENT_OBSERVER_REGISTRATION", + GeneratedInjectService.EventObserverRegistration.class); + + // generated interception types + checkField(toCheck, checked, fields, "INTERCEPT_G_WRAPPER_SUPPLIER_FACTORY", + GeneratedInjectService.SupplierFactoryInterceptionWrapper.class); + checkField(toCheck, checked, fields, "INTERCEPT_G_WRAPPER_SERVICES_FACTORY", + GeneratedInjectService.ServicesFactoryInterceptionWrapper.class); + checkField(toCheck, checked, fields, "INTERCEPT_G_WRAPPER_IP_FACTORY", + GeneratedInjectService.IpFactoryInterceptionWrapper.class); + checkField(toCheck, checked, fields, "INTERCEPT_G_WRAPPER_QUALIFIED_FACTORY", + GeneratedInjectService.QualifiedFactoryInterceptionWrapper.class); + + assertThat("If the collection is not empty, please add appropriate checkField line to this test", + toCheck, + IsEmptyCollection.empty()); + } + + private void checkField(Set namesToCheck, + Set checkedNames, + Map namesToFields, + String name, + Class expectedType) { + Field field = namesToFields.get(name); + assertThat("Field " + name + " does not exist in the class", field, notNullValue()); + try { + namesToCheck.remove(name); + if (checkedNames.add(name)) { + TypeName value = (TypeName) field.get(null); + assertThat("Field " + name, value.fqName(), is(expectedType.getCanonicalName())); + } else { + fail("Field " + name + " is checked more than once"); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/service/tests/inject/events/pom.xml b/service/tests/inject/events/pom.xml new file mode 100644 index 00000000000..966d87120c6 --- /dev/null +++ b/service/tests/inject/events/pom.xml @@ -0,0 +1,137 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-events + Helidon Service Tests Inject Events + Tests for injection events + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + io.helidon.config + helidon-config + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + diff --git a/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/AsyncEventTypes.java b/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/AsyncEventTypes.java new file mode 100644 index 00000000000..7e426061fa5 --- /dev/null +++ b/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/AsyncEventTypes.java @@ -0,0 +1,103 @@ +/* + * 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.events; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.helidon.service.inject.api.Event; +import io.helidon.service.inject.api.Injection; + +class AsyncEventTypes { + private AsyncEventTypes() { + } + + @Injection.Singleton + static class EventEmitter { + private final Event.Emitter event; + + @Injection.Inject + EventEmitter(Event.Emitter event) { + this.event = event; + } + + CompletionStage emit(EventObject eventObject) { + return this.event.emitAsync(eventObject); + } + } + + static class EventObject { + private final String message; + + EventObject(String message) { + this.message = message; + } + + String message() { + return message; + } + } + + @Injection.Singleton + static class SyncEventObserver { + private volatile String threadName; + private volatile EventObject eventObject; + private volatile CountDownLatch latch = new CountDownLatch(1); + + @Event.Observer + void event(EventObject eventObject) { + this.threadName = Thread.currentThread().getName(); + this.eventObject = eventObject; + latch.countDown(); + } + + EventObject eventObject() throws InterruptedException { + latch.await(10, TimeUnit.SECONDS); + latch = new CountDownLatch(1); + return eventObject; + } + + String threadName() { + return threadName; + } + } + + @Injection.Singleton + static class EventObserver { + private volatile String threadName; + private volatile EventObject eventObject; + private volatile CountDownLatch latch = new CountDownLatch(1); + + @Event.AsyncObserver + void event(EventObject eventObject) { + this.threadName = Thread.currentThread().getName(); + this.eventObject = eventObject; + latch.countDown(); + } + + EventObject eventObject() throws InterruptedException { + latch.await(10, TimeUnit.SECONDS); + latch = new CountDownLatch(1); + return eventObject; + } + + String threadName() { + return threadName; + } + } +} diff --git a/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/EventTypes.java b/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/EventTypes.java new file mode 100644 index 00000000000..89f3c2d5258 --- /dev/null +++ b/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/EventTypes.java @@ -0,0 +1,79 @@ +/* + * 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.events; + +import io.helidon.service.inject.api.Event; +import io.helidon.service.inject.api.Injection; + +class EventTypes { + private EventTypes() { + } + + @Injection.Singleton + static class EventEmitter { + private final Event.Emitter event; + + @Injection.Inject + EventEmitter(Event.Emitter event) { + this.event = event; + } + + void emit(EventObject eventObject) { + this.event.emit(eventObject); + } + } + + @Injection.Singleton + static class EventEmitter2 { + private final Event.Emitter event; + + @Injection.Inject + EventEmitter2(Event.Emitter event) { + this.event = event; + } + + void emit(EventObject eventObject) { + this.event.emit(eventObject); + } + } + + static class EventObject { + private final String message; + + EventObject(String message) { + this.message = message; + } + + String message() { + return message; + } + } + + @Injection.Singleton + static class EventObserver { + private volatile EventObject eventObject; + + @Event.Observer + void event(EventObject eventObject) { + this.eventObject = eventObject; + } + + EventObject eventObject() { + return eventObject; + } + } +} diff --git a/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/QualifiedEventTypes.java b/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/QualifiedEventTypes.java new file mode 100644 index 00000000000..d2f0e7d2a82 --- /dev/null +++ b/service/tests/inject/events/src/main/java/io/helidon/service/tests/inject/events/QualifiedEventTypes.java @@ -0,0 +1,85 @@ +/* + * 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.events; + +import io.helidon.service.inject.api.Event; +import io.helidon.service.inject.api.Injection; + +class QualifiedEventTypes { + private QualifiedEventTypes() { + } + + @Injection.Qualifier + @interface EventQualifier { + } + + @Injection.Singleton + static class EventEmitter { + private final Event.Emitter unqualifiedEvent; + private final Event.Emitter event; + + + @Injection.Inject + EventEmitter(@EventQualifier Event.Emitter event, + Event.Emitter unqualifiedEvent) { + this.event = event; + this.unqualifiedEvent = unqualifiedEvent; + } + + void emit(EventObject eventObject) { + this.event.emit(eventObject); + this.unqualifiedEvent.emit(eventObject); + } + } + + static class EventObject { + private final String message; + + EventObject(String message) { + this.message = message; + } + + String message() { + return message; + } + } + + @Injection.Singleton + static class EventObserver { + private volatile EventObject eventObject; + private volatile EventObject unqualifiedEventObject; + + @Event.Observer + @EventQualifier + void event(EventObject eventObject) { + this.eventObject = eventObject; + } + + @Event.Observer + void unqualifiedEvent(EventObject object) { + this.unqualifiedEventObject = object; + } + + EventObject eventObject() { + return eventObject; + } + + EventObject unqualifiedEventObject() { + return unqualifiedEventObject; + } + } +} diff --git a/service/tests/inject/events/src/main/java/module-info.java b/service/tests/inject/events/src/main/java/module-info.java new file mode 100644 index 00000000000..3d96521bd0e --- /dev/null +++ b/service/tests/inject/events/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +module io.helidon.service.tests.inject.events { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + requires io.helidon.service.inject; + requires io.helidon.http; + requires io.helidon.common.context; + requires java.logging; + + exports io.helidon.service.tests.inject.events; +} \ No newline at end of file diff --git a/service/tests/inject/events/src/test/java/io/helidon/service/tests/inject/events/EventTest.java b/service/tests/inject/events/src/test/java/io/helidon/service/tests/inject/events/EventTest.java new file mode 100644 index 00000000000..f2e16b49eaf --- /dev/null +++ b/service/tests/inject/events/src/test/java/io/helidon/service/tests/inject/events/EventTest.java @@ -0,0 +1,102 @@ +/* + * 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.events; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; + +class EventTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + static void initRegistry() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + static void tearDownRegistry() { + registryManager.shutdown(); + } + + @Test + void testEvent() { + var emitter = registry.get(EventTypes.EventEmitter.class); + emitter.emit(new EventTypes.EventObject("unit-test-event")); + + var eventObserver = registry.get(EventTypes.EventObserver.class); + var eventObject = eventObserver.eventObject(); + assertThat("Event should have been received in observer", eventObject, notNullValue()); + assertThat(eventObject.message(), is("unit-test-event")); + } + + @Test + void testQualifiedEvent() { + var emitter = registry.get(QualifiedEventTypes.EventEmitter.class); + emitter.emit(new QualifiedEventTypes.EventObject("unit-test-event")); + + var eventObserver = registry.get(QualifiedEventTypes.EventObserver.class); + var eventObject = eventObserver.eventObject(); + assertThat("Event should have been received in observer", eventObject, notNullValue()); + assertThat(eventObject.message(), is("unit-test-event")); + + eventObject = eventObserver.unqualifiedEventObject(); + assertThat("Event should have been received in observer", eventObject, notNullValue()); + assertThat(eventObject.message(), is("unit-test-event")); + } + + @Test + void testAsyncEvent() throws ExecutionException, InterruptedException, TimeoutException { + var emitter = registry.get(AsyncEventTypes.EventEmitter.class); + var future = emitter.emit(new AsyncEventTypes.EventObject("unit-test-event")); + var returnedObject = future.toCompletableFuture().get(10, TimeUnit.SECONDS); + assertThat(returnedObject.message(), is("unit-test-event")); + + var eventObserver = registry.get(AsyncEventTypes.EventObserver.class); + var eventObject = eventObserver.eventObject(); + assertThat("Event should have been received in observer", eventObject, notNullValue()); + assertThat(eventObject.message(), is("unit-test-event")); + // make sure it was really an asynchronous delivery + var threadName = eventObserver.threadName(); + assertThat(threadName, startsWith("inject-event-manager-")); + + var syncObserver = registry.get(AsyncEventTypes.SyncEventObserver.class); + var syncEventObject = syncObserver.eventObject(); + assertThat("Event should have been received in observer", syncEventObject, notNullValue()); + assertThat(syncEventObject, sameInstance(eventObject)); + // make sure it was really an asynchronous delivery + var syncThreadName = syncObserver.threadName(); + assertThat(syncThreadName, startsWith("inject-event-manager-")); + assertThat(syncThreadName, not(threadName)); + } +} diff --git a/service/tests/inject/inject/pom.xml b/service/tests/inject/inject/pom.xml new file mode 100644 index 00000000000..3d483dca7b4 --- /dev/null +++ b/service/tests/inject/inject/pom.xml @@ -0,0 +1,142 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject + Helidon Service Tests Inject + Tests for injection operations + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + io.helidon.config + helidon-config + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.logging + helidon-logging-jul + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/AContract.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/AContract.java new file mode 100644 index 00000000000..b1f944889dc --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/AContract.java @@ -0,0 +1,24 @@ +/* + * 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.tests.inject; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface AContract { + String message(); +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/AContractSupplier.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/AContractSupplier.java new file mode 100644 index 00000000000..42cca575fcf --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/AContractSupplier.java @@ -0,0 +1,29 @@ +/* + * 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.tests.inject; + +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class AContractSupplier implements Supplier { + @Override + public AContract get() { + return () -> "Hello!"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ContractOfNamed.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ContractOfNamed.java new file mode 100644 index 00000000000..4fb45b250da --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ContractOfNamed.java @@ -0,0 +1,24 @@ +/* + * 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.tests.inject; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractOfNamed { + String name(); +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ContractOfQualified.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ContractOfQualified.java new file mode 100644 index 00000000000..3f3f777ea30 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ContractOfQualified.java @@ -0,0 +1,24 @@ +/* + * 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.tests.inject; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractOfQualified { + String qualifier(); +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/DescribeTypes.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/DescribeTypes.java new file mode 100644 index 00000000000..5b831665ece --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/DescribeTypes.java @@ -0,0 +1,56 @@ +/* + * 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.api.Injection; +import io.helidon.service.registry.Service; + +final class DescribeTypes { + private DescribeTypes() { + } + + /** + * A greeting that needs to be described separately. + */ + @Injection.Describe + @Service.Contract + interface DescribedContract { + String sayHello(); + } + + /** + * A non-service implementation of the greeting. + * It is instantiated manually and passed to the registry manager config. + */ + static class DescribedContractImpl implements DescribedContract { + @Override + public String sayHello() { + return "Hello World!"; + } + } + + /** + * A singleton service that injects the described greeting. + * + * @param myContract myContract + */ + @Injection.Singleton + record DescribedReceiver(DescribedContract myContract) { + } + + +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/InnerTypes.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/InnerTypes.java new file mode 100644 index 00000000000..f2dc69e0eea --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/InnerTypes.java @@ -0,0 +1,41 @@ +/* + * 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.util.concurrent.atomic.AtomicInteger; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; + +/* +Show that we can declare services and contracts as inner classes + */ +class InnerTypes { + @Service.Contract + interface InnerContract { + } + + @Injection.Singleton + static class InnerService implements InnerContract { + static final AtomicInteger postConstructCount = new AtomicInteger(); + + @Service.PostConstruct + void postConstruct() { + postConstructCount.incrementAndGet(); + } + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/LifecycleReceiver.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/LifecycleReceiver.java new file mode 100644 index 00000000000..a3731da6ac2 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/LifecycleReceiver.java @@ -0,0 +1,47 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; + +@Injection.Singleton +class LifecycleReceiver { + private boolean postConstructCalled; + private boolean preDestroyCalled; + + LifecycleReceiver() { + } + + @Service.PostConstruct + void postConstruct() { + this.postConstructCalled = true; + } + + @Service.PreDestroy + void preDestroy() { + this.preDestroyCalled = true; + } + + boolean postConstructCalled() { + return postConstructCalled; + } + + boolean preDestroyCalled() { + return preDestroyCalled; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutableService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutableService.java new file mode 100644 index 00000000000..bd7f977ea1a --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutableService.java @@ -0,0 +1,55 @@ +/* + * 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.common.Weight; +import io.helidon.service.registry.Service; + +@Service.Provider +@Service.Contract +@Weight(5) +class MutableService { + private int counter; + + private int a; + private int b; + private int c; + + void mutateA() { + a = ++counter; + } + + void mutateB() { + b = ++counter; + } + + void mutateC() { + c = ++counter; + } + + int a() { + return a; + } + + int b() { + return b; + } + + int c() { + return c; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorA.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorA.java new file mode 100644 index 00000000000..711c3ddbcea --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorA.java @@ -0,0 +1,39 @@ +/* + * 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.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(10) +class MutatorA implements Supplier { + private final MutableService m; + + @Injection.Inject + MutatorA(MutableService m) { + this.m = m; + } + + @Override + public MutableService get() { + m.mutateA(); + return m; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorB.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorB.java new file mode 100644 index 00000000000..935afe7441e --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorB.java @@ -0,0 +1,39 @@ +/* + * 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.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(20) +class MutatorB implements Supplier { + private final MutableService m; + + @Injection.Inject + MutatorB(MutableService m) { + this.m = m; + } + + @Override + public MutableService get() { + m.mutateB(); + return m; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorC.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorC.java new file mode 100644 index 00000000000..6e53e52a5ba --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorC.java @@ -0,0 +1,39 @@ +/* + * 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.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(50) +class MutatorC implements Supplier { + private final MutableService m; + + @Injection.Inject + MutatorC(MutableService m) { + this.m = m; + } + + @Override + public MutableService get() { + m.mutateC(); + return m; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorReceiver.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorReceiver.java new file mode 100644 index 00000000000..db0ffb16d6e --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MutatorReceiver.java @@ -0,0 +1,35 @@ +/* + * 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.common.Weight; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(100) +class MutatorReceiver { + private final MutableService m; + + @Injection.Inject + MutatorReceiver(MutableService m) { + this.m = m; + } + + MutableService mutableService() { + return m; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyContract.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyContract.java new file mode 100644 index 00000000000..8627a5ed6bf --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyContract.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 io.helidon.service.tests.inject; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface MyContract { + String message(); +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService.java new file mode 100644 index 00000000000..e24e2e50dde --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService.java @@ -0,0 +1,33 @@ +/* + * 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.api.Injection; + +@Injection.Singleton +class MyService implements MyContract { + static int instances = 0; + + MyService() { + instances++; + } + + @Override + public String message() { + return "MyService"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService2.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService2.java new file mode 100644 index 00000000000..d5df2af2daf --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService2.java @@ -0,0 +1,46 @@ +/* + * 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.common.Weight; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(102) +class MyService2 implements MyContract { + static int instances; + + private final MyService service; + + @Injection.Inject + MyService2(MyService service) { + instances++; + this.service = service; + } + + MyService2(MyService service, boolean increaseInstances) { + this.service = service; + if (increaseInstances) { + instances++; + } + } + + @Override + public String message() { + return service.message() + ":MyService2"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService3.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService3.java new file mode 100644 index 00000000000..716c204f7b8 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/MyService3.java @@ -0,0 +1,34 @@ +/* + * 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.common.Weight; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(90) +class MyService3 extends MyService2 { + @Injection.Inject + MyService3(MyService service) { + super(service, false); + } + + @Override + public String message() { + return super.message() + ":MyService3"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NamedReceiver.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NamedReceiver.java new file mode 100644 index 00000000000..1400f7dcd49 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NamedReceiver.java @@ -0,0 +1,34 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class NamedReceiver { + private final ContractOfNamed named; + + @Injection.Inject + NamedReceiver(@Injection.Named("named") ContractOfNamed named) { + this.named = named; + } + + ContractOfNamed named() { + return named; + } + +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NamedService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NamedService.java new file mode 100644 index 00000000000..e61ea0ece86 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NamedService.java @@ -0,0 +1,28 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.Named("named") +@Injection.Singleton +class NamedService implements ContractOfNamed { + @Override + public String name() { + return "named"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NonSingletonService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NonSingletonService.java new file mode 100644 index 00000000000..049668e70ce --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/NonSingletonService.java @@ -0,0 +1,33 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.PerLookup +class NonSingletonService { + private final SingletonService singleton; + + @Injection.Inject + NonSingletonService(SingletonService singleton) { + this.singleton = singleton; + } + + SingletonService singletonService() { + return singleton; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ParameterizedTypes.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ParameterizedTypes.java new file mode 100644 index 00000000000..2a51afb485d --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ParameterizedTypes.java @@ -0,0 +1,99 @@ +/* + * 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.util.List; +import java.util.stream.Collectors; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; + +final class ParameterizedTypes { + private ParameterizedTypes() { + } + + @Service.Contract + interface Color { + String name(); + } + + @Service.Contract + interface Circle { + T color(); + } + + @Injection.Singleton + static class Blue implements Color { + @Override + public String name() { + return "blue"; + } + } + + @Injection.Singleton + static class Green implements Color { + @Override + public String name() { + return "green"; + } + } + + @Injection.Singleton + @Weight(Weighted.DEFAULT_WEIGHT + 10) + record BlueCircle(Blue color) implements Circle { + } + + @Injection.Singleton + record GreenCircle(Green color) implements Circle { + } + + @Injection.Singleton + static class ColorReceiver { + private final List> circles; + + @Injection.Inject + ColorReceiver(List> circles) { + this.circles = circles; + } + + String getString() { + return circles.stream() + .map(Circle::color) + .map(Color::name) + .collect(Collectors.joining("-")); + } + } + + @Injection.Singleton + static class ColorsReceiver { + private final Circle greenCircle; + private final Circle blueCircle; + + @Injection.Inject + ColorsReceiver(Circle greenCircle, + Circle blueCircle) { + this.greenCircle = greenCircle; + this.blueCircle = blueCircle; + } + + String getString() { + return greenCircle.color().name() + "-" + blueCircle.color().name(); + } + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ProviderReceiver.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ProviderReceiver.java new file mode 100644 index 00000000000..59af08bdca9 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ProviderReceiver.java @@ -0,0 +1,58 @@ +/* + * 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.tests.inject; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class ProviderReceiver { + private final Supplier provider; + private final Supplier> listOfProviders; + private final Supplier> optionalProvider; + private final AContract contract; + + @Injection.Inject + ProviderReceiver(Supplier provider, + Supplier> listOfProviders, + Supplier> optionalProvider, + AContract contract) { + this.provider = provider; + this.listOfProviders = listOfProviders; + this.optionalProvider = optionalProvider; + this.contract = contract; + } + + NonSingletonService nonSingletonService() { + return provider.get(); + } + + List listOfServices() { + return listOfProviders.get(); + } + + Optional optionalService() { + return optionalProvider.get(); + } + + AContract contract() { + return contract; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifiedReceiver.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifiedReceiver.java new file mode 100644 index 00000000000..d1707bb5ef6 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifiedReceiver.java @@ -0,0 +1,33 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class QualifiedReceiver { + private final ContractOfQualified qualified; + + @Injection.Inject + QualifiedReceiver(@QualifierAnnotation("qualified") ContractOfQualified qualified) { + this.qualified = qualified; + } + + ContractOfQualified qualified() { + return qualified; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifiedService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifiedService.java new file mode 100644 index 00000000000..158c197aec3 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifiedService.java @@ -0,0 +1,28 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@QualifierAnnotation("qualified") +@Injection.Singleton +class QualifiedService implements ContractOfQualified { + @Override + public String qualifier() { + return "qualified"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifierAnnotation.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifierAnnotation.java new file mode 100644 index 00000000000..49bc6d62692 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/QualifierAnnotation.java @@ -0,0 +1,40 @@ +/* + * 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.tests.inject; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import io.helidon.service.inject.api.Injection; + +/** + * Custom qualifier. + */ +@Injection.Qualifier +@Documented +@Retention(RetentionPolicy.CLASS) +@interface QualifierAnnotation { + + /** + * Testing. + * + * @return for testing + */ + String value() default ""; + +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ServiceSupplier.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ServiceSupplier.java new file mode 100644 index 00000000000..c90e43aec44 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/ServiceSupplier.java @@ -0,0 +1,33 @@ +/* + * 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.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import io.helidon.service.registry.Service; + +@Service.Provider +class ServiceSupplier implements Supplier { + private static final AtomicInteger COUNTER = new AtomicInteger(); + + @Override + public SuppliedContract get() { + int i = COUNTER.incrementAndGet(); + return () -> "Supplied:" + i; + } +} \ No newline at end of file diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/SingletonService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/SingletonService.java new file mode 100644 index 00000000000..874d90d0082 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/SingletonService.java @@ -0,0 +1,23 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class SingletonService { +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/SuppliedContract.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/SuppliedContract.java new file mode 100644 index 00000000000..aa6ac3d571b --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/SuppliedContract.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 io.helidon.service.tests.inject; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface SuppliedContract { + String message(); +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/UnQualifiedService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/UnQualifiedService.java new file mode 100644 index 00000000000..4e7b2401d5f --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/UnQualifiedService.java @@ -0,0 +1,27 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class UnQualifiedService implements ContractOfQualified { + @Override + public String qualifier() { + return "unqualified"; + } +} diff --git a/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/UnnamedService.java b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/UnnamedService.java new file mode 100644 index 00000000000..3c7ac74394d --- /dev/null +++ b/service/tests/inject/inject/src/main/java/io/helidon/service/tests/inject/UnnamedService.java @@ -0,0 +1,27 @@ +/* + * 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.tests.inject; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class UnnamedService implements ContractOfNamed { + @Override + public String name() { + return "unnamed"; + } +} diff --git a/service/tests/inject/inject/src/main/java/module-info.java b/service/tests/inject/inject/src/main/java/module-info.java new file mode 100644 index 00000000000..03cb3e4a205 --- /dev/null +++ b/service/tests/inject/inject/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +module io.helidon.service.tests.inject { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + requires io.helidon.service.inject; + requires io.helidon.http; + requires io.helidon.common.context; + + exports io.helidon.service.tests.inject; +} \ No newline at end of file diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/CyclicDependencyCoreTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/CyclicDependencyCoreTest.java new file mode 100644 index 00000000000..90682e16be7 --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/CyclicDependencyCoreTest.java @@ -0,0 +1,160 @@ +/* + * 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.util.List; +import java.util.Set; + +import io.helidon.common.GenericType; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.Ip; +import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.ServiceDescriptor; +import io.helidon.service.registry.ServiceRegistry; +import io.helidon.service.registry.ServiceRegistryConfig; +import io.helidon.service.registry.ServiceRegistryException; +import io.helidon.service.registry.ServiceRegistryManager; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CyclicDependencyCoreTest { + private static final TypeName SERVICE_1 = TypeName.create(Service1.class); + private static final TypeName SERVICE_2 = TypeName.create(Service2.class); + + @Test + public void testCyclicDependency() { + ServiceRegistryManager manager = ServiceRegistryManager.create(ServiceRegistryConfig.builder() + .discoverServices(false) + .addServiceDescriptor(new Descriptor1()) + .addServiceDescriptor(new Descriptor2()) + .build()); + + try { + ServiceRegistry registry = manager.registry(); + ServiceRegistryException sre = assertThrows(ServiceRegistryException.class, () -> registry.get(Service1.class)); + assertThat(sre.getMessage(), startsWith("Cyclic dependency")); + sre = assertThrows(ServiceRegistryException.class, () -> registry.get(Service2.class)); + assertThat(sre.getMessage(), startsWith("Cyclic dependency")); + } finally { + manager.shutdown(); + } + } + + private static class Service1 { + Service1(Service2 second) { + } + } + + private static class Service2 { + Service2(Service1 first) { + } + } + + private static class Descriptor1 implements ServiceDescriptor { + private static final TypeName TYPE = TypeName.create(Descriptor1.class); + + private static final Dependency DEP = Dependency.builder() + .contract(SERVICE_2) + .descriptor(TYPE) + .descriptorConstant("DEP") + .name("second") + .service(SERVICE_1) + .typeName(SERVICE_2) + .contractType(new GenericType() { }) + .build(); + + @Override + public Object instantiate(DependencyContext ctx) { + return new Service1(ctx.dependency(DEP)); + } + + @Override + public TypeName serviceType() { + return SERVICE_1; + } + + @Override + public TypeName descriptorType() { + return TYPE; + } + + @Override + public List dependencies() { + return List.of(DEP); + } + + @Override + public Set contracts() { + return Set.of(ResolvedType.create(SERVICE_1)); + } + } + + private static class Descriptor2 implements InjectServiceDescriptor { + private static final TypeName TYPE = TypeName.create(Descriptor2.class); + + private static final Ip DEP = Ip.builder() + .elementKind(ElementKind.CONSTRUCTOR) + .contract(SERVICE_1) + .descriptor(TYPE) + .descriptorConstant("DEP") + .name("first") + .service(SERVICE_2) + .typeName(SERVICE_1) + .contractType(new GenericType() { }) + .build(); + + @Override + public Object instantiate(DependencyContext ctx, InterceptionMetadata interceptionMetadata) { + return new Service2(ctx.dependency(DEP)); + } + + @Override + public TypeName serviceType() { + return SERVICE_1; + } + + @Override + public TypeName descriptorType() { + return TYPE; + } + + @Override + public TypeName scope() { + return Injection.Singleton.TYPE; + } + + @Override + public List dependencies() { + return List.of(DEP); + } + + @Override + public Set contracts() { + return Set.of(ResolvedType.create(SERVICE_2)); + } + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/CyclicDependencyInjectTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/CyclicDependencyInjectTest.java new file mode 100644 index 00000000000..9051f0bf4df --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/CyclicDependencyInjectTest.java @@ -0,0 +1,164 @@ +/* + * 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.util.List; +import java.util.Set; + +import io.helidon.common.GenericType; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.ResolvedType; +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.InjectServiceDescriptor; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.InterceptionMetadata; +import io.helidon.service.inject.api.Ip; +import io.helidon.service.registry.DependencyContext; +import io.helidon.service.registry.ServiceRegistry; +import io.helidon.service.registry.ServiceRegistryConfig; +import io.helidon.service.registry.ServiceRegistryException; +import io.helidon.service.registry.ServiceRegistryManager; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CyclicDependencyInjectTest { + private static final TypeName SERVICE_1 = TypeName.create(Service1.class); + private static final TypeName SERVICE_2 = TypeName.create(Service2.class); + + @Test + public void testCyclicDependency() { + ServiceRegistryManager manager = ServiceRegistryManager.create(ServiceRegistryConfig.builder() + .discoverServices(false) + .addServiceDescriptor(new Descriptor1()) + .addServiceDescriptor(new Descriptor2()) + .build()); + + try { + ServiceRegistry registry = manager.registry(); + ServiceRegistryException sre = assertThrows(ServiceRegistryException.class, () -> registry.get(Service1.class)); + assertThat(sre.getMessage(), startsWith("Cyclic dependency")); + sre = assertThrows(ServiceRegistryException.class, () -> registry.get(Service2.class)); + assertThat(sre.getMessage(), startsWith("Cyclic dependency")); + } finally { + manager.shutdown(); + } + } + + private static class Service1 { + Service1(Service2 second) { + } + } + + private static class Service2 { + Service2(Service1 first) { + } + } + + private static class Descriptor1 implements InjectServiceDescriptor { + private static final TypeName TYPE = TypeName.create(Descriptor1.class); + + private static final Ip DEP = Ip.builder() + .elementKind(ElementKind.CONSTRUCTOR) + .contract(SERVICE_2) + .descriptor(TYPE) + .descriptorConstant("DEP") + .name("second") + .service(SERVICE_1) + .typeName(SERVICE_2) + .contractType(new GenericType() { }) + .build(); + + @Override + public Object instantiate(DependencyContext ctx, InterceptionMetadata interceptionMetadata) { + return new Service1(ctx.dependency(DEP)); + } + + @Override + public TypeName serviceType() { + return SERVICE_1; + } + + @Override + public TypeName descriptorType() { + return TYPE; + } + + @Override + public List dependencies() { + return List.of(DEP); + } + + @Override + public Set contracts() { + return Set.of(ResolvedType.create(SERVICE_1)); + } + + @Override + public TypeName scope() { + return Injection.Singleton.TYPE; + } + } + + private static class Descriptor2 implements InjectServiceDescriptor { + private static final TypeName TYPE = TypeName.create(Descriptor2.class); + + private static final Ip DEP = Ip.builder() + .elementKind(ElementKind.CONSTRUCTOR) + .contract(SERVICE_1) + .descriptor(TYPE) + .descriptorConstant("DEP") + .name("first") + .service(SERVICE_2) + .typeName(SERVICE_1) + .contractType(new GenericType() { }) + .build(); + + @Override + public Object instantiate(DependencyContext ctx, InterceptionMetadata interceptionMetadata) { + return new Service2(ctx.dependency(DEP)); + } + + @Override + public TypeName serviceType() { + return SERVICE_1; + } + + @Override + public TypeName descriptorType() { + return TYPE; + } + + @Override + public List dependencies() { + return List.of(DEP); + } + + @Override + public Set contracts() { + return Set.of(ResolvedType.create(SERVICE_2)); + } + + @Override + public TypeName scope() { + return Injection.Singleton.TYPE; + } + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/DescribeTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/DescribeTest.java new file mode 100644 index 00000000000..7813254b49d --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/DescribeTest.java @@ -0,0 +1,66 @@ +/* + * 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.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +class DescribeTest { + private static final DescribeTypes.DescribedContract INSTANCE = new DescribeTypes.DescribedContractImpl(); + + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + public static void initRegistry() { + var injectConfig = InjectConfig.builder() + .putContractInstance(DescribeTypes.DescribedContract.class, INSTANCE) + .addServiceDescriptor(DescribeTypes_DescribedReceiver__ServiceDescriptor.INSTANCE) + .discoverServices(false) + .discoverServicesFromServiceLoader(false) + .build(); + registryManager = InjectRegistryManager.create(injectConfig); + registry = registryManager.registry(); + } + + @AfterAll + public static void tearDownRegistry() { + registryManager.shutdown(); + } + + @Test + void testContractInstance() { + var myContract = registry.get(DescribeTypes.DescribedContract.class); + + assertThat(myContract, sameInstance(INSTANCE)); + } + + @Test + void testReceiver() { + var receiver = registry.get(DescribeTypes.DescribedReceiver.class); + + assertThat(receiver.myContract(), sameInstance(INSTANCE)); + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectRegistryConfigTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectRegistryConfigTest.java new file mode 100644 index 00000000000..9dfe1bf53a6 --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectRegistryConfigTest.java @@ -0,0 +1,88 @@ +/* + * 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.util.List; +import java.util.Map; + +import io.helidon.common.configurable.LruCache; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.service.inject.InjectConfig; +import io.helidon.service.inject.api.Activator; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +public class InjectRegistryConfigTest { + @Test + void testDefaults() { + InjectConfig cfg = InjectConfig.create(); + // service registry config + assertThat(cfg.config(), is(optionalEmpty())); + assertThat(cfg.discoverServices(), is(true)); + assertThat(cfg.serviceDescriptors(), is(empty())); + assertThat(cfg.serviceInstances().size(), is(0)); + // injection specific config + assertThat(cfg.interceptionEnabled(), is(true)); + assertThat(cfg.limitRuntimePhase(), is(Activator.Phase.ACTIVE)); + assertThat(cfg.lookupCacheEnabled(), is(false)); + assertThat(cfg.lookupCache(), is(optionalEmpty())); + assertThat(cfg.useBinding(), is(true)); + } + + @Test + void testFromConfig() { + Config config = Config.builder( + ConfigSources.create( + Map.of("inject.discover-services", "false", + "inject.interception-enabled", "false", + "inject.limit-runtime-phase", "CONSTRUCTING", + "inject.lookup-cache-enabled", "true", + "inject.lookup-cache.capacity", "200", + "inject.use-binding", "false" + ), "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + Config injectConfig = config.get("inject"); + InjectConfig cfg = InjectConfig.create(injectConfig); + + // service registry config + assertThat(cfg.config(), optionalValue(sameInstance(injectConfig))); + assertThat(cfg.discoverServices(), is(false)); + assertThat(cfg.serviceDescriptors(), is(empty())); + assertThat(cfg.serviceInstances().size(), is(0)); + // injection specific config + assertThat(cfg.interceptionEnabled(), is(false)); + assertThat(cfg.limitRuntimePhase(), is(Activator.Phase.CONSTRUCTING)); + assertThat(cfg.lookupCacheEnabled(), is(true)); + assertThat(cfg.lookupCache(), is(optionalPresent())); + LruCache> cache = cfg.lookupCache().get(); + assertThat(cache.capacity(), is(200)); + assertThat(cfg.useBinding(), is(false)); + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectRegistryTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectRegistryTest.java new file mode 100644 index 00000000000..2376988cd59 --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectRegistryTest.java @@ -0,0 +1,87 @@ +/* + * 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.util.List; + +import io.helidon.logging.common.LogConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.registry.ServiceRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +public class InjectRegistryTest { + static { + LogConfig.initClass(); + } + private static InjectRegistryManager registryManager; + private static ServiceRegistry registry; + + @BeforeAll + public static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + public static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + registryManager = null; + registry = null; + } + + @Test + public void testRegistryGet() { + MyContract myContract = registry.get(MyContract.class); + // higher weight + assertThat(myContract, instanceOf(MyService2.class)); + + assertThat(MyService2.instances, is(1)); + assertThat(MyService.instances, is(1)); + } + + @Test + public void testRegistryFirst() { + MyContract myContract = registry.first(MyContract.class).get(); + // higher weight + assertThat(myContract, instanceOf(MyService2.class)); + assertThat(MyService2.instances, is(1)); + assertThat(MyService.instances, is(1)); + } + + @Test + public void testRegistryAll() { + List myContracts = registry.all(MyContract.class); + assertThat(myContracts, hasSize(3)); + // higher weight + assertThat(myContracts.get(0), instanceOf(MyService2.class)); + assertThat(myContracts.get(1), instanceOf(MyService.class)); + assertThat(myContracts.get(2), instanceOf(MyService3.class)); + + assertThat(MyService2.instances, is(1)); + assertThat(MyService.instances, is(1)); + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectionTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectionTest.java new file mode 100644 index 00000000000..1046b67b61b --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/InjectionTest.java @@ -0,0 +1,174 @@ +/* + * 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.tests.inject; + +import java.util.function.Supplier; + +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +/* + All code generation should be done as part of main source processing, we can just start service registry and test + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class InjectionTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + private static LifecycleReceiver lifecycleReceiver; + + @BeforeAll + public static void initRegistry() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + public static void tearDownRegistry() { + registryManager.shutdown(); + if (lifecycleReceiver != null) { + assertThat("Pre destroy of a singleton should have been called", lifecycleReceiver.preDestroyCalled(), is(true)); + } + } + + @Test + @Order(0) + public void testSingleton() { + Supplier provider = registry.supply(SingletonService.class); + + assertThat(provider, notNullValue()); + + SingletonService first = provider.get(); + assertThat(first, notNullValue()); + + SingletonService second = provider.get(); + // singleton should always yield the same instance + assertThat(first, sameInstance(second)); + } + + @Test + @Order(1) + public void testLifecycle() { + Supplier provider = registry.supply(LifecycleReceiver.class); + + assertThat(provider, notNullValue()); + + lifecycleReceiver = provider.get(); + assertThat(lifecycleReceiver.postConstructCalled(), is(true)); + } + + @Test + @Order(2) + public void testNonSingleton() { + Supplier provider = registry.supply(NonSingletonService.class); + + assertThat(provider, notNullValue()); + + NonSingletonService first = provider.get(); + assertThat(first, notNullValue()); + + NonSingletonService second = provider.get(); + // non-singleton should always yield different instance + assertThat(first, not(sameInstance(second))); + + SingletonService firstSingleton = first.singletonService(); + SingletonService secondSingleton = second.singletonService(); + // singleton should always yield the same instance + assertThat(firstSingleton, sameInstance(secondSingleton)); + } + + @Test + @Order(3) + public void testNamed() { + Supplier provider = registry.supply(NamedReceiver.class); + + assertThat(provider, notNullValue()); + + NamedReceiver instance = provider.get(); + assertThat(instance.named(), notNullValue()); + assertThat(instance.named().name(), is("named")); + } + + @Test + @Order(4) + public void testQualified() { + Supplier provider = registry.supply(QualifiedReceiver.class); + + assertThat(provider, notNullValue()); + + QualifiedReceiver instance = provider.get(); + assertThat(instance.qualified(), notNullValue()); + assertThat(instance.qualified().qualifier(), is("qualified")); + } + + @Test + @Order(5) + public void testProvider() { + Supplier provider = registry.supply(ProviderReceiver.class); + + assertThat(provider, notNullValue()); + + ProviderReceiver instance = provider.get(); + assertThat(instance.nonSingletonService(), notNullValue()); + assertThat(instance.listOfServices(), not(empty())); + assertThat(instance.optionalService(), optionalPresent()); + assertThat(instance.contract(), notNullValue()); + + NonSingletonService first = instance.nonSingletonService(); + NonSingletonService second = instance.nonSingletonService(); + assertThat(first, not(sameInstance(second))); + } + + @Test + @Order(6) + public void testInnerTypes() { + InnerTypes.InnerContract innerContract = registry.get(InnerTypes.InnerContract.class); + assertThat(innerContract, instanceOf(InnerTypes.InnerService.class)); + assertThat(InnerTypes.InnerService.postConstructCount.get(), is(1)); + } + + @Test + @Order(7) + public void testSupplier() { + Supplier supplier = registry.supply(SuppliedContract.class); + + SuppliedContract first = supplier.get(); + // sanity check we use the counter per instance, not per call + assertThat(first.message(), is("Supplied:1")); + assertThat(first.message(), is("Supplied:1")); + + // second instance should be a new one + SuppliedContract second = supplier.get(); + assertThat(second, not(sameInstance(first))); + assertThat(second.message(), is("Supplied:2")); + } +} \ No newline at end of file diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/MutationTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/MutationTest.java new file mode 100644 index 00000000000..14ac1e5bce0 --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/MutationTest.java @@ -0,0 +1,57 @@ +/* + * 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.InjectRegistryManager; +import io.helidon.service.registry.ServiceRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MutationTest { + private static InjectRegistryManager registryManager; + private static ServiceRegistry registry; + + @BeforeAll + public static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + public static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + registryManager = null; + registry = null; + } + + @Test + public void testMutatorServices() { + MutatorReceiver m = registry.get(MutatorReceiver.class); + MutableService mutableService = m.mutableService(); + + assertThat(mutableService.a(), is(1)); + assertThat(mutableService.b(), is(2)); + assertThat(mutableService.c(), is(3)); + } +} diff --git a/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/ParameterizedTypesTest.java b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/ParameterizedTypesTest.java new file mode 100644 index 00000000000..9a87bc1f9e8 --- /dev/null +++ b/service/tests/inject/inject/src/test/java/io/helidon/service/tests/inject/ParameterizedTypesTest.java @@ -0,0 +1,70 @@ +/* + * 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.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ParameterizedTypesTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + public static void initRegistry() { + var injectConfig = InjectConfig.builder() + .addServiceDescriptor(ParameterizedTypes_Blue__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ParameterizedTypes_Green__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ParameterizedTypes_BlueCircle__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ParameterizedTypes_GreenCircle__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ParameterizedTypes_ColorReceiver__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ParameterizedTypes_ColorsReceiver__ServiceDescriptor.INSTANCE) + .discoverServices(false) + .discoverServicesFromServiceLoader(false) + .build(); + registryManager = InjectRegistryManager.create(injectConfig); + registry = registryManager.registry(); + } + + @AfterAll + public static void tearDownRegistry() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void testColorReceiver() { + var receiver = registry.get(ParameterizedTypes.ColorReceiver.class); + + assertThat(receiver.getString(), is("blue-green")); + } + + @Test + void testColorsReceiver() { + var receiver = registry.get(ParameterizedTypes.ColorsReceiver.class); + + assertThat(receiver.getString(), is("green-blue")); + } +} diff --git a/service/tests/inject/inject/src/test/resources/logging.properties b/service/tests/inject/inject/src/test/resources/logging.properties new file mode 100644 index 00000000000..5190b108e32 --- /dev/null +++ b/service/tests/inject/inject/src/test/resources/logging.properties @@ -0,0 +1,22 @@ +# +# 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.service.inject.LookupTrace.level=ALL diff --git a/service/tests/inject/interception/pom.xml b/service/tests/inject/interception/pom.xml new file mode 100644 index 00000000000..ec48f21a2ce --- /dev/null +++ b/service/tests/inject/interception/pom.xml @@ -0,0 +1,137 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-interception + Helidon Service Tests Inject Interception + Tests for injection operations + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.logging + helidon-logging-jul + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/AbstractClassTypes.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/AbstractClassTypes.java new file mode 100644 index 00000000000..ae0c8da78ae --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/AbstractClassTypes.java @@ -0,0 +1,87 @@ +/* + * 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.interception; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; +import io.helidon.service.registry.Service; + +/** + * An example that illustrates usages of {@link io.helidon.service.inject.api.Interception.Interceptor}. + */ +class AbstractClassTypes { + + /** + * An annotation to mark methods to be intercepted. + */ + @Interception.Intercepted + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @interface Traced { + } + + /** + * An abstract class contract with an intercepted method. + */ + @Service.Contract + static abstract class MyAbstractClassContract { + + @Traced + abstract String sayHello(String name); + + @Traced + String sayHelloDirect(String name) { + return "Hello %s!".formatted(name); + } + } + + /** + * An interceptor implementation that supports {@link AbstractClassTypes.Traced}. + */ + @Injection.Singleton + @Injection.NamedByType(Traced.class) + static class MyServiceInterceptor implements Interception.Interceptor { + static final List INVOKED = new ArrayList<>(); + + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + INVOKED.add("%s.%s: %s".formatted( + ctx.serviceInfo().serviceType().declaredName(), + ctx.elementInfo().elementName(), + Arrays.asList(args))); + return chain.proceed(args); + } + } + + /** + * A service that extends an abstract class contract with an intercepted method. + */ + @Injection.Singleton + static class MyAbstractClassContractImpl extends MyAbstractClassContract { + + @Override + public String sayHello(String name) { + return "Hello %s!".formatted(name); + } + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Construct.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Construct.java new file mode 100644 index 00000000000..af1fedd51e6 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Construct.java @@ -0,0 +1,30 @@ +/* + * 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.interception; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.inject.api.Interception; + +@Interception.Intercepted +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.CONSTRUCTOR) +@interface Construct { +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ConstructorInterceptor.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ConstructorInterceptor.java new file mode 100644 index 00000000000..7a7c033467a --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ConstructorInterceptor.java @@ -0,0 +1,37 @@ +/* + * 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.interception; + +import java.util.HashSet; +import java.util.Set; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; + +@Injection.Singleton +@Injection.NamedByType(Construct.class) +class ConstructorInterceptor implements Interception.Interceptor { + static final Set CONSTRUCTED = new HashSet<>(); + + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + CONSTRUCTED.add(ctx.serviceInfo().serviceType()); + return chain.proceed(args); + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedClass.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedClass.java new file mode 100644 index 00000000000..30d88d36201 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedClass.java @@ -0,0 +1,41 @@ +/* + * 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.interception; + +import io.helidon.service.inject.api.Interception; +import io.helidon.service.registry.Service; + +@Service.Contract +@Interception.Delegate +class DelegatedClass { + private boolean throwException = false; + + @Modify + @Repeat + @Return + protected String intercepted(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + return message; + } + + void throwException(boolean throwException) { + this.throwException = throwException; + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedClassServiceProvider.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedClassServiceProvider.java new file mode 100644 index 00000000000..1562bad45dc --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedClassServiceProvider.java @@ -0,0 +1,29 @@ +/* + * 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.interception; + +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class DelegatedClassServiceProvider implements Supplier { + @Override + public DelegatedClass get() { + return new DelegatedClass(); + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedContract.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedContract.java new file mode 100644 index 00000000000..bc29178823a --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedContract.java @@ -0,0 +1,31 @@ +/* + * 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.interception; + +import io.helidon.service.inject.api.Interception; +import io.helidon.service.registry.Service; + +@Service.Contract +@Interception.Delegate +interface DelegatedContract { + @Modify + @Repeat + @Return + String intercepted(String message, boolean modify, boolean repeat, boolean doReturn); + + void throwException(boolean throwException); +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedServiceProvider.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedServiceProvider.java new file mode 100644 index 00000000000..14e583e660a --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/DelegatedServiceProvider.java @@ -0,0 +1,47 @@ +/* + * 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.interception; + +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class DelegatedServiceProvider implements Supplier { + @Override + public DelegatedContract get() { + return new DelegatedImpl(); + } + + private static class DelegatedImpl implements DelegatedContract { + private boolean throwException = false; + + @Override + public String intercepted(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + return message; + } + + @Override + public void throwException(boolean throwException) { + this.throwException = throwException; + } + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Invocation.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Invocation.java new file mode 100644 index 00000000000..9a5af3c5803 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Invocation.java @@ -0,0 +1,26 @@ +/* + * 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.interception; + +import java.util.Arrays; + +record Invocation(String methodName, Object[] args) { + @Override + public String toString() { + return methodName + "(" + Arrays.toString(args) + ")"; + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Modify.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Modify.java new file mode 100644 index 00000000000..ae05f747962 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Modify.java @@ -0,0 +1,37 @@ +/* + * 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.tests.inject.interception; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.inject.api.Interception; + +/** + * Modify call. + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Interception.Intercepted +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Inherited +@interface Modify { +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ModifyingInterceptor.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ModifyingInterceptor.java new file mode 100644 index 00000000000..803b84d1029 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ModifyingInterceptor.java @@ -0,0 +1,56 @@ +/* + * 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.tests.inject.interception; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; + +@Injection.NamedByType(Modify.class) +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 50) +class ModifyingInterceptor implements Interception.Interceptor { + private static final AtomicReference LAST_CALL = new AtomicReference<>(); + + static Invocation lastCall() { + return LAST_CALL.getAndSet(null); + } + + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + LAST_CALL.set(new Invocation(ctx.elementInfo().elementName(), Arrays.copyOf(args, args.length))); + if (args.length < 2) { + // safeguard + return chain.proceed(args); + } + System.out.println("Modify"); + // args: + // 0: String message + // 1: Boolean modify + // 2: Boolean repeat + // 3: Boolean return + if ((Boolean) args[1]) { + args[0] = "mod_" + args[0]; + } + return chain.proceed(args); + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/OtherContract.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/OtherContract.java new file mode 100644 index 00000000000..9feaa7c72fb --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/OtherContract.java @@ -0,0 +1,35 @@ +/* + * 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.tests.inject.interception; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface OtherContract { + @Modify + @Repeat + @Return + String intercepted(String message, boolean modify, boolean repeat, boolean doReturn); + + @Return + String interceptedSubset(String message, boolean modify, boolean repeat, boolean doReturn); + + String notIntercepted(String message, boolean modify, boolean repeat, boolean doReturn); + + void throwException(boolean throwException); + +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Repeat.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Repeat.java new file mode 100644 index 00000000000..089f3b7a192 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Repeat.java @@ -0,0 +1,38 @@ +/* + * 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.tests.inject.interception; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.inject.api.Interception; + +/** + * Repeat the call twice. + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Interception.Intercepted +@Target(ElementType.METHOD) +@Inherited +@interface Repeat { + +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/RepeatingInterceptor.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/RepeatingInterceptor.java new file mode 100644 index 00000000000..2c9bfa5d89b --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/RepeatingInterceptor.java @@ -0,0 +1,65 @@ +/* + * 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.tests.inject.interception; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; +import io.helidon.service.inject.api.InterceptionException; + +@Injection.NamedByType(Repeat.class) +@Injection.Singleton +class RepeatingInterceptor implements Interception.Interceptor { + private static final AtomicReference LAST_CALL = new AtomicReference<>(); + + static Invocation lastCall() { + return LAST_CALL.getAndSet(null); + } + + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + LAST_CALL.set(new Invocation(ctx.elementInfo().elementName(), Arrays.copyOf(args, args.length))); + if (args.length < 3) { + // safeguard + return chain.proceed(args); + } + System.out.println("Repeat"); + // args: + // 0: String message + // 1: Boolean modify + // 2: Boolean repeat + // 3: Boolean return + if ((Boolean) args[2]) { + try { + chain.proceed(args); + } catch (Exception e) { + System.out.println("exception 1: " + e.getClass().getName() + ": " + e.getMessage()); + } + } + try { + return chain.proceed(args); + } catch (InterceptionException e) { + throw e; + } catch (Exception e) { + System.out.println("exception 2: " + e.getClass().getName() + ": " + e.getMessage()); + return null; + } + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Return.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Return.java new file mode 100644 index 00000000000..944aec677d4 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/Return.java @@ -0,0 +1,38 @@ +/* + * 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.tests.inject.interception; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.inject.api.Interception; + +/** + * Return an explicit value (do not call target). + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Interception.Intercepted +@Target(ElementType.METHOD) +@Inherited +@interface Return { + +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ReturningInterceptor.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ReturningInterceptor.java new file mode 100644 index 00000000000..9e522b9ce22 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/ReturningInterceptor.java @@ -0,0 +1,57 @@ +/* + * 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.tests.inject.interception; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Interception; +import io.helidon.service.inject.api.InterceptionContext; + +@Injection.NamedByType(Return.class) +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 100) +class ReturningInterceptor implements Interception.Interceptor { + private static final AtomicReference LAST_CALL = new AtomicReference<>(); + + static Invocation lastCall() { + return LAST_CALL.getAndSet(null); + } + + @SuppressWarnings("unchecked") + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + LAST_CALL.set(new Invocation(ctx.elementInfo().elementName(), Arrays.copyOf(args, args.length))); + if (args.length < 4) { + // safeguard + return chain.proceed(args); + } + System.out.println("Return"); + // args: + // 0: String message + // 1: Boolean modify + // 2: Boolean repeat + // 3: Boolean return + if ((Boolean) args[3]) { + return (V) "fixed_answer"; + } + return chain.proceed(args); + } +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheOtherService.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheOtherService.java new file mode 100644 index 00000000000..88b974707a5 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheOtherService.java @@ -0,0 +1,67 @@ +/* + * 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.tests.inject.interception; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class TheOtherService implements OtherContract { + private boolean throwException; + + @Modify + @Construct + TheOtherService() { + } + + @Override + public void throwException(boolean throwException) { + this.throwException = throwException; + } + + @Override + public String intercepted(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + + return message; + } + + // one interceptor on interface, one on implementation + @Repeat + @Override + public String interceptedSubset(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + + return message; + } + + @Override + public String notIntercepted(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + + return message; + } + +} diff --git a/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheService.java b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheService.java new file mode 100644 index 00000000000..8b5144df941 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheService.java @@ -0,0 +1,70 @@ +/* + * 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.tests.inject.interception; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class TheService { + private boolean throwException; + + @Modify + TheService() { + } + + void throwException(boolean throwException) { + this.throwException = throwException; + } + + // args: + // 0: String message + // 1: Boolean modify + // 2: Boolean repeat + // 3: Boolean return + @Modify + @Repeat + @Return + String intercepted(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + + return message; + } + + @Repeat + @Return + String interceptedSubset(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + + return message; + } + + String notIntercepted(String message, boolean modify, boolean repeat, boolean doReturn) { + if (throwException) { + throwException = false; + throw new RuntimeException("forced"); + } + + return message; + } + +} diff --git a/service/tests/inject/interception/src/main/java/module-info.java b/service/tests/inject/interception/src/main/java/module-info.java new file mode 100644 index 00000000000..cce16042ec4 --- /dev/null +++ b/service/tests/inject/interception/src/main/java/module-info.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. + */ + +module io.helidon.service.tests.inject.interception { + requires io.helidon.service.inject.api; + requires io.helidon.service.inject; + requires io.helidon.http; + requires io.helidon.common.context; + + exports io.helidon.service.tests.inject.interception; +} \ No newline at end of file diff --git a/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/AbstractClassInterceptionTest.java b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/AbstractClassInterceptionTest.java new file mode 100644 index 00000000000..ceb30ada7cd --- /dev/null +++ b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/AbstractClassInterceptionTest.java @@ -0,0 +1,66 @@ +/* + * 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.interception; + +import java.util.List; + +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.tests.inject.interception.AbstractClassTypes.MyAbstractClassContract; +import io.helidon.service.tests.inject.interception.AbstractClassTypes.MyAbstractClassContractImpl; +import io.helidon.service.tests.inject.interception.AbstractClassTypes.MyServiceInterceptor; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +// test interceptor annotated on abstract method of an abstract class +class AbstractClassInterceptionTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void testInterceptor() { + MyServiceInterceptor.INVOKED.clear(); + + var myAbstractClassContract = registry.get(MyAbstractClassContract.class); + assertThat(myAbstractClassContract.sayHello("Jessica"), is("Hello Jessica!")); + assertThat(myAbstractClassContract.sayHello("Juliet"), is("Hello Juliet!")); + assertThat(myAbstractClassContract.sayHelloDirect("John"), is("Hello John!")); + + assertThat(MyServiceInterceptor.INVOKED, is(List.of( + "%s.sayHello: [Jessica]".formatted(MyAbstractClassContractImpl.class.getName()), + "%s.sayHello: [Juliet]".formatted(MyAbstractClassContractImpl.class.getName()), + "%s.sayHelloDirect: [John]".formatted(MyAbstractClassContractImpl.class.getName())))); + } +} diff --git a/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/DelegatingInterceptionTest.java b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/DelegatingInterceptionTest.java new file mode 100644 index 00000000000..a1ca9b9cae4 --- /dev/null +++ b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/DelegatingInterceptionTest.java @@ -0,0 +1,182 @@ +/* + * 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.interception; + +import io.helidon.logging.common.LogConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InterceptionException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* +Order of interceptors: +Returning +Modifying +Repeating + */ +class DelegatingInterceptionTest { + static { + LogConfig.initClass(); + } + private static InjectRegistryManager registryManager; + private static DelegatedContract service; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + InjectRegistry registry = registryManager.registry(); + service = registry.get(DelegatedContract.class); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @BeforeEach + void beforeEach() { + // cleanup possible last calls from failed tests + ReturningInterceptor.lastCall(); + ModifyingInterceptor.lastCall(); + RepeatingInterceptor.lastCall(); + } + + @Test + void testReturn() { + String response = service.intercepted("hello", false, false, true); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should not be called as ReturningInterceptor should have returned", + modifying, + nullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should not be called as ReturningInterceptor should have returned", + repeating, + nullValue()) + ); + + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", false, false, true})) + ); + + assertThat(response, is("fixed_answer")); + } + + @Test + void testModify() { + String response = service.intercepted("hello", true, false, false); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should be called for method annotated with @Modify", + modifying, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Modifying last call", modifying.methodName(), is("intercepted")), + () -> assertThat("Modifying last call", modifying.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("intercepted")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"mod_hello", true, false, false})) + ); + + // and the message + assertThat(response, is("mod_hello")); + } + + /** + * Once the target is called once successfully it should not be allowed to repeat normally. + */ + @Test + void testRepeatWithNoExceptionThrownFromTarget() { + InterceptionException e = assertThrows(InterceptionException.class, + () -> service.intercepted("hello", false, true, false)); + assertThat(e.getMessage(), startsWith("Duplicate invocation, or unknown call type: java.lang.String intercepted")); + assertThat(e.targetWasCalled(), is(true)); + } + + @Test + void testRepeatWithExceptionThrownFromTarget() { + service.throwException(true); + + String response = service.intercepted("hello", false, true, false); + assertThat(response, equalTo("hello")); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should be called for method annotated with @Modify", + modifying, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", false, true, false})), + () -> assertThat("Modifying last call", modifying.methodName(), is("intercepted")), + () -> assertThat("Modifying last call", modifying.args(), is(new Object[] {"hello", false, true, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("intercepted")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"hello", false, true, false})) + ); + } + +} diff --git a/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/InterceptionTest.java b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/InterceptionTest.java new file mode 100644 index 00000000000..570fafa5a63 --- /dev/null +++ b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/InterceptionTest.java @@ -0,0 +1,248 @@ +/* + * 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.tests.inject.interception; + +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InterceptionException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* +Order of interceptors: +Returning +Modifying +Repeating + */ +class InterceptionTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + private static TheService service; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + service = registry.get(TheService.class); + + assertAll( + () -> assertThat("Interceptors should not be called for constructor - returning", + ReturningInterceptor.lastCall(), + nullValue()), + () -> assertThat("Interceptors should be called for constructor - modifying", + ModifyingInterceptor.lastCall(), + notNullValue()), + () -> assertThat("Interceptors should not be called for constructor - repeating", + RepeatingInterceptor.lastCall(), + nullValue()) + ); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @BeforeEach + void beforeEach() { + // cleanup possible last calls from failed tests + ReturningInterceptor.lastCall(); + ModifyingInterceptor.lastCall(); + RepeatingInterceptor.lastCall(); + service.throwException(false); + } + + @Test + void testNotIntercepted() { + String response = service.notIntercepted("hello", true, true, true); + + assertAll( + () -> assertThat("Interceptors should not be called for method not annotated", + ReturningInterceptor.lastCall(), + nullValue()), + () -> assertThat("Interceptors should not be called for method not annotated", + ModifyingInterceptor.lastCall(), + nullValue()), + () -> assertThat("Interceptors should not be called for method not annotated", + RepeatingInterceptor.lastCall(), + nullValue()) + ); + + assertThat(response, is("hello")); + } + + @Test + void testInterceptedSubset() { + // test that only the interceptors valid for annotations are invoked + String response = service.interceptedSubset("hello", true, false, false); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should not be called for method not annotated with @Modify", + modifying, + nullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("interceptedSubset")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("interceptedSubset")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"hello", true, false, false})) + ); + + // and finally the response string + assertThat(response, is("hello")); + } + + @Test + void testReturn() { + String response = service.intercepted("hello", false, false, true); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should not be called as ReturningInterceptor should have returned", + modifying, + nullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should not be called as ReturningInterceptor should have returned", + repeating, + nullValue()) + ); + + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", false, false, true})) + ); + + assertThat(response, is("fixed_answer")); + } + + @Test + void testModify() { + String response = service.intercepted("hello", true, false, false); + + // weight + 100 + Invocation returning = ReturningInterceptor.lastCall(); + // weight + 50 + Invocation modifying = ModifyingInterceptor.lastCall(); + // default weight + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should be called for method annotated with @Modify", + modifying, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Modifying last call", modifying.methodName(), is("intercepted")), + () -> assertThat("Modifying last call", modifying.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("intercepted")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"mod_hello", true, false, false})) + ); + + // and the message + assertThat(response, is("mod_hello")); + } + + /** + * Once the target is called once successfully it should not be allowed to repeat normally. + */ + @Test + void testRepeatWithNoExceptionThrownFromTarget() { + InterceptionException e = assertThrows(InterceptionException.class, + () -> service.intercepted("hello", false, true, false)); + assertThat(e.getMessage(), startsWith("Duplicate invocation, or unknown call type: java.lang.String intercepted")); + assertThat(e.targetWasCalled(), is(true)); + } + + @Test + void testRepeatWithExceptionThrownFromTarget() { + service.throwException(true); + + String response = service.intercepted("hello", false, true, false); + assertThat(response, equalTo("hello")); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should be called for method annotated with @Modify", + modifying, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", false, true, false})), + () -> assertThat("Modifying last call", modifying.methodName(), is("intercepted")), + () -> assertThat("Modifying last call", modifying.args(), is(new Object[] {"hello", false, true, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("intercepted")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"hello", false, true, false})) + ); + } + +} diff --git a/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/InterfaceInterceptionTest.java b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/InterfaceInterceptionTest.java new file mode 100644 index 00000000000..92d34ffc409 --- /dev/null +++ b/service/tests/inject/interception/src/test/java/io/helidon/service/tests/inject/interception/InterfaceInterceptionTest.java @@ -0,0 +1,248 @@ +/* + * 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.tests.inject.interception; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InterceptionException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* +Order of interceptors: +Returning +Modifying +Repeating + */ +class InterfaceInterceptionTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + private static OtherContract service; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + service = registry.get(OtherContract.class); + + assertAll( + () -> assertThat("Interceptors should not be called for constructor - returning", + ReturningInterceptor.lastCall(), + nullValue()), + () -> assertThat("Interceptors should be called for constructor - modifying", + ModifyingInterceptor.lastCall(), + notNullValue()), + () -> assertThat("Interceptors should not be called for constructor - repeating", + RepeatingInterceptor.lastCall(), + nullValue()) + ); + + assertThat(ConstructorInterceptor.CONSTRUCTED, hasItems(TypeName.create(TheOtherService.class))); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @BeforeEach + void beforeEach() { + // cleanup possible last calls from failed tests + ReturningInterceptor.lastCall(); + ModifyingInterceptor.lastCall(); + RepeatingInterceptor.lastCall(); + } + + @Test + void testNotIntercepted() { + String response = service.notIntercepted("hello", true, true, true); + + assertAll( + () -> assertThat("Interceptors should not be called for method not annotated", + ReturningInterceptor.lastCall(), + nullValue()), + () -> assertThat("Interceptors should not be called for method not annotated", + ModifyingInterceptor.lastCall(), + nullValue()), + () -> assertThat("Interceptors should not be called for method not annotated", + RepeatingInterceptor.lastCall(), + nullValue()) + ); + + assertThat(response, is("hello")); + } + + @Test + void testInterceptedSubset() { + // test that only the interceptors valid for annotations are invoked + String response = service.interceptedSubset("hello", true, false, false); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should not be called for method not annotated with @Modify", + modifying, + nullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("interceptedSubset")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("interceptedSubset")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"hello", true, false, false})) + ); + + // and finally the response string + assertThat(response, is("hello")); + } + + @Test + void testReturn() { + String response = service.intercepted("hello", false, false, true); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should not be called as ReturningInterceptor should have returned", + modifying, + nullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should not be called as ReturningInterceptor should have returned", + repeating, + nullValue()) + ); + + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", false, false, true})) + ); + + assertThat(response, is("fixed_answer")); + } + + @Test + void testModify() { + String response = service.intercepted("hello", true, false, false); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should be called for method annotated with @Modify", + modifying, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Modifying last call", modifying.methodName(), is("intercepted")), + () -> assertThat("Modifying last call", modifying.args(), is(new Object[] {"hello", true, false, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("intercepted")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"mod_hello", true, false, false})) + ); + + // and the message + assertThat(response, is("mod_hello")); + } + + /** + * Once the target is called once successfully it should not be allowed to repeat normally. + */ + @Test + void testRepeatWithNoExceptionThrownFromTarget() { + InterceptionException e = assertThrows(InterceptionException.class, + () -> service.intercepted("hello", false, true, false)); + assertThat(e.getMessage(), startsWith("Duplicate invocation, or unknown call type: java.lang.String intercepted")); + assertThat(e.targetWasCalled(), is(true)); + } + + @Test + void testRepeatWithExceptionThrownFromTarget() { + service.throwException(true); + + String response = service.intercepted("hello", false, true, false); + assertThat(response, equalTo("hello")); + + Invocation returning = ReturningInterceptor.lastCall(); + Invocation modifying = ModifyingInterceptor.lastCall(); + Invocation repeating = RepeatingInterceptor.lastCall(); + + // first make sure the interceptors were/were not called + assertAll( + () -> assertThat("Interceptors should be called for method annotated with @Modify", + modifying, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Return", + returning, + notNullValue()), + () -> assertThat("Interceptor should be called for method annotated with @Repeat", + repeating, + notNullValue()) + ); + + // then assert the called values + assertAll( + () -> assertThat("Returning last call", returning.methodName(), is("intercepted")), + () -> assertThat("Returning last call", returning.args(), is(new Object[] {"hello", false, true, false})), + () -> assertThat("Modifying last call", modifying.methodName(), is("intercepted")), + () -> assertThat("Modifying last call", modifying.args(), is(new Object[] {"hello", false, true, false})), + () -> assertThat("Repeating last call", repeating.methodName(), is("intercepted")), + () -> assertThat("Repeating last call", repeating.args(), is(new Object[] {"hello", false, true, false})) + ); + } + +} diff --git a/service/tests/inject/interception/src/test/resources/logging.properties b/service/tests/inject/interception/src/test/resources/logging.properties new file mode 100644 index 00000000000..5190b108e32 --- /dev/null +++ b/service/tests/inject/interception/src/test/resources/logging.properties @@ -0,0 +1,22 @@ +# +# 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.service.inject.LookupTrace.level=ALL diff --git a/service/tests/inject/lookup/pom.xml b/service/tests/inject/lookup/pom.xml new file mode 100644 index 00000000000..4ee6d85a335 --- /dev/null +++ b/service/tests/inject/lookup/pom.xml @@ -0,0 +1,143 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-lookup + Helidon Service Tests Inject Lookup + Tests for all lookup methods + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.logging + helidon-logging-common + test + + + io.helidon.logging + helidon-logging-jul + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + + diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractCommon.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractCommon.java new file mode 100644 index 00000000000..f7bedc78d58 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractCommon.java @@ -0,0 +1,26 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +/** + * Implemented by all services. + */ +@Service.Contract +interface ContractCommon { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoIpProvider.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoIpProvider.java new file mode 100644 index 00000000000..4ae5b3b41ea --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoIpProvider.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoScope.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoScope.java new file mode 100644 index 00000000000..d66302fcca6 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoScope.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractNoScope extends ContractCommon { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoScopeNoIpProvider.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoScopeNoIpProvider.java new file mode 100644 index 00000000000..5b0b27303a5 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractNoScopeNoIpProvider.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractNoScopeNoIpProvider extends ContractNoScope, ContractNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractRequestScope.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractRequestScope.java new file mode 100644 index 00000000000..c216c91536d --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractRequestScope.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractRequestScope extends ContractCommon { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractRequestScopeNoIpProvider.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractRequestScopeNoIpProvider.java new file mode 100644 index 00000000000..f6ccc0be232 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractRequestScopeNoIpProvider.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +public interface ContractRequestScopeNoIpProvider extends ContractRequestScope, ContractNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractSingleton.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractSingleton.java new file mode 100644 index 00000000000..4c484461ab4 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractSingleton.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ContractSingleton extends ContractCommon { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractSingletonNoIpProvider.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractSingletonNoIpProvider.java new file mode 100644 index 00000000000..365980c51a4 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/ContractSingletonNoIpProvider.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.registry.Service; + +@Service.Contract +public interface ContractSingletonNoIpProvider extends ContractSingleton, ContractNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeDirectExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeDirectExample.java new file mode 100644 index 00000000000..dab285f41c7 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeDirectExample.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.inject.api.Injection; + +@Injection.PerLookup +class NoScopeDirectExample implements ContractNoScopeNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeInjectionPointProviderExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeInjectionPointProviderExample.java new file mode 100644 index 00000000000..82b457522da --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeInjectionPointProviderExample.java @@ -0,0 +1,60 @@ +/* + * 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.tests.lookup; + +import java.util.Optional; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.InjectionPointFactory; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; + +@Injection.PerLookup +@NoScopeInjectionPointProviderExample.FirstQuali +@NoScopeInjectionPointProviderExample.SecondQuali +class NoScopeInjectionPointProviderExample implements InjectionPointFactory { + static final Qualifier FIRST_QUALI = Qualifier.create(NoScopeInjectionPointProviderExample.FirstQuali.class); + static final Qualifier SECOND_QUALI = Qualifier.create(NoScopeInjectionPointProviderExample.SecondQuali.class); + + @Override + public Optional> first(Lookup lookup) { + if (lookup.qualifiers().contains(FIRST_QUALI)) { + return Optional.of(QualifiedInstance.create(new FirstClass(), FIRST_QUALI)); + } + if (lookup.qualifiers().contains(SECOND_QUALI)) { + return Optional.of(QualifiedInstance.create(new SecondClass(), SECOND_QUALI)); + } + return Optional.empty(); + } + + @Injection.Qualifier + @interface FirstQuali { + } + + @Injection.Qualifier + @interface SecondQuali { + } + + static class FirstClass implements ContractNoScope { + + } + + static class SecondClass implements ContractNoScope { + + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeServicesProviderExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeServicesProviderExample.java new file mode 100644 index 00000000000..0a499217665 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeServicesProviderExample.java @@ -0,0 +1,55 @@ +/* + * 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.tests.lookup; + +import java.util.List; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Qualifier; + +@Injection.PerLookup +@NoScopeServicesProviderExample.FirstQuali +@NoScopeServicesProviderExample.SecondQuali +class NoScopeServicesProviderExample implements Injection.ServicesFactory { + static final Qualifier FIRST_QUALI = Qualifier.create(FirstQuali.class); + static final Qualifier SECOND_QUALI = Qualifier.create(SecondQuali.class); + + @Override + public List> services() { + return List.of( + QualifiedInstance.create(new FirstClass(), FIRST_QUALI), + QualifiedInstance.create(new SecondClass(), SECOND_QUALI) + ); + } + + @Injection.Qualifier + @interface FirstQuali { + } + + @Injection.Qualifier + @interface SecondQuali { + } + + static class FirstClass implements ContractNoScopeNoIpProvider { + + } + + static class SecondClass implements ContractNoScopeNoIpProvider { + + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeSupplierExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeSupplierExample.java new file mode 100644 index 00000000000..a7325fe7790 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/NoScopeSupplierExample.java @@ -0,0 +1,36 @@ +/* + * 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.tests.lookup; + +import java.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; + +@Injection.PerLookup +@Weight(Weighted.DEFAULT_WEIGHT + 1) // higher than other no-scope, lower than singleton supplier +class NoScopeSupplierExample implements Supplier { + + @Override + public ContractNoScopeNoIpProvider get() { + return new First(); + } + + static class First implements ContractNoScopeNoIpProvider { + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeDirectExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeDirectExample.java new file mode 100644 index 00000000000..69bb9d14306 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeDirectExample.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.inject.api.Injection; + +@Injection.PerRequest +class RequestScopeDirectExample implements ContractRequestScopeNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeInjectionPointProviderExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeInjectionPointProviderExample.java new file mode 100644 index 00000000000..ccd0fce70a3 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeInjectionPointProviderExample.java @@ -0,0 +1,65 @@ +/* + * 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.tests.lookup; + +import java.util.Optional; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; + +@Injection.PerRequest +@RequestScopeInjectionPointProviderExample.FirstQuali +@RequestScopeInjectionPointProviderExample.SecondQuali +class RequestScopeInjectionPointProviderExample implements Injection.InjectionPointFactory { + static final Qualifier FIRST_QUALI = Qualifier.create(RequestScopeInjectionPointProviderExample.FirstQuali.class); + static final Qualifier SECOND_QUALI = Qualifier.create(RequestScopeInjectionPointProviderExample.SecondQuali.class); + static final QualifiedInstance FIRST = + QualifiedInstance.create(new RequestScopeInjectionPointProviderExample.FirstClass(), + FIRST_QUALI); + static final QualifiedInstance SECOND = + QualifiedInstance.create(new RequestScopeInjectionPointProviderExample.SecondClass(), + SECOND_QUALI); + + @Override + public Optional> first(Lookup lookup) { + if (lookup.qualifiers().contains(FIRST_QUALI)) { + return Optional.of(FIRST); + } + if (lookup.qualifiers().contains(SECOND_QUALI)) { + return Optional.of(SECOND); + } + return Optional.empty(); + } + + @Injection.Qualifier + @interface FirstQuali { + } + + @Injection.Qualifier + @interface SecondQuali { + } + + static class FirstClass implements ContractRequestScope { + + } + + static class SecondClass implements ContractRequestScope { + + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeServicesProviderExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeServicesProviderExample.java new file mode 100644 index 00000000000..64fe6704f78 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeServicesProviderExample.java @@ -0,0 +1,56 @@ +/* + * 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.tests.lookup; + +import java.util.List; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Injection.ServicesFactory; +import io.helidon.service.inject.api.Qualifier; + +@Injection.PerRequest +@RequestScopeServicesProviderExample.FirstQuali // need to qualify, so lookups for specific qualifier match this provider +@RequestScopeServicesProviderExample.SecondQuali +class RequestScopeServicesProviderExample implements ServicesFactory { + static final Qualifier FIRST_QUALI = Qualifier.create(FirstQuali.class); + static final Qualifier SECOND_QUALI = Qualifier.create(SecondQuali.class); + + @Override + public List> services() { + return List.of( + QualifiedInstance.create(new FirstClass(), FIRST_QUALI), + QualifiedInstance.create(new SecondClass(), SECOND_QUALI) + ); + } + + @Injection.Qualifier + @interface FirstQuali { + } + + @Injection.Qualifier + @interface SecondQuali { + } + + static class FirstClass implements ContractRequestScopeNoIpProvider { + + } + + static class SecondClass implements ContractRequestScopeNoIpProvider { + + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeSupplierExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeSupplierExample.java new file mode 100644 index 00000000000..d01cca47abc --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/RequestScopeSupplierExample.java @@ -0,0 +1,37 @@ +/* + * 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.tests.lookup; + +import java.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; + +@Injection.PerRequest +@Weight(Weighted.DEFAULT_WEIGHT + 1) // higher than other no-scope, lower than singleton supplier +class RequestScopeSupplierExample implements Supplier { + private static final ContractRequestScopeNoIpProvider FIRST = new First(); + + @Override + public ContractRequestScopeNoIpProvider get() { + return FIRST; + } + + static class First implements ContractRequestScopeNoIpProvider { + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonDirectExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonDirectExample.java new file mode 100644 index 00000000000..0e553ab2892 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonDirectExample.java @@ -0,0 +1,23 @@ +/* + * 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.tests.lookup; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class SingletonDirectExample implements ContractSingletonNoIpProvider { +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonInjectionPointProviderExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonInjectionPointProviderExample.java new file mode 100644 index 00000000000..613f376cd1a --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonInjectionPointProviderExample.java @@ -0,0 +1,62 @@ +/* + * 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.tests.lookup; + +import java.util.Optional; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.InjectionPointFactory; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; + +@Injection.Singleton +@SingletonInjectionPointProviderExample.FirstQuali +@SingletonInjectionPointProviderExample.SecondQuali +class SingletonInjectionPointProviderExample implements InjectionPointFactory { + static final Qualifier FIRST_QUALI = Qualifier.create(FirstQuali.class); + static final Qualifier SECOND_QUALI = Qualifier.create(SecondQuali.class); + static final QualifiedInstance FIRST = QualifiedInstance.create(new FirstClass(), FIRST_QUALI); + static final QualifiedInstance SECOND = QualifiedInstance.create(new SecondClass(), SECOND_QUALI); + + @Override + public Optional> first(Lookup lookup) { + if (lookup.qualifiers().contains(FIRST_QUALI)) { + return Optional.of(FIRST); + } + if (lookup.qualifiers().contains(SECOND_QUALI)) { + return Optional.of(SECOND); + } + return Optional.empty(); + } + + @Injection.Qualifier + @interface FirstQuali { + } + + @Injection.Qualifier + @interface SecondQuali { + } + + static class FirstClass implements ContractSingleton { + + } + + static class SecondClass implements ContractSingleton { + + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonServicesProviderExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonServicesProviderExample.java new file mode 100644 index 00000000000..5b552260d0d --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonServicesProviderExample.java @@ -0,0 +1,55 @@ +/* + * 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.tests.lookup; + +import java.util.List; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Qualifier; + +@Injection.Singleton +@SingletonServicesProviderExample.FirstQuali // need to qualify, so lookups for specific qualifier match this provider +@SingletonServicesProviderExample.SecondQuali +class SingletonServicesProviderExample implements Injection.ServicesFactory { + static final Qualifier FIRST_QUALI = Qualifier.create(FirstQuali.class); + static final Qualifier SECOND_QUALI = Qualifier.create(SecondQuali.class); + + @Override + public List> services() { + return List.of( + QualifiedInstance.create(new FirstClass(), FIRST_QUALI), + QualifiedInstance.create(new SecondClass(), SECOND_QUALI) + ); + } + + @Injection.Qualifier + @interface FirstQuali { + } + + @Injection.Qualifier + @interface SecondQuali { + } + + static class FirstClass implements ContractSingletonNoIpProvider { + + } + + static class SecondClass implements ContractSingletonNoIpProvider { + + } +} diff --git a/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonSupplierExample.java b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonSupplierExample.java new file mode 100644 index 00000000000..c0c2dc2a70d --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/io/helidon/service/inject/tests/lookup/SingletonSupplierExample.java @@ -0,0 +1,37 @@ +/* + * 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.tests.lookup; + +import java.util.function.Supplier; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 2) // the only weighted one, should be first +class SingletonSupplierExample implements Supplier { + private static final ContractSingletonNoIpProvider FIRST = new First(); + + @Override + public ContractSingletonNoIpProvider get() { + return FIRST; + } + + static class First implements ContractSingletonNoIpProvider { + } +} diff --git a/service/tests/inject/lookup/src/main/java/module-info.java b/service/tests/inject/lookup/src/main/java/module-info.java new file mode 100644 index 00000000000..0dce7930575 --- /dev/null +++ b/service/tests/inject/lookup/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +module io.helidon.service.tests.inject.lookup { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + requires io.helidon.service.inject; + + exports io.helidon.service.inject.tests.lookup; +} \ No newline at end of file diff --git a/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/AllScopesLookupTest.java b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/AllScopesLookupTest.java new file mode 100644 index 00000000000..2d9f63c9f77 --- /dev/null +++ b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/AllScopesLookupTest.java @@ -0,0 +1,245 @@ +/* + * 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.tests.lookup; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.service.inject.InjectRegistryManager; +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.inject.api.Scopes; +import io.helidon.service.inject.api.Scope; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test all lookup methods for all scopes (combination). + */ +class AllScopesLookupTest { + private static final Lookup LOOKUP = Lookup.create(ContractCommon.class); + private static final Class CONTRACT = ContractCommon.class; + + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + private Scope requestScope; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @BeforeEach + void startRequestScope() { + // we need to have request scope active, so we can access providers from everywhere + var scopes = registry.get(Scopes.class); + requestScope = scopes.createScope(Injection.PerRequest.TYPE, "unit-test", Map.of()); + } + + @AfterEach + void stopRequestScope() { + if (requestScope != null) { + requestScope.close(); + } + } + + @Test + void getLookupTest() { + ContractCommon first = registry.get(LOOKUP); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + } + + @Test + void getTypeTest() { + ContractCommon first = registry.get(CONTRACT); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + } + + @Test + void firstLookupTest() { + Optional first = registry.first(LOOKUP); + checkOptional(first, SingletonSupplierExample.First.class); + } + + @Test + void firstTypeTest() { + Optional first = registry.first(CONTRACT); + checkOptional(first, SingletonSupplierExample.First.class); + } + + @Test + void allLookupTest() { + List all = registry.all(LOOKUP); + + assertThat(all, hasSize(12)); + } + + @Test + void allTypeTest() { + List all = registry.all(CONTRACT); + + assertThat(all, hasSize(12)); + } + + @Test + void supplyLookupTest() { + Supplier supply = registry.supply(LOOKUP); + ContractCommon first = supply.get(); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + } + + @Test + void supplyTypeTest() { + Supplier supply = registry.supply(CONTRACT); + ContractCommon first = supply.get(); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + } + + @Test + void supplyFirstLookupTest() { + Supplier> supply = registry.supplyFirst(LOOKUP); + + Optional first = supply.get(); + checkOptional(first, SingletonSupplierExample.First.class); + } + + @Test + void supplyFirstTypeTest() { + Supplier> supply = registry.supplyFirst(CONTRACT); + + Optional first = supply.get(); + checkOptional(first, SingletonSupplierExample.First.class); + } + + @Test + void supplyAllLookupTest() { + List all = registry.supplyAll(LOOKUP) + .get(); + + assertThat(all, hasSize(12)); + } + + @Test + void supplyAllTypeTest() { + List all = registry.supplyAll(CONTRACT) + .get(); + + assertThat(all, hasSize(12)); + } + + @Test + void lookupServicesTest() { + List serviceDescriptors = registry.lookupServices(LOOKUP); + + /* + Order: + weight 102 + 1. SingletonSupplier + weight 101 + 2. NoScopeSupplier + 3. RequestScopeSupplier + default weight, no qualifiers, alphabet + 4. NoScopeDirect + 5. RequestScopeDirect + 6. SingletonDirect + default weight, qualified, alphabet + 7. NoScopeIp + 8. NoScopeServices + 9. RequestScopeIp + 10. RequestScopeServices + 11. SingletonIp + 12. SingletonServices + */ + + assertThat(serviceDescriptors, hasSize(12)); + + int i = 0; + // 102 + assertThat(serviceDescriptors.get(i++), sameInstance(SingletonSupplierExample__ServiceDescriptor.INSTANCE)); + // 101 + assertThat(serviceDescriptors.get(i++), sameInstance(NoScopeSupplierExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), sameInstance(RequestScopeSupplierExample__ServiceDescriptor.INSTANCE)); + // default weight, no qualifiers, alphabet + assertThat(serviceDescriptors.get(i++), sameInstance(NoScopeDirectExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), sameInstance(RequestScopeDirectExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), sameInstance(SingletonDirectExample__ServiceDescriptor.INSTANCE)); + + // default weight, qualified, ordered by class name (package also, but these share the same package) + assertThat(serviceDescriptors.get(i++), sameInstance(NoScopeInjectionPointProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), sameInstance(NoScopeServicesProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), + sameInstance(RequestScopeInjectionPointProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), sameInstance(RequestScopeServicesProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i++), sameInstance(SingletonInjectionPointProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(i), sameInstance(SingletonServicesProviderExample__ServiceDescriptor.INSTANCE)); + } + + @Test + void qualifiedServicesProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractCommon.class) + .addQualifier(SingletonServicesProviderExample.SECOND_QUALI) + .build(); + + ContractCommon first = registry.get(lookup); + assertThat(first, instanceOf(SingletonServicesProviderExample.SecondClass.class)); + + ContractCommon second = registry.get(lookup); + assertThat(second, sameInstance(first)); + } + + @Test + void qualifiedIpProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractCommon.class) + .addQualifier(SingletonInjectionPointProviderExample.SECOND_QUALI) + .build(); + + ContractCommon instance = registry.get(lookup); + assertThat(instance, instanceOf(SingletonInjectionPointProviderExample.SecondClass.class)); + } + + private ContractCommon checkOptional(Optional first, Class expectedType) { + assertThat(first, optionalPresent()); + assertThat(first, optionalValue(instanceOf(expectedType))); + return first.get(); + } +} diff --git a/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/NoScopeLookupTest.java b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/NoScopeLookupTest.java new file mode 100644 index 00000000000..109032dd318 --- /dev/null +++ b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/NoScopeLookupTest.java @@ -0,0 +1,263 @@ +/* + * 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.tests.lookup; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test all lookup methods for both noScope and no scope. + */ +class NoScopeLookupTest { + private static final Lookup LOOKUP = Lookup.create(ContractNoScope.class); + private static final Class CONTRACT = ContractNoScope.class; + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void getLookupTest() { + ContractNoScope first = registry.get(LOOKUP); + assertThat(first, instanceOf(NoScopeSupplierExample.First.class)); + ContractNoScope second = registry.get(LOOKUP); + assertThat(first, not(sameInstance(second))); + } + + @Test + void getTypeTest() { + ContractNoScope first = registry.get(CONTRACT); + assertThat(first, instanceOf(NoScopeSupplierExample.First.class)); + ContractNoScope second = registry.get(CONTRACT); + assertThat(first, not(sameInstance(second))); + } + + @Test + void firstLookupTest() { + Optional first = registry.first(LOOKUP); + ContractNoScope firstValue = checkOptional(first, NoScopeSupplierExample.First.class); + + Optional second = registry.first(LOOKUP); + ContractNoScope secondValue = checkOptional(second, NoScopeSupplierExample.First.class); + + assertThat(firstValue, not(sameInstance(secondValue))); + } + + @Test + void firstTypeTest() { + Optional first = registry.first(CONTRACT); + ContractNoScope firstValue = checkOptional(first, NoScopeSupplierExample.First.class); + + Optional second = registry.first(CONTRACT); + ContractNoScope secondValue = checkOptional(second, NoScopeSupplierExample.First.class); + + assertThat(firstValue, not(sameInstance(secondValue))); + } + + @Test + void allLookupTest() { + List all = registry.all(LOOKUP); + + checkAll(all, 4); + } + + @Test + void allTypeTest() { + List all = registry.all(CONTRACT); + + checkAll(all, 4); + } + + @Test + void supplyLookupTest() { + Supplier supply = registry.supply(LOOKUP); + ContractNoScope first = supply.get(); + assertThat(first, instanceOf(NoScopeSupplierExample.First.class)); + + supply = registry.supply(LOOKUP); + ContractNoScope second = supply.get(); + + assertThat(first, not(sameInstance(second))); + } + + @Test + void supplyTypeTest() { + Supplier supply = registry.supply(CONTRACT); + ContractNoScope first = supply.get(); + assertThat(first, instanceOf(NoScopeSupplierExample.First.class)); + + supply = registry.supply(CONTRACT); + ContractNoScope second = supply.get(); + + assertThat(first, not(sameInstance(second))); + } + + @Test + void supplyFirstLookupTest() { + Supplier> supply = registry.supplyFirst(LOOKUP); + + Optional first = supply.get(); + ContractNoScope firstValue = checkOptional(first, NoScopeSupplierExample.First.class); + + supply = registry.supplyFirst(LOOKUP); + Optional second = supply.get(); + ContractNoScope secondValue = checkOptional(second, NoScopeSupplierExample.First.class); + + assertThat(firstValue, not(sameInstance(secondValue))); + } + + @Test + void supplyFirstTypeTest() { + Supplier> supply = registry.supplyFirst(CONTRACT); + + Optional first = supply.get(); + ContractNoScope firstValue = checkOptional(first, NoScopeSupplierExample.First.class); + + supply = registry.supplyFirst(CONTRACT); + Optional second = supply.get(); + ContractNoScope secondValue = checkOptional(second, NoScopeSupplierExample.First.class); + + assertThat(firstValue, not(sameInstance(secondValue))); + } + + @Test + void supplyAllLookupTest() { + List all = registry.supplyAll(LOOKUP) + .get(); + + checkAll(all, 4); + } + + @Test + void supplyAllTypeTest() { + List all = registry.supplyAll(CONTRACT) + .get(); + + checkAll(all, 4); + } + + @Test + void supplyFromDescriptorTest() { + Supplier supply = registry.supply(NoScopeDirectExample.class); + + ContractNoScope first = supply.get(); + assertThat(first, instanceOf(NoScopeDirectExample.class)); + + supply = registry.supply(NoScopeDirectExample.class); + ContractNoScope second = supply.get(); + + assertThat(first, not(sameInstance(second))); + } + + @Test + void lookupServicesTest() { + List serviceDescriptors = registry.lookupServices(LOOKUP); + + /* + Order: + 1. NoScopeSupplierExample (highest weight) + 2. NoScopeDirectExample (alphabet...) + 3. NoScopeInjectionPointExample + 4. NoScopeServicesProviderExample + */ + + assertThat(serviceDescriptors, hasSize(4)); + + assertThat(serviceDescriptors.getFirst(), sameInstance(NoScopeSupplierExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(1), sameInstance(NoScopeDirectExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(2), sameInstance(NoScopeInjectionPointProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(3), sameInstance(NoScopeServicesProviderExample__ServiceDescriptor.INSTANCE)); + } + + @Test + void qualifiedServicesProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractNoScope.class) + .addQualifier(NoScopeServicesProviderExample.SECOND_QUALI) + .build(); + + ContractNoScope first = registry.get(lookup); + assertThat(first, instanceOf(NoScopeServicesProviderExample.SecondClass.class)); + + ContractNoScope second = registry.get(lookup); + assertThat(second, not(sameInstance(first))); + } + + @Test + void qualifiedIpProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractNoScope.class) + .addQualifier(NoScopeInjectionPointProviderExample.SECOND_QUALI) + .build(); + + ContractNoScope instance = registry.get(lookup); + assertThat(instance, instanceOf(NoScopeInjectionPointProviderExample.SecondClass.class)); + } + + private void checkAll(List all, int size) { + /* + Order: + 1. NoScopeSupplierExample (highest weight) + 2. NoScopeDirectExample (alphabet...) + 3. NoScopeInjectionPointExample - no instance, as we do not have a qualifier + 4. NoScopeServicesProviderExample - two qualified instances + */ + assertThat(all, hasSize(size)); + + assertThat(all.getFirst(), instanceOf(NoScopeSupplierExample.First.class)); + assertThat(all.get(1), instanceOf(NoScopeDirectExample.class)); + assertThat(all.get(2), instanceOf(NoScopeServicesProviderExample.FirstClass.class)); + if (size > 3) { + assertThat(all.get(3), instanceOf(NoScopeServicesProviderExample.SecondClass.class)); + } + } + + private ContractNoScope checkOptional(Optional first, Class expectedType) { + assertThat(first, optionalPresent()); + assertThat(first, optionalValue(instanceOf(expectedType))); + return first.get(); + } +} diff --git a/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/RequestScopeLookupTest.java b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/RequestScopeLookupTest.java new file mode 100644 index 00000000000..fff7488f7bc --- /dev/null +++ b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/RequestScopeLookupTest.java @@ -0,0 +1,289 @@ +/* + * 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.tests.lookup; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.logging.common.LogConfig; +import io.helidon.service.inject.InjectRegistryManager; +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.inject.api.Scope; +import io.helidon.service.inject.api.Scopes; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test all lookup methods for requestScope. + */ +class RequestScopeLookupTest { + static { + LogConfig.initClass(); + } + + private static final Lookup LOOKUP = Lookup.create(ContractRequestScope.class); + private static final Class CONTRACT = ContractRequestScope.class; + + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + private Scope requestScope; + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @BeforeEach + void startRequestScope() { + var scopes = registry.get(Scopes.class); + requestScope = scopes.createScope(Injection.PerRequest.TYPE, "unit-test", Map.of()); + } + + @AfterEach + void stopRequestScope() { + if (requestScope != null) { + requestScope.close(); + } + } + + @Test + void getLookupTest() { + ContractRequestScope first = registry.get(LOOKUP); + assertThat(first, instanceOf(RequestScopeSupplierExample.First.class)); + ContractRequestScope second = registry.get(LOOKUP); + assertThat(first, sameInstance(second)); + } + + @Test + void getTypeTest() { + ContractRequestScope first = registry.get(CONTRACT); + assertThat(first, instanceOf(RequestScopeSupplierExample.First.class)); + ContractRequestScope second = registry.get(CONTRACT); + assertThat(first, sameInstance(second)); + } + + @Test + void firstLookupTest() { + Optional first = registry.first(LOOKUP); + ContractRequestScope firstValue = checkOptional(first, RequestScopeSupplierExample.First.class); + + Optional second = registry.first(LOOKUP); + ContractRequestScope secondValue = checkOptional(second, RequestScopeSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void firstTypeTest() { + Optional first = registry.first(CONTRACT); + ContractRequestScope firstValue = checkOptional(first, RequestScopeSupplierExample.First.class); + + Optional second = registry.first(CONTRACT); + ContractRequestScope secondValue = checkOptional(second, RequestScopeSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void allLookupTest() { + List all = registry.all(LOOKUP); + + checkAll(all, 4); + } + + @Test + void allTypeTest() { + List all = registry.all(CONTRACT); + + checkAll(all, 4); + } + + @Test + void supplyLookupTest() { + Supplier supply = registry.supply(LOOKUP); + ContractRequestScope first = supply.get(); + assertThat(first, instanceOf(RequestScopeSupplierExample.First.class)); + + supply = registry.supply(LOOKUP); + ContractRequestScope second = supply.get(); + + assertThat(first, sameInstance(second)); + } + + @Test + void supplyTypeTest() { + Supplier supply = registry.supply(CONTRACT); + ContractRequestScope first = supply.get(); + assertThat(first, instanceOf(RequestScopeSupplierExample.First.class)); + + supply = registry.supply(CONTRACT); + ContractRequestScope second = supply.get(); + + assertThat(first, sameInstance(second)); + } + + @Test + void supplyFirstLookupTest() { + Supplier> supply = registry.supplyFirst(LOOKUP); + + Optional first = supply.get(); + ContractRequestScope firstValue = checkOptional(first, RequestScopeSupplierExample.First.class); + + supply = registry.supplyFirst(LOOKUP); + Optional second = supply.get(); + ContractRequestScope secondValue = checkOptional(second, RequestScopeSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void supplyFirstTypeTest() { + Supplier> supply = registry.supplyFirst(CONTRACT); + + Optional first = supply.get(); + ContractRequestScope firstValue = checkOptional(first, RequestScopeSupplierExample.First.class); + + supply = registry.supplyFirst(CONTRACT); + Optional second = supply.get(); + ContractRequestScope secondValue = checkOptional(second, RequestScopeSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void supplyAllLookupTest() { + List all = registry.supplyAll(LOOKUP) + .get(); + + checkAll(all, 4); + } + + @Test + void supplyAllTypeTest() { + List all = registry.supplyAll(CONTRACT) + .get(); + + checkAll(all, 4); + } + + @Test + void supplyFromDescriptorTest() { + Supplier supply = registry.supply(RequestScopeDirectExample.class); + + ContractRequestScope first = supply.get(); + assertThat(first, instanceOf(RequestScopeDirectExample.class)); + + supply = registry.supply(RequestScopeDirectExample.class); + ContractRequestScope second = supply.get(); + + assertThat(first, sameInstance(second)); + } + + @Test + void lookupServicesTest() { + List serviceDescriptors = registry.lookupServices(LOOKUP); + + /* + Order: + 1. RequestScopeSupplierExample (highest weight) + 2. RequestScopeDirectExample (alphabet...) + 3. RequestScopeInjectionPointExample + 4. RequestScopeServicesProviderExample + */ + + assertThat(serviceDescriptors, hasSize(4)); + + assertThat(serviceDescriptors.getFirst(), sameInstance(RequestScopeSupplierExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(1), sameInstance(RequestScopeDirectExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(2), + sameInstance(RequestScopeInjectionPointProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(3), sameInstance(RequestScopeServicesProviderExample__ServiceDescriptor.INSTANCE)); + } + + @Test + void qualifiedServicesProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractRequestScope.class) + .addQualifier(RequestScopeServicesProviderExample.SECOND_QUALI) + .build(); + + ContractRequestScope first = registry.get(lookup); + assertThat(first, instanceOf(RequestScopeServicesProviderExample.SecondClass.class)); + + ContractRequestScope second = registry.get(lookup); + assertThat(second, sameInstance(first)); + } + + @Test + void qualifiedIpProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractRequestScope.class) + .addQualifier(RequestScopeInjectionPointProviderExample.SECOND_QUALI) + .build(); + + ContractRequestScope instance = registry.get(lookup); + assertThat(instance, instanceOf(RequestScopeInjectionPointProviderExample.SecondClass.class)); + } + + private void checkAll(List all, int size) { + /* + Order: + 1. RequestScopeSupplierExample (highest weight) + 2. RequestScopeDirectExample (alphabet...) + 3. RequestScopeInjectionPointExample - no instance, as we do not have a qualifier + 4. RequestScopeServicesProviderExample - two qualified instances + */ + assertThat(all, hasSize(size)); + + assertThat(all.getFirst(), instanceOf(RequestScopeSupplierExample.First.class)); + assertThat(all.get(1), instanceOf(RequestScopeDirectExample.class)); + assertThat(all.get(2), instanceOf(RequestScopeServicesProviderExample.FirstClass.class)); + if (size > 3) { + assertThat(all.get(3), instanceOf(RequestScopeServicesProviderExample.SecondClass.class)); + } + } + + private ContractRequestScope checkOptional(Optional first, Class expectedType) { + assertThat(first, optionalPresent()); + assertThat(first, optionalValue(instanceOf(expectedType))); + return first.get(); + } +} diff --git a/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/SingletonLookupTest.java b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/SingletonLookupTest.java new file mode 100644 index 00000000000..e3ea0fb064f --- /dev/null +++ b/service/tests/inject/lookup/src/test/java/io/helidon/service/inject/tests/lookup/SingletonLookupTest.java @@ -0,0 +1,291 @@ +/* + * 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.tests.lookup; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.logging.common.LogConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.FactoryType; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Injection.InjectionPointFactory; +import io.helidon.service.inject.api.Lookup; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test all lookup methods for singleton. + */ +class SingletonLookupTest { + private static final Lookup LOOKUP = Lookup.create(ContractSingleton.class); + private static final Class CONTRACT = ContractSingleton.class; + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + static { + LogConfig.initClass(); + } + + @BeforeAll + static void init() { + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void getLookupTest() { + ContractSingleton first = registry.get(LOOKUP); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + ContractSingleton second = registry.get(LOOKUP); + assertThat(first, sameInstance(second)); + } + + @Test + void getTypeTest() { + ContractSingleton first = registry.get(CONTRACT); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + ContractSingleton second = registry.get(CONTRACT); + assertThat(first, sameInstance(second)); + } + + @Test + void firstLookupTest() { + Optional first = registry.first(LOOKUP); + ContractSingleton firstValue = checkOptional(first, SingletonSupplierExample.First.class); + + Optional second = registry.first(LOOKUP); + ContractSingleton secondValue = checkOptional(second, SingletonSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void firstTypeTest() { + Optional first = registry.first(CONTRACT); + ContractSingleton firstValue = checkOptional(first, SingletonSupplierExample.First.class); + + Optional second = registry.first(CONTRACT); + ContractSingleton secondValue = checkOptional(second, SingletonSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void allLookupTest() { + List all = registry.all(LOOKUP); + + checkAll(all, 4); + } + + @Test + void allTypeTest() { + List all = registry.all(CONTRACT); + + checkAll(all, 4); + } + + @Test + void supplyLookupTest() { + Supplier supply = registry.supply(LOOKUP); + ContractSingleton first = supply.get(); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + + supply = registry.supply(LOOKUP); + ContractSingleton second = supply.get(); + + assertThat(first, sameInstance(second)); + } + + @Test + void supplyTypeTest() { + Supplier supply = registry.supply(CONTRACT); + ContractSingleton first = supply.get(); + assertThat(first, instanceOf(SingletonSupplierExample.First.class)); + + supply = registry.supply(CONTRACT); + ContractSingleton second = supply.get(); + + assertThat(first, sameInstance(second)); + } + + @Test + void supplyFirstLookupTest() { + Supplier> supply = registry.supplyFirst(LOOKUP); + + Optional first = supply.get(); + ContractSingleton firstValue = checkOptional(first, SingletonSupplierExample.First.class); + + supply = registry.supplyFirst(LOOKUP); + Optional second = supply.get(); + ContractSingleton secondValue = checkOptional(second, SingletonSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void supplyFirstTypeTest() { + Supplier> supply = registry.supplyFirst(CONTRACT); + + Optional first = supply.get(); + ContractSingleton firstValue = checkOptional(first, SingletonSupplierExample.First.class); + + supply = registry.supplyFirst(CONTRACT); + Optional second = supply.get(); + ContractSingleton secondValue = checkOptional(second, SingletonSupplierExample.First.class); + + assertThat(firstValue, sameInstance(secondValue)); + } + + @Test + void supplyAllLookupTest() { + List all = registry.supplyAll(LOOKUP) + .get(); + + checkAll(all, 4); + } + + @Test + void supplyAllTypeTest() { + List all = registry.supplyAll(CONTRACT) + .get(); + + checkAll(all, 4); + } + + @Test + void supplyFromDescriptorTest() { + Supplier supply = registry.supply(SingletonDirectExample.class); + + ContractSingleton first = supply.get(); + assertThat(first, instanceOf(SingletonDirectExample.class)); + + supply = registry.supply(SingletonDirectExample.class); + ContractSingleton second = supply.get(); + + assertThat(first, sameInstance(second)); + } + + @Test + void lookupServicesTest() { + List serviceDescriptors = registry.lookupServices(LOOKUP); + + /* + Order: + 1. SingletonSupplierExample (highest weight) + 2. SingletonDirectExample (alphabet...) + 3. SingletonInjectionPointExample + 4. SingletonServicesProviderExample + */ + + assertThat(serviceDescriptors, hasSize(4)); + + assertThat(serviceDescriptors.getFirst(), sameInstance(SingletonSupplierExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(1), sameInstance(SingletonDirectExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(2), sameInstance(SingletonInjectionPointProviderExample__ServiceDescriptor.INSTANCE)); + assertThat(serviceDescriptors.get(3), sameInstance(SingletonServicesProviderExample__ServiceDescriptor.INSTANCE)); + } + + @Test + void qualifiedServicesProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractSingleton.class) + .addQualifier(SingletonServicesProviderExample.SECOND_QUALI) + .build(); + + ContractSingleton first = registry.get(lookup); + assertThat(first, instanceOf(SingletonServicesProviderExample.SecondClass.class)); + + ContractSingleton second = registry.get(lookup); + assertThat(second, sameInstance(first)); + } + + @Test + void qualifiedIpProviderTest() { + Lookup lookup = Lookup.builder() + .addContract(ContractSingleton.class) + .addQualifier(SingletonInjectionPointProviderExample.SECOND_QUALI) + .build(); + + ContractSingleton instance = registry.get(lookup); + assertThat(instance, instanceOf(SingletonInjectionPointProviderExample.SecondClass.class)); + } + + @Test + void testIpProviderLookup() { + Lookup lookup = Lookup.builder() + .addContract(ContractSingleton.class) + .addFactoryType(FactoryType.INJECTION_POINT) + .build(); + InjectionPointFactory instance = registry.get(lookup); + + assertThat(instance, instanceOf(SingletonInjectionPointProviderExample.class)); + } + + @Test + void testSupplierLookup() { + Lookup lookup = Lookup.builder() + .addContract(ContractSingleton.class) + .addFactoryType(FactoryType.SUPPLIER) + .build(); + Supplier instance = registry.get(lookup); + + assertThat(instance, instanceOf(SingletonSupplierExample.class)); + } + + private void checkAll(List all, int size) { + /* + Order: + 1. SingletonSupplierExample (highest weight) + 2. SingletonDirectExample (alphabet...) + 3. SingletonInjectionPointExample - no instance, as we do not have a qualifier + 4. SingletonServicesProviderExample - two qualified instances + */ + assertThat(all, hasSize(size)); + + assertThat(all.getFirst(), instanceOf(SingletonSupplierExample.First.class)); + assertThat(all.get(1), instanceOf(SingletonDirectExample.class)); + assertThat(all.get(2), instanceOf(SingletonServicesProviderExample.FirstClass.class)); + if (size > 3) { + assertThat(all.get(3), instanceOf(SingletonServicesProviderExample.SecondClass.class)); + } + } + + private ContractSingleton checkOptional(Optional first, Class expectedType) { + assertThat(first, optionalPresent()); + assertThat(first, optionalValue(instanceOf(expectedType))); + return first.get(); + } +} diff --git a/service/tests/inject/lookup/src/test/resources/logging.properties b/service/tests/inject/lookup/src/test/resources/logging.properties new file mode 100644 index 00000000000..5190b108e32 --- /dev/null +++ b/service/tests/inject/lookup/src/test/resources/logging.properties @@ -0,0 +1,22 @@ +# +# 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.service.inject.LookupTrace.level=ALL diff --git a/service/tests/inject/pom.xml b/service/tests/inject/pom.xml new file mode 100644 index 00000000000..71b7de940a5 --- /dev/null +++ b/service/tests/inject/pom.xml @@ -0,0 +1,50 @@ + + + + + + io.helidon.service.tests + helidon-service-tests-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + Helidon Service Tests Inject Project + Tests for injection operations + + pom + + + codegen + inject + lookup + service-lifecycle + scopes + interception + qualified-providers + stacking + toolbox + events + + diff --git a/service/tests/inject/qualified-providers/pom.xml b/service/tests/inject/qualified-providers/pom.xml new file mode 100644 index 00000000000..0772cf44a96 --- /dev/null +++ b/service/tests/inject/qualified-providers/pom.xml @@ -0,0 +1,132 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-qualified-providers + Helidon Service Tests Inject Qualified Providers + Tests for qualified providers - both typed and untyped + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + diff --git a/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/FirstQualifiedProvider.java b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/FirstQualifiedProvider.java new file mode 100644 index 00000000000..a4f4ef35592 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/FirstQualifiedProvider.java @@ -0,0 +1,50 @@ +/* + * 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.qualified.providers; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.GenericType; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; + +@Injection.Singleton +class FirstQualifiedProvider implements Injection.QualifiedFactory { + private final Map values = Map.of("first", "first", + "second", "49"); + + @Override + public Optional> first(Qualifier qualifier, Lookup lookup, GenericType type) { + Optional stringValue = Optional.of(values.get(qualifier.value().orElse("not-defined"))); + + return stringValue.map(str -> QualifiedInstance.create(mapType(str, type), qualifier)); + } + + private Object mapType(String str, GenericType type) { + if (type.equals(GenericType.OBJECT) || type.equals(GenericType.STRING)) { + return str; + } + if (type.rawType().equals(Integer.class) || type.rawType().equals(int.class)) { + return Integer.parseInt(str); + } + throw new IllegalArgumentException("This provider only supports string and int, but " + type.getTypeName() + " was " + + "requested"); + } +} diff --git a/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/FirstQualifier.java b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/FirstQualifier.java new file mode 100644 index 00000000000..67584981a59 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/FirstQualifier.java @@ -0,0 +1,31 @@ +/* + * 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.qualified.providers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.inject.api.Injection; + +@Injection.Qualifier +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@interface FirstQualifier { + String value(); +} diff --git a/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/QualifiedContract.java b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/QualifiedContract.java new file mode 100644 index 00000000000..de7c81795dc --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/QualifiedContract.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 io.helidon.service.tests.inject.qualified.providers; + +import io.helidon.service.registry.Service; + +@Service.Contract +public interface QualifiedContract { + String name(); +} diff --git a/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/SecondQualifiedProvider.java b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/SecondQualifiedProvider.java new file mode 100644 index 00000000000..3ab6cc384a1 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/SecondQualifiedProvider.java @@ -0,0 +1,55 @@ +/* + * 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.qualified.providers; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.GenericType; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.QualifiedFactory; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; + +@Injection.Singleton +class SecondQualifiedProvider implements QualifiedFactory { + private final Map values = Map.of("first", new QualifiedContractImpl("first"), + "second", new QualifiedContractImpl("second")); + + @Override + public Optional> first(Qualifier qualifier, + Lookup lookup, + GenericType type) { + return Optional.ofNullable(values.get(qualifier.value().orElse("not-defined"))) + .map(it -> QualifiedInstance.create(it, qualifier)); + } + + private final static class QualifiedContractImpl implements QualifiedContract { + private final String value; + + private QualifiedContractImpl(String value) { + this.value = value; + } + + @Override + public String name() { + return value; + } + } + +} diff --git a/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/SecondQualifier.java b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/SecondQualifier.java new file mode 100644 index 00000000000..a336a0902e1 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/SecondQualifier.java @@ -0,0 +1,31 @@ +/* + * 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.qualified.providers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.service.inject.api.Injection; + +@Injection.Qualifier +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@interface SecondQualifier { + String value(); +} diff --git a/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/TheService.java b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/TheService.java new file mode 100644 index 00000000000..7fc7493d5b3 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/io/helidon/service/tests/inject/qualified/providers/TheService.java @@ -0,0 +1,54 @@ +/* + * 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.qualified.providers; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class TheService { + private final String first; + private final int second; + private final QualifiedContract firstContract; + private final QualifiedContract secondContract; + + @Injection.Inject + TheService(@FirstQualifier("first") String first, + @FirstQualifier("second") int second, + @SecondQualifier("first") QualifiedContract firstContract, + @SecondQualifier("second") QualifiedContract secondContract) { + this.first = first; + this.second = second; + this.firstContract = firstContract; + this.secondContract = secondContract; + } + + String first() { + return first; + } + + int second() { + return second; + } + + QualifiedContract firstContract() { + return firstContract; + } + + QualifiedContract secondContract() { + return secondContract; + } +} diff --git a/service/tests/inject/qualified-providers/src/main/java/module-info.java b/service/tests/inject/qualified-providers/src/main/java/module-info.java new file mode 100644 index 00000000000..bd6e8292e08 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +module io.helidon.service.tests.qualified.providers { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + // we use Application + requires io.helidon.service.inject; + requires io.helidon.http; + requires io.helidon.common.context; + + exports io.helidon.service.tests.inject.qualified.providers; +} \ No newline at end of file diff --git a/service/tests/inject/qualified-providers/src/test/java/io/helidon/service/tests/inject/qualified/providers/QualifiedProvidersTest.java b/service/tests/inject/qualified-providers/src/test/java/io/helidon/service/tests/inject/qualified/providers/QualifiedProvidersTest.java new file mode 100644 index 00000000000..fdda2504399 --- /dev/null +++ b/service/tests/inject/qualified-providers/src/test/java/io/helidon/service/tests/inject/qualified/providers/QualifiedProvidersTest.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.qualified.providers; + +import io.helidon.service.inject.InjectConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.registry.ServiceRegistry; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class QualifiedProvidersTest { + @Test + public void testQualifiedProvidersNoApp() { + InjectRegistryManager registryManager = InjectRegistryManager.create(InjectConfig.builder() + .useBinding(false) + .build()); + + try { + testServices(registryManager.registry()); + } finally { + registryManager.shutdown(); + } + } + + @Test + public void testQualifiedProvidersWithApp() { + InjectRegistryManager registryManager = InjectRegistryManager.create(); + + try { + testServices(registryManager.registry()); + } finally { + registryManager.shutdown(); + } + } + + private void testServices(ServiceRegistry registry) { + TheService theService = registry.get(TheService.class); + + assertThat(theService.first(), is("first")); + assertThat(theService.second(), is(49)); + assertThat(theService.firstContract().name(), is("first")); + assertThat(theService.secondContract().name(), is("second")); + } +} diff --git a/service/tests/inject/scopes/pom.xml b/service/tests/inject/scopes/pom.xml new file mode 100644 index 00000000000..ad65e443cd1 --- /dev/null +++ b/service/tests/inject/scopes/pom.xml @@ -0,0 +1,109 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-scopes + Helidon Service Tests Inject Scopes + Tets for injection scopes (Request scope, singleton, dependent) + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScope.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScope.java new file mode 100644 index 00000000000..ab658b566d5 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScope.java @@ -0,0 +1,34 @@ +/* + * 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.scopes; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.Injection; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Injection.Scope +@Target(ElementType.TYPE) +public @interface CustomScope { + TypeName TYPE = TypeName.create(CustomScope.class); +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeDescribedContract.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeDescribedContract.java new file mode 100644 index 00000000000..f0543172611 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeDescribedContract.java @@ -0,0 +1,26 @@ +/* + * 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.scopes; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; + +@Injection.Describe(CustomScope.class) +@Service.Contract +interface CustomScopeDescribedContract { + String message(); +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeDescribedContractImpl.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeDescribedContractImpl.java new file mode 100644 index 00000000000..5f530af1f83 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeDescribedContractImpl.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 io.helidon.service.tests.inject.scopes; + +class CustomScopeDescribedContractImpl implements CustomScopeDescribedContract { + @Override + public String message() { + return "It works!"; + } +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeHandler.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeHandler.java new file mode 100644 index 00000000000..53835c5de13 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopeHandler.java @@ -0,0 +1,41 @@ +/* + * 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.scopes; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Scope; + +@Injection.Singleton +@Injection.NamedByType(CustomScope.class) +public class CustomScopeHandler implements Injection.ScopeHandler { + + private final AtomicReference currentScope = new AtomicReference<>(); + + @Override + public Optional currentScope() { + return Optional.ofNullable(currentScope.get()); + } + + @Override + public void activate(Scope scope) { + currentScope.set(scope); + scope.registry().activate(); + } +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopedContract.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopedContract.java new file mode 100644 index 00000000000..2f04c970e38 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopedContract.java @@ -0,0 +1,25 @@ +/* + * 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.scopes; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface CustomScopedContract { + int id(); + String message(); +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopedProducer.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopedProducer.java new file mode 100644 index 00000000000..3ea2a47ed99 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/CustomScopedProducer.java @@ -0,0 +1,44 @@ +/* + * 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.scopes; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.service.inject.api.Injection; + +@CustomScope +class CustomScopedProducer implements CustomScopedContract { + private static final AtomicInteger COUNTER = new AtomicInteger(); + + private final int id = COUNTER.incrementAndGet(); + private final CustomScopeDescribedContract describedContract; + + @Injection.Inject + CustomScopedProducer(CustomScopeDescribedContract describedContract) { + this.describedContract = describedContract; + } + + @Override + public int id() { + return id; + } + + @Override + public String message() { + return describedContract.message(); + } +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/RequestScopedContract.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/RequestScopedContract.java new file mode 100644 index 00000000000..7d66be8c55d --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/RequestScopedContract.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 io.helidon.service.tests.inject.scopes; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface RequestScopedContract { + int id(); +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/RequestScopedProducer.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/RequestScopedProducer.java new file mode 100644 index 00000000000..ab2e0301d4e --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/RequestScopedProducer.java @@ -0,0 +1,33 @@ +/* + * 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.scopes; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.service.inject.api.Injection; + +@Injection.PerRequest +class RequestScopedProducer implements RequestScopedContract { + private static final AtomicInteger COUNTER = new AtomicInteger(); + + private final int id = COUNTER.incrementAndGet(); + + @Override + public int id() { + return id; + } +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/SingletonContract.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/SingletonContract.java new file mode 100644 index 00000000000..4dfb2b9d101 --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/SingletonContract.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 io.helidon.service.tests.inject.scopes; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface SingletonContract { + int id(); +} diff --git a/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/SingletonService.java b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/SingletonService.java new file mode 100644 index 00000000000..a79d3347a4a --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/io/helidon/service/tests/inject/scopes/SingletonService.java @@ -0,0 +1,36 @@ +/* + * 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.scopes; + +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class SingletonService implements SingletonContract { + private final Supplier contract; + + @Injection.Inject + SingletonService(Supplier contract) { + this.contract = contract; + } + + @Override + public int id() { + return contract.get().id(); + } +} diff --git a/service/tests/inject/scopes/src/main/java/module-info.java b/service/tests/inject/scopes/src/main/java/module-info.java new file mode 100644 index 00000000000..2c0e5e6f0cf --- /dev/null +++ b/service/tests/inject/scopes/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +module io.helidon.service.inject.tests.scopes { + requires io.helidon.service.inject.api; + + // we use Application + requires io.helidon.service.inject; + requires io.helidon.http; + requires java.net.http; + requires io.helidon.common.context; + + exports io.helidon.service.tests.inject.scopes; +} \ No newline at end of file diff --git a/service/tests/inject/scopes/src/test/java/io/helidon/service/tests/inject/scopes/TestScopes.java b/service/tests/inject/scopes/src/test/java/io/helidon/service/tests/inject/scopes/TestScopes.java new file mode 100644 index 00000000000..fab247fee10 --- /dev/null +++ b/service/tests/inject/scopes/src/test/java/io/helidon/service/tests/inject/scopes/TestScopes.java @@ -0,0 +1,116 @@ +/* + * 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.scopes; + +import java.util.Map; +import java.util.function.Supplier; + +import io.helidon.service.inject.InjectConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Scope; +import io.helidon.service.inject.api.ScopeNotActiveException; +import io.helidon.service.inject.api.Scopes; +import io.helidon.service.registry.ServiceDescriptor; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TestScopes { + private static final Map, Object> CUSTOM_BINDINGS = + Map.of(CustomScopeDescribedContract__ServiceDescriptor.INSTANCE, + new CustomScopeDescribedContractImpl()); + + private InjectRegistryManager registryManager; + private InjectRegistry registry; + + @BeforeEach + void init() { + registryManager = InjectRegistryManager.create(InjectConfig.create()); + registry = registryManager.registry(); + } + + @AfterEach + void destroy() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void testScopeNotAvailable() { + Supplier serviceProvider = registry.supply(SingletonContract.class); + SingletonContract service = serviceProvider.get(); + + ScopeNotActiveException scopeNotAvailableException = assertThrows(ScopeNotActiveException.class, service::id); + assertThat(scopeNotAvailableException.scope(), is(Injection.PerRequest.TYPE)); + } + + @Test + void testDifferentScopeDifferentValue() { + Supplier serviceProvider = registry.supply(SingletonContract.class); + SingletonContract service = serviceProvider.get(); + + Scopes scopes = registry.get(Scopes.class); + + int id; + try (Scope ignored = scopes.createScope(Injection.PerRequest.TYPE, "test-1", Map.of())) { + id = service.id(); + assertThat("We should get a request scope based id", id, not(-1)); + } + + ScopeNotActiveException scopeNotAvailableException = assertThrows(ScopeNotActiveException.class, service::id); + assertThat("We should not be in scope when it has been closed", + scopeNotAvailableException.scope(), + is(Injection.PerRequest.TYPE)); + + try (Scope ignored = scopes.createScope(Injection.PerRequest.TYPE, "test-2", Map.of())) { + int nextId = service.id(); + assertThat("We should get a request scope based id", nextId, not(-1)); + assertThat("We should get a different request scope than last time", nextId, not(id)); + } + } + + @Test + void testDifferentScopeDifferentValueCustomScope() { + Scopes scopes = registry.get(Scopes.class); + Supplier supply = registry.supply(CustomScopedContract.class); + + int id; + try (Scope ignored = scopes.createScope(CustomScope.TYPE, "42", CUSTOM_BINDINGS)) { + id = supply.get().id(); + assertThat(supply.get().message(), is("It works!")); + } + + ScopeNotActiveException scopeNotAvailableException = assertThrows(ScopeNotActiveException.class, supply::get); + assertThat("We should not be in scope when it has been closed", + scopeNotAvailableException.scope(), + is(CustomScope.TYPE)); + + try (Scope ignored = scopes.createScope(CustomScope.TYPE, "42", CUSTOM_BINDINGS)) { + int nextId = supply.get().id(); + assertThat("We should get a different custom scope than last time", nextId, not(id)); + } + } +} diff --git a/service/tests/inject/service-lifecycle/pom.xml b/service/tests/inject/service-lifecycle/pom.xml new file mode 100644 index 00000000000..0e384a5a9a9 --- /dev/null +++ b/service/tests/inject/service-lifecycle/pom.xml @@ -0,0 +1,137 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-inject-tests-service-lifecycle + Helidon Service Tests Inject Service Lifecycle + + Tests for service lifecycle. This test validates that singletons are instantiated and injected just once, and at the right + time. This test validates that non-scoped services are instantiated for each lookup, and at the right time. This test + validates that when we limit phase to CONSTRUCTING, injection is not done + + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + + diff --git a/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/AServiceContract.java b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/AServiceContract.java new file mode 100644 index 00000000000..63222df7e35 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/AServiceContract.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 io.helidon.tests.service.inject.lifecycle; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface AServiceContract { + InjectedService service(); +} diff --git a/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/AServiceContractImpl.java b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/AServiceContractImpl.java new file mode 100644 index 00000000000..27412598ad8 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/AServiceContractImpl.java @@ -0,0 +1,44 @@ +/* + * 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.tests.service.inject.lifecycle; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.service.inject.api.Injection; + +@Injection.PerLookup +class AServiceContractImpl implements AServiceContract { + static final AtomicInteger INSTANCES = new AtomicInteger(); + static final AtomicInteger INJECTIONS = new AtomicInteger(); + + private InjectedService injectedService; + + AServiceContractImpl() { + INSTANCES.incrementAndGet(); + } + + @Override + public InjectedService service() { + return injectedService; + } + + @Injection.Inject + void setInjectedService(InjectedService injectedService) { + this.injectedService = injectedService; + INJECTIONS.incrementAndGet(); + } +} diff --git a/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/ASingletonContract.java b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/ASingletonContract.java new file mode 100644 index 00000000000..967d23d0ef3 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/ASingletonContract.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 io.helidon.tests.service.inject.lifecycle; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface ASingletonContract { + InjectedService service(); +} diff --git a/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/ASingletonContractImpl.java b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/ASingletonContractImpl.java new file mode 100644 index 00000000000..4620c7e9462 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/ASingletonContractImpl.java @@ -0,0 +1,44 @@ +/* + * 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.tests.service.inject.lifecycle; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class ASingletonContractImpl implements ASingletonContract { + static final AtomicInteger INSTANCES = new AtomicInteger(); + static final AtomicInteger INJECTIONS = new AtomicInteger(); + + private InjectedService injectedService; + + ASingletonContractImpl() { + INSTANCES.incrementAndGet(); + } + + @Override + public InjectedService service() { + return injectedService; + } + + @Injection.Inject + void setInjectedService(InjectedService injectedService) { + this.injectedService = injectedService; + INJECTIONS.incrementAndGet(); + } +} diff --git a/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/InjectedService.java b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/InjectedService.java new file mode 100644 index 00000000000..6ca0b8972e7 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/main/java/io/helidon/tests/service/inject/lifecycle/InjectedService.java @@ -0,0 +1,23 @@ +/* + * 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.tests.service.inject.lifecycle; + +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +class InjectedService { +} diff --git a/service/tests/inject/service-lifecycle/src/main/java/module-info.java b/service/tests/inject/service-lifecycle/src/main/java/module-info.java new file mode 100644 index 00000000000..9494710f2c0 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +open module io.helidon.service.inject.tests.service.lifecycle { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + +} \ No newline at end of file diff --git a/service/tests/inject/service-lifecycle/src/test/java/io/helidon/tests/service/inject/lifecycle/ServiceLifecycleLimitPhaseTest.java b/service/tests/inject/service-lifecycle/src/test/java/io/helidon/tests/service/inject/lifecycle/ServiceLifecycleLimitPhaseTest.java new file mode 100644 index 00000000000..4e7320b35f9 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/test/java/io/helidon/tests/service/inject/lifecycle/ServiceLifecycleLimitPhaseTest.java @@ -0,0 +1,150 @@ +/* + * 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.tests.service.inject.lifecycle; + +import java.util.function.Supplier; + +import io.helidon.service.inject.InjectConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.Activator.Phase; +import io.helidon.service.inject.api.InjectRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +class ServiceLifecycleLimitPhaseTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + static void initClass() { + ASingletonContractImpl.INJECTIONS.set(0); + ASingletonContractImpl.INSTANCES.set(0); + AServiceContractImpl.INJECTIONS.set(0); + AServiceContractImpl.INSTANCES.set(0); + + registryManager = InjectRegistryManager.create(InjectConfig.builder() + .limitRuntimePhase(Phase.CONSTRUCTING) + .build()); + registry = registryManager.registry(); + + assertThat(registry, notNullValue()); + } + + @AfterAll + static void destroyClass() { + if (registryManager != null) { + registryManager.shutdown(); + } + registryManager = null; + registry = null; + } + + @Test + void singletonLifecycleTest() { + // there should be no instances "just" created when registry starts + assertThat(ASingletonContractImpl.INSTANCES.get(), is(0)); + + Supplier supplier = registry.supply(ASingletonContract.class); + + // there should be no instances created even when I lookup a supplier + assertThat(ASingletonContractImpl.INSTANCES.get(), is(0)); + assertThat(supplier, notNullValue()); + + ASingletonContract instance = supplier.get(); + + assertThat(instance, notNullValue()); + + // now we should have created a single instance + assertThat(ASingletonContractImpl.INSTANCES.get(), is(1)); + assertThat(ASingletonContractImpl.INJECTIONS.get(), is(0)); + + // now on the next lookup, I should get the same instance + supplier = registry.supply(ASingletonContract.class); + ASingletonContract secondInstance = supplier.get(); + + // must be the same + assertThat(secondInstance, sameInstance(instance)); + // and we still should have created only a single instance + assertThat(ASingletonContractImpl.INSTANCES.get(), is(1)); + assertThat(ASingletonContractImpl.INJECTIONS.get(), is(0)); + + InjectedService firstService = instance.service(); + assertThat(firstService, nullValue()); + InjectedService secondService = secondInstance.service(); + assertThat(secondService, nullValue()); + } + + @Test + void serviceLifecycleTest() { + // there should be no instances "just" created when registry starts + assertThat(AServiceContractImpl.INSTANCES.get(), is(0)); + + Supplier supplier = registry.supply(AServiceContract.class); + + // there should be no instances created even when I lookup a supplier + assertThat(AServiceContractImpl.INSTANCES.get(), is(0)); + assertThat(supplier, notNullValue()); + + AServiceContract instance = supplier.get(); + + assertThat(instance, notNullValue()); + + // now we should have created a single instance + assertThat(AServiceContractImpl.INSTANCES.get(), is(1)); + assertThat(AServiceContractImpl.INJECTIONS.get(), is(0)); + + AServiceContract secondInstance = supplier.get(); + + // now we should have created another instance + assertThat(AServiceContractImpl.INSTANCES.get(), is(2)); + assertThat(AServiceContractImpl.INJECTIONS.get(), is(0)); + + // and it should be a different instance + assertThat(secondInstance, not(sameInstance(instance))); + + // now on the next lookup, we should get another instance + supplier = registry.supply(AServiceContract.class); + AServiceContract thirdInstance = supplier.get(); + + // and another instance created + assertThat(AServiceContractImpl.INSTANCES.get(), is(3)); + assertThat(AServiceContractImpl.INJECTIONS.get(), is(0)); + + // must not be the same + assertThat(thirdInstance, not(sameInstance(instance))); + assertThat(thirdInstance, not(sameInstance(secondInstance))); + + // now validate the injected singleton + InjectedService service = instance.service(); + assertThat(service, nullValue()); + + InjectedService secondService = secondInstance.service(); + assertThat(secondService, nullValue()); + + InjectedService thirdService = thirdInstance.service(); + assertThat(thirdService, nullValue()); + } +} diff --git a/service/tests/inject/service-lifecycle/src/test/java/io/helidon/tests/service/inject/lifecycle/ServiceLifecycleTest.java b/service/tests/inject/service-lifecycle/src/test/java/io/helidon/tests/service/inject/lifecycle/ServiceLifecycleTest.java new file mode 100644 index 00000000000..3d897d538d7 --- /dev/null +++ b/service/tests/inject/service-lifecycle/src/test/java/io/helidon/tests/service/inject/lifecycle/ServiceLifecycleTest.java @@ -0,0 +1,147 @@ +/* + * 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.tests.service.inject.lifecycle; + +import java.util.function.Supplier; + +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +class ServiceLifecycleTest { + private static InjectRegistryManager registryManager; + private static InjectRegistry registry; + + @BeforeAll + static void initClass() { + ASingletonContractImpl.INJECTIONS.set(0); + ASingletonContractImpl.INSTANCES.set(0); + AServiceContractImpl.INJECTIONS.set(0); + AServiceContractImpl.INSTANCES.set(0); + + registryManager = InjectRegistryManager.create(); + registry = registryManager.registry(); + + assertThat(registry, notNullValue()); + } + + @AfterAll + static void destroyClass() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void singletonLifecycleTest() { + // there should be no instances "just" created when registry starts + assertThat(ASingletonContractImpl.INSTANCES.get(), is(0)); + + Supplier supplier = registry.supply(ASingletonContract.class); + + // there should be no instances created even when I lookup a supplier + assertThat(ASingletonContractImpl.INSTANCES.get(), is(0)); + assertThat(supplier, notNullValue()); + + ASingletonContract instance = supplier.get(); + + assertThat(instance, notNullValue()); + + // now we should have created a single instance + assertThat(ASingletonContractImpl.INSTANCES.get(), is(1)); + assertThat(ASingletonContractImpl.INJECTIONS.get(), is(1)); + + // now on the next lookup, I should get the same instance + supplier = registry.supply(ASingletonContract.class); + ASingletonContract secondInstance = supplier.get(); + + // must be the same + assertThat(secondInstance, sameInstance(instance)); + // and we still should have created only a single instance + assertThat(ASingletonContractImpl.INSTANCES.get(), is(1)); + assertThat(ASingletonContractImpl.INJECTIONS.get(), is(1)); + + InjectedService firstService = instance.service(); + assertThat(firstService, notNullValue()); + InjectedService secondService = secondInstance.service(); + assertThat(secondService, notNullValue()); + assertThat(secondService, sameInstance(firstService)); + } + + @Test + void serviceLifecycleTest() { + // there should be no instances "just" created when registry starts + assertThat(AServiceContractImpl.INSTANCES.get(), is(0)); + + Supplier supplier = registry.supply(AServiceContract.class); + + // there should be no instances created even when I lookup a supplier + assertThat(AServiceContractImpl.INSTANCES.get(), is(0)); + assertThat(supplier, notNullValue()); + + AServiceContract instance = supplier.get(); + + assertThat(instance, notNullValue()); + + // now we should have created a single instance + assertThat(AServiceContractImpl.INSTANCES.get(), is(1)); + assertThat(AServiceContractImpl.INJECTIONS.get(), is(1)); + + AServiceContract secondInstance = supplier.get(); + + // now we should have created another instance + assertThat(AServiceContractImpl.INSTANCES.get(), is(2)); + assertThat(AServiceContractImpl.INJECTIONS.get(), is(2)); + + // and it should be a different instance + assertThat(secondInstance, not(sameInstance(instance))); + + // now on the next lookup, we should get another instance + supplier = registry.supply(AServiceContract.class); + AServiceContract thirdInstance = supplier.get(); + + // and another instance created + assertThat(AServiceContractImpl.INSTANCES.get(), is(3)); + assertThat(AServiceContractImpl.INJECTIONS.get(), is(3)); + + // must not be the same + assertThat(thirdInstance, not(sameInstance(instance))); + assertThat(thirdInstance, not(sameInstance(secondInstance))); + + // now validate the injected singleton + InjectedService service = instance.service(); + assertThat(service, notNullValue()); + + InjectedService secondService = secondInstance.service(); + assertThat(secondService, notNullValue()); + + InjectedService thirdService = thirdInstance.service(); + assertThat(thirdService, notNullValue()); + + assertThat(secondService, sameInstance(service)); + assertThat(thirdService, sameInstance(service)); + } +} diff --git a/service/tests/inject/stacking/pom.xml b/service/tests/inject/stacking/pom.xml new file mode 100644 index 00000000000..95ce2725ff3 --- /dev/null +++ b/service/tests/inject/stacking/pom.xml @@ -0,0 +1,139 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-stacking + Helidon Service Tests Inject Stacking + + Tests for service stacking. This test validates that we can have services that implement the same contract stacked. + + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + io.helidon.config + helidon-config + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + diff --git a/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/CommonContract.java b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/CommonContract.java new file mode 100644 index 00000000000..c8149f2d8d5 --- /dev/null +++ b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/CommonContract.java @@ -0,0 +1,32 @@ +/* + * 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.tests.inject.stacking; + +import io.helidon.service.registry.Service; + +/** + * All implementors will implement this {@link io.helidon.service.registry.Service.Contract}, + * but using varying {@link io.helidon.common.Weight}'s. + */ +@Service.Contract +public interface CommonContract { + + CommonContract getInner(); + + String sayHello(String arg); + +} diff --git a/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/CommonContractImpl.java b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/CommonContractImpl.java new file mode 100644 index 00000000000..e648e42dde7 --- /dev/null +++ b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/CommonContractImpl.java @@ -0,0 +1,47 @@ +/* + * 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.tests.inject.stacking; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Injection.RunLevel(1) +@Weight(Weighted.DEFAULT_WEIGHT + 1) +public class CommonContractImpl implements CommonContract { + + private final CommonContract inner; + + @Injection.Inject + public CommonContractImpl(Optional inner) { + this.inner = inner.orElse(null); + } + + @Override + public CommonContract getInner() { + return inner; + } + + @Override + public String sayHello(String arg) { + return getClass().getSimpleName() + ":" + (inner != null ? inner.sayHello(arg) : arg); + } + +} diff --git a/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/MostOuterCommonContractImpl.java b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/MostOuterCommonContractImpl.java new file mode 100644 index 00000000000..3798a2889f1 --- /dev/null +++ b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/MostOuterCommonContractImpl.java @@ -0,0 +1,34 @@ +/* + * 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.tests.inject.stacking; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 3) +public class MostOuterCommonContractImpl extends OuterCommonContractImpl { + + @Injection.Inject + public MostOuterCommonContractImpl(Optional inner) { + super(inner); + } + +} diff --git a/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/OuterCommonContractImpl.java b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/OuterCommonContractImpl.java new file mode 100644 index 00000000000..c534b6f182f --- /dev/null +++ b/service/tests/inject/stacking/src/main/java/io/helidon/service/tests/inject/stacking/OuterCommonContractImpl.java @@ -0,0 +1,34 @@ +/* + * 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.tests.inject.stacking; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; + +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 2) +public class OuterCommonContractImpl extends CommonContractImpl { + + @Injection.Inject + public OuterCommonContractImpl(Optional inner) { + super(inner); + } + +} diff --git a/service/tests/inject/stacking/src/main/java/module-info.java b/service/tests/inject/stacking/src/main/java/module-info.java new file mode 100644 index 00000000000..da29fb5efd5 --- /dev/null +++ b/service/tests/inject/stacking/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +module io.helidon.service.tests.inject.stacking { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + + // we use Application + requires io.helidon.service.inject; + requires io.helidon.http; + requires io.helidon.common.context; + + exports io.helidon.service.tests.inject.stacking; +} \ No newline at end of file diff --git a/service/tests/inject/toolbox/pom.xml b/service/tests/inject/toolbox/pom.xml new file mode 100644 index 00000000000..5cc2a7b0322 --- /dev/null +++ b/service/tests/inject/toolbox/pom.xml @@ -0,0 +1,146 @@ + + + + + + io.helidon.service.tests.inject + helidon-service-tests-inject-project + 4.2.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-service-tests-inject-toolbox + Helidon Service Tests Inject Toolbox + Tests for a more complex set of services + + + + io.helidon.service + helidon-service-registry + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.metrics + helidon-metrics + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-jakarta-not-included + + enforce + + + + + + jakarta.inject:jakarta.inject-api + jakarta.annotation:jakarta.annotation-api + + + + true + + + + + + + diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/AbstractBlade.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/AbstractBlade.java new file mode 100644 index 00000000000..36ca0a4108d --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/AbstractBlade.java @@ -0,0 +1,28 @@ +/* + * 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.tests.inject.toolbox; + +import io.helidon.service.inject.api.Injection; + +public abstract class AbstractBlade { + + // intended to be a void injection point + @Injection.Inject + protected AbstractBlade() { + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/AbstractSaw.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/AbstractSaw.java new file mode 100644 index 00000000000..4fd55c3e8a8 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/AbstractSaw.java @@ -0,0 +1,108 @@ +/* + * 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.tests.inject.toolbox; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; +import io.helidon.service.tests.inject.toolbox.impl.DullBlade; + +@Service.Contract +public abstract class AbstractSaw extends Verification implements Tool { + @Injection.Inject protected Supplier fieldInjectedProtectedProviderInAbstractBase; + @Injection.Inject protected Optional fieldInjectedProtectedOptionalInAbstractBase; + @Injection.Inject protected List fieldInjectedProtectedListInAbstractBase; + @Injection.Inject protected List fieldInjectedProtectedProviderListInAbstractBase; + + @Injection.Inject Supplier fieldInjectedPkgPrivateProviderInAbstractBase; + @Injection.Inject Optional fieldInjectedPkgPrivateOptionalInAbstractBase; + @Injection.Inject List fieldInjectedPkgPrivateListInAbstractBase; + @Injection.Inject List> fieldInjectedPkgPrivateProviderListInAbstractBase; + + Supplier setterInjectedPkgPrivateProviderInAbstractBase; + Optional setterInjectedPkgPrivateOptionalInAbstractBase; + List setterInjectedPkgPrivateListInAbstractBase; + List> setterInjectedPkgPrivateProviderListInAbstractBase; + + int setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount; + int setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount; + int setterInjectedPkgPrivateListInAbstractBaseInjectedCount; + int setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount; + + @Injection.Inject + public void setBladeProviders(List> blades) { + setterInjectedPkgPrivateProviderListInAbstractBase = blades; + setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount++; + } + + public void verifyState() { + verifyInjected(fieldInjectedProtectedOptionalInAbstractBase, getClass() + + ".fieldInjectedProtectedOptionalInAbstractBase", null, true, DullBlade.class); + // we use cardinality of the InjectionPointProvider + verifyInjected(fieldInjectedProtectedProviderInAbstractBase, getClass() + + ".fieldInjectedProtectedProviderInAbstractBase", null, false, DullBlade.class); + verifyInjected(fieldInjectedProtectedListInAbstractBase, getClass() + + ".fieldInjectedProtectedListInAbstractBase", null, 1, AbstractBlade.class); + verifyInjected(setterInjectedPkgPrivateProviderListInAbstractBase, getClass() + + ".setterInjectedPkgPrivateProviderListInAbstractBase", null, 1, Supplier.class); + + // we use cardinality of the InjectionPointProvider + verifyInjected(fieldInjectedPkgPrivateProviderInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateProviderInAbstractBase", null, false, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateOptionalInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateOptionalInAbstractBase", null, true, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateListInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateListInAbstractBase", null, 1, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateProviderListInAbstractBase", null, 1, Supplier.class); + + // we use cardinality of the InjectionPointProvider + verifyInjected(setterInjectedPkgPrivateProviderInAbstractBase, getClass() + + ".setBladeProvider(Provider blade)", + setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount, false, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateOptionalInAbstractBase, getClass() + + ".setBladeOptional(Optional blade)", + setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateListInAbstractBase, getClass() + + ".setBladeList(List blades)", + setterInjectedPkgPrivateListInAbstractBaseInjectedCount, 1, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateProviderListInAbstractBase", null, 1, Supplier.class); + } + + @Injection.Inject + void setBladeProvider(Supplier blade) { + setterInjectedPkgPrivateProviderInAbstractBase = blade; + setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount++; + } + + @Injection.Inject + void setBladeOptional(Optional blade) { + setterInjectedPkgPrivateOptionalInAbstractBase = blade; + setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount++; + } + + @Injection.Inject + void setBladeList(List blades) { + setterInjectedPkgPrivateListInAbstractBase = blades; + setterInjectedPkgPrivateListInAbstractBaseInjectedCount++; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Awl.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Awl.java new file mode 100644 index 00000000000..f4b167f3b49 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Awl.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. + */ + +package io.helidon.service.tests.inject.toolbox; + +import io.helidon.service.registry.Service; + +/** + * Testing. + */ +@Service.Contract +public interface Awl extends Tool { +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Hammer.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Hammer.java new file mode 100644 index 00000000000..7365bbdf0b8 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Hammer.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. + */ + +package io.helidon.service.tests.inject.toolbox; + +import io.helidon.service.registry.Service; + +/** + * Testing. + */ +@Service.Contract +public interface Hammer extends Tool { +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Lubricant.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Lubricant.java new file mode 100644 index 00000000000..d705537d49f --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Lubricant.java @@ -0,0 +1,25 @@ +/* + * 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.tests.inject.toolbox; + +/** + * Testing. + */ +// @Singleton -- intentionally not declared to be a contract +public interface Lubricant { + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Preferred.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Preferred.java new file mode 100644 index 00000000000..ecb82dfffd6 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Preferred.java @@ -0,0 +1,40 @@ +/* + * 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.tests.inject.toolbox; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import io.helidon.service.inject.api.Injection; + +/** + * Custom qualifier. + */ +@Injection.Qualifier +@Documented +@Retention(RetentionPolicy.CLASS) +public @interface Preferred { + + /** + * Testing. + * + * @return for testing + */ + String value() default ""; + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/SomeOtherLocalNonContractInterface1.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/SomeOtherLocalNonContractInterface1.java new file mode 100644 index 00000000000..a77e623c517 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/SomeOtherLocalNonContractInterface1.java @@ -0,0 +1,24 @@ +/* + * 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.tests.inject.toolbox; + +/** + * Testing. + */ +public interface SomeOtherLocalNonContractInterface1 { + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/TableSaw.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/TableSaw.java new file mode 100644 index 00000000000..ffa48756b2d --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/TableSaw.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.tests.inject.toolbox; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.impl.CoarseBlade; +import io.helidon.service.tests.inject.toolbox.impl.DullBlade; + +/** + * Intentionally in the same package as {AbstractSaw}. + */ +@Injection.Singleton +class TableSaw extends AbstractSaw { + + @Injection.Inject + @Injection.Named(CoarseBlade.NAME) + Supplier coarseBladeFieldInjectedPkgPrivateProviderInSubClass; + + @Injection.Inject + @Injection.Named(CoarseBlade.NAME) + Optional coarseBladeFieldInjectedPkgPrivateOptionalInSubClass; + + @Injection.Inject + @Injection.Named(CoarseBlade.NAME) + List coarseBladeFieldInjectedPkgPrivateListInSubClass; + + @Injection.Inject + @Injection.Named(CoarseBlade.NAME) + List> coarseBladeFieldInjectedPkgPrivateProviderListInSubClass; + + Supplier setterInjectedPkgPrivateProviderInSubClass; + Optional setterInjectedPkgPrivateOptionalInSubClass; + List setterInjectedPkgPrivateListInSubClass; + List> setterInjectedPkgPrivateProviderListInSubClass; + int setterInjectedPkgPrivateProviderInSubClassInjectedCount; + int setterInjectedPkgPrivateOptionalInSubClassInjectedCount; + int setterInjectedPkgPrivateListInSubClassInjectedCount; + int setterInjectedPkgPrivateProviderListInSubClassInjectedCount; + private Optional ctorInjectedLubricantInSubClass; + private Optional setterInjectedLubricantInSubClass; + private int setterInjectedLubricantInSubClassInjectedCount; + + TableSaw() { + } + + @Injection.Inject + public TableSaw(Optional lubricant) { + ctorInjectedLubricantInSubClass = lubricant; + } + + @Override + public String name() { + return getClass().getSimpleName(); + } + + @Override + public void verifyState() { + verifyInjected(ctorInjectedLubricantInSubClass, getClass() + ".", null, false, null); + verifyInjected(setterInjectedLubricantInSubClass, + getClass() + ".injectLubricant(Optional lubricant)", + setterInjectedLubricantInSubClassInjectedCount, + false, + null); + + // injection point provider uses cardinality of the provider implementation (scope is not handled by registry) + verifyInjected(coarseBladeFieldInjectedPkgPrivateProviderInSubClass, getClass() + + ".coarseBladeFieldInjectedPkgPrivateProviderInSubClass", null, false, CoarseBlade.class); + verifyInjected(coarseBladeFieldInjectedPkgPrivateOptionalInSubClass, getClass() + + ".coarseBladeFieldInjectedPkgPrivateOptionalInSubClass", null, true, CoarseBlade.class); + verifyInjected(coarseBladeFieldInjectedPkgPrivateListInSubClass, getClass() + + ".coarseBladeFieldInjectedPkgPrivateListInSubClass", null, 1, CoarseBlade.class); + + // injection point provider uses cardinality of the provider implementation (scope is not handled by registry) + verifyInjected(setterInjectedPkgPrivateProviderInSubClass, + getClass() + + ".setBladeProvider(Provider blade)", + setterInjectedPkgPrivateProviderInSubClassInjectedCount, + false, + DullBlade.class); + verifyInjected(setterInjectedPkgPrivateOptionalInSubClass, + getClass() + + ".setBladeOptional(Optional blade)", + setterInjectedPkgPrivateOptionalInSubClassInjectedCount, + true, + DullBlade.class); + verifyInjected(setterInjectedPkgPrivateListInSubClass, + getClass() + + ".setAllBladesInSubclass(List blades)", + setterInjectedPkgPrivateListInSubClassInjectedCount, + 3, + AbstractBlade.class); + + super.verifyState(); + } + + @Injection.Inject + protected void injectLubricant(Optional lubricant) { + setterInjectedLubricantInSubClass = lubricant; + setterInjectedLubricantInSubClassInjectedCount++; + } + + @Injection.Inject + void setBladeProviderInSubclass(Supplier blade) { + setterInjectedPkgPrivateProviderInSubClass = blade; + setterInjectedPkgPrivateProviderInSubClassInjectedCount++; + } + + @Injection.Inject + void setBladeOptionalInSubclass(Optional blade) { + setterInjectedPkgPrivateOptionalInSubClass = blade; + setterInjectedPkgPrivateOptionalInSubClassInjectedCount++; + } + + @Injection.Inject + void setAllBladesInSubclass(@Injection.Named("*") List blades) { + setterInjectedPkgPrivateListInSubClass = blades; + setterInjectedPkgPrivateListInSubClassInjectedCount++; + } + + @Injection.Inject + void setBladeProviderListInSubclass(List> blades) { + setterInjectedPkgPrivateProviderListInSubClass = blades; + setterInjectedPkgPrivateProviderListInSubClassInjectedCount++; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Tool.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Tool.java new file mode 100644 index 00000000000..7c28f79e03d --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Tool.java @@ -0,0 +1,33 @@ +/* + * 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.tests.inject.toolbox; + +import io.helidon.service.registry.Service; + +/** + * Testing. + */ +@Service.Contract +public interface Tool { + + /** + * Testing. + * + * @return for testing + */ + String name(); +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/ToolBox.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/ToolBox.java new file mode 100644 index 00000000000..b672afa8c99 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/ToolBox.java @@ -0,0 +1,44 @@ +/* + * 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.tests.inject.toolbox; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.service.registry.Service; + +/** + * Testing. + */ +@Service.Contract +public interface ToolBox { + + /** + * Testing. + * + * @return for testing + */ + List> toolsInBox(); + + /** + * Testing. + * + * @return for testing + */ + Supplier preferredHammer(); + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Verification.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Verification.java new file mode 100644 index 00000000000..a77b59bbf1c --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/Verification.java @@ -0,0 +1,131 @@ +/* + * 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.toolbox; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Testing. + */ +public class Verification { + /** + * Testing. + * + * @param injectee injectee + * @param tag tag + * @param injectedCount injectedCount + * @param expected expected + * @param expectedType expectedType + */ + public static void verifyInjected(Optional injectee, + String tag, + Integer injectedCount, + boolean expected, + Class expectedType) { + if (expected && injectee.isEmpty()) { + throw new AssertionError(tag + " was expected to be present"); + } else if (!expected && injectee.isPresent()) { + throw new AssertionError(tag + " was not expected to be present"); + } + + if (expectedType != null && expected && !expectedType.isInstance(injectee.get())) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + injectee); + } + + if (injectedCount != null && injectedCount != 1) { + throw new AssertionError(tag + + " was was expected to be injected 1 time; it was actually injected " + injectedCount + " times"); + } + } + + /** + * Testing. + * + * @param injectee injectee + * @param tag tag + * @param injectedCount injectedCount + * @param expectedSingleton expectedSingleton + * @param expectedType expectedType + */ + public static void verifyInjected(Supplier injectee, + String tag, + Integer injectedCount, + boolean expectedSingleton, + Class expectedType) { + Objects.requireNonNull(injectee, tag + " was not injected"); + Object provided = Objects.requireNonNull(injectee.get(), tag + " was expected to be provided"); + + if (expectedType != null && !expectedType.isInstance(provided)) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + provided); + } + + Object provided2 = injectee.get(); + if (expectedSingleton && provided != provided2) { + throw new AssertionError(tag + " was expected to be a singleton provided type"); + } + if (expectedType != null && !(expectedType.isInstance(provided2))) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + provided2); + } + + if (injectedCount != null && injectedCount != 1) { + throw new AssertionError(tag + + " was was expected to be injected 1 time; it was actually injected " + + injectedCount + " times"); + } + } + + /** + * Testing. + * + * @param injectee injectee + * @param tag tag + * @param injectedCount injectedCount + * @param expectedSize expectedSize + * @param expectedType expectedType + */ + public static void verifyInjected(List injectee, + String tag, + Integer injectedCount, + int expectedSize, + Class expectedType) { + Objects.requireNonNull(injectee, tag + " was not injected"); + + int size = injectee.size(); + if (size != expectedSize) { + throw new AssertionError(tag + " was expected to be size of " + expectedSize + + " but instead was injected with: " + injectee); + } + + if (injectedCount != null && injectedCount != 1) { + throw new AssertionError(tag + + " was was expected to be injected 1 time; it was actually injected " + + injectedCount + " times"); + } + + if (expectedType != null) { + injectee.forEach(item -> { + if (!expectedType.isInstance(item)) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + item); + } + }); + } + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/AwlImpl.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/AwlImpl.java new file mode 100644 index 00000000000..8de51278704 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/AwlImpl.java @@ -0,0 +1,32 @@ +/* + * 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.tests.inject.toolbox.impl; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.Awl; + +@Injection.Singleton +public class AwlImpl implements Awl { + @Injection.Inject + AwlImpl() { + } + + @Override + public String name() { + return "awl"; + } +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/BigHammer.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/BigHammer.java new file mode 100644 index 00000000000..ca26e078c44 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/BigHammer.java @@ -0,0 +1,38 @@ +/* + * 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.tests.inject.toolbox.impl; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.Hammer; +import io.helidon.service.tests.inject.toolbox.Preferred; + +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 1) +@Injection.Named(BigHammer.NAME) +@Preferred +public class BigHammer implements Hammer { + + public static final String NAME = "big"; + + @Override + public String name() { + return NAME + " hammer"; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/BladeProvider.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/BladeProvider.java new file mode 100644 index 00000000000..d73b65129e5 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/BladeProvider.java @@ -0,0 +1,83 @@ +/* + * 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.tests.inject.toolbox.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import io.helidon.common.types.TypeName; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Injection.InjectionPointFactory; +import io.helidon.service.inject.api.Injection.QualifiedInstance; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.inject.api.Qualifier; +import io.helidon.service.tests.inject.toolbox.AbstractBlade; + +/** + * Provides contextual injection for blades. + */ +@Injection.Singleton +@Injection.Named("*") +public class BladeProvider implements InjectionPointFactory { + + static final Qualifier QUALIFIER_ALL = Qualifier.WILDCARD_NAMED; + static final Qualifier QUALIFIER_COARSE = Qualifier.createNamed("coarse"); + static final Qualifier QUALIFIER_FINE = Qualifier.createNamed("fine"); + static final Qualifier QUALIFIER_DULL = Qualifier.createNamed("dull"); + + @Override + public Optional> first(Lookup lookup) { + assert (lookup.contracts().size() == 1) : lookup; + assert (lookup.contracts().contains(TypeName.create(AbstractBlade.class))) : lookup; + + AbstractBlade blade; + Qualifier qualifier; + if (lookup.qualifiers().contains(QUALIFIER_ALL) || lookup.qualifiers().contains(QUALIFIER_COARSE)) { + qualifier = QUALIFIER_COARSE; + blade = new CoarseBlade(); + } else if (lookup.qualifiers().contains(QUALIFIER_FINE)) { + qualifier = QUALIFIER_FINE; + blade = new FineBlade(); + } else { + assert (lookup.qualifiers().isEmpty()); + qualifier = QUALIFIER_DULL; + blade = new DullBlade(); + } + + return Optional.of(QualifiedInstance.create(blade, qualifier)); + } + + @Override + public List> list(Lookup lookup) { + List> result = new ArrayList<>(); + if (lookup.qualifiers().contains(QUALIFIER_ALL) || lookup.qualifiers().contains(QUALIFIER_COARSE)) { + result.add(QualifiedInstance.create(new CoarseBlade(), QUALIFIER_COARSE)); + } + + if (lookup.qualifiers().contains(QUALIFIER_ALL) || lookup.qualifiers().contains(QUALIFIER_FINE)) { + result.add(QualifiedInstance.create(new FineBlade(), QUALIFIER_FINE)); + } + + if (lookup.qualifiers().contains(QUALIFIER_ALL) || lookup.qualifiers().isEmpty()) { + result.add(QualifiedInstance.create(new DullBlade(), QUALIFIER_DULL)); + } + + return result; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/CoarseBlade.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/CoarseBlade.java new file mode 100644 index 00000000000..2eaf796bf69 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/CoarseBlade.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. + */ + +package io.helidon.service.tests.inject.toolbox.impl; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.AbstractBlade; + +@Injection.Named(CoarseBlade.NAME) +public class CoarseBlade extends AbstractBlade { + + public static final String NAME = "coarse"; +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/DullBlade.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/DullBlade.java new file mode 100644 index 00000000000..d2dae20cc95 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/DullBlade.java @@ -0,0 +1,28 @@ +/* + * 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.tests.inject.toolbox.impl; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.AbstractBlade; + +/** + * When a particular blade name is not "asked for" explicitly then we give out a dull blade. + */ +@Injection.Named("dull blade") +public class DullBlade extends AbstractBlade { + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/FineBlade.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/FineBlade.java new file mode 100644 index 00000000000..0a4d4d4af39 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/FineBlade.java @@ -0,0 +1,27 @@ +/* + * 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.tests.inject.toolbox.impl; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.AbstractBlade; + +@Injection.Named(FineBlade.NAME) +public class FineBlade extends AbstractBlade { + + static final String NAME = "fine"; + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/HandSaw.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/HandSaw.java new file mode 100644 index 00000000000..d2a12b0dd4b --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/HandSaw.java @@ -0,0 +1,124 @@ +/* + * 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.tests.inject.toolbox.impl; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.AbstractBlade; +import io.helidon.service.tests.inject.toolbox.AbstractSaw; +import io.helidon.service.tests.inject.toolbox.Lubricant; +import io.helidon.service.tests.inject.toolbox.Verification; + +/** + * Kept intentionally in a different package from {@link AbstractSaw} for testing. + */ +@Injection.Singleton +public class HandSaw extends AbstractSaw { + + @Injection.Inject @Injection.Named(FineBlade.NAME) Supplier fineBladeFieldInjectedPkgPrivateProviderInSubClass; + @Injection.Inject @Injection.Named(FineBlade.NAME) Optional fineBladeFieldInjectedPkgPrivateOptionalInSubClass; + @Injection.Inject @Injection.Named(FineBlade.NAME) List fineBladeFieldInjectedPkgPrivateListInSubClass; + Supplier setterInjectedPkgPrivateProviderInSubClass; + Optional setterInjectedPkgPrivateOptionalInSubClass; + List setterInjectedPkgPrivateListInSubClass; + + int setterInjectedPkgPrivateProviderInSubClassInjectedCount; + int setterInjectedPkgPrivateOptionalInSubClassInjectedCount; + int setterInjectedPkgPrivateListInSubClassInjectedCount; + + private Optional ctorInjectedLubricantInSubClass; + private Optional setterInjectedLubricantInSubClass; + private int setterInjectedLubricantInSubClassInjectedCount; + + HandSaw() { + } + + @Injection.Inject + public HandSaw(Optional lubricant) { + ctorInjectedLubricantInSubClass = lubricant; + } + + @Override + public String name() { + return getClass().getSimpleName(); + } + + @Override + public void verifyState() { + Verification.verifyInjected(ctorInjectedLubricantInSubClass, getClass() + + ".", null, false, null); + Verification.verifyInjected(setterInjectedLubricantInSubClass, getClass() + + ".injectLubricant(Optional lubricant)", setterInjectedLubricantInSubClassInjectedCount, false, null); + + // we use cardinality of the InjectionPointProvider + Verification.verifyInjected(fineBladeFieldInjectedPkgPrivateProviderInSubClass, getClass() + + ".fineBladeFieldInjectedPkgPrivateProviderInSubClass", null, false, FineBlade.class); + Verification.verifyInjected(fineBladeFieldInjectedPkgPrivateOptionalInSubClass, getClass() + + ".fineBladeFieldInjectedPkgPrivateOptionalInSubClass", null, true, FineBlade.class); + Verification.verifyInjected(fineBladeFieldInjectedPkgPrivateListInSubClass, getClass() + + ".fineBladeFieldInjectedPkgPrivateListInSubClass", null, 1, FineBlade.class); + + // we use cardinality of the InjectionPointProvider + Verification.verifyInjected(setterInjectedPkgPrivateProviderInSubClass, + getClass() + + ".setBladeProvider(Provider blade)", + setterInjectedPkgPrivateProviderInSubClassInjectedCount, + false, + DullBlade.class); + Verification.verifyInjected(setterInjectedPkgPrivateOptionalInSubClass, + getClass() + + ".setBladeOptional(Optional blade)", + setterInjectedPkgPrivateOptionalInSubClassInjectedCount, + true, + DullBlade.class); + Verification.verifyInjected(setterInjectedPkgPrivateListInSubClass, + getClass() + + ".setBladeList(List blades)", + setterInjectedPkgPrivateListInSubClassInjectedCount, + 1, + AbstractBlade.class); + + super.verifyState(); + } + + @Injection.Inject + protected void injectLubricant(Optional lubricant) { + setterInjectedLubricantInSubClass = lubricant; + setterInjectedLubricantInSubClassInjectedCount++; + } + + @Injection.Inject + void setBladeProvider(Supplier blade) { + setterInjectedPkgPrivateProviderInSubClass = blade; + setterInjectedPkgPrivateProviderInSubClassInjectedCount++; + } + + @Injection.Inject + void setBladeOptional(Optional blade) { + setterInjectedPkgPrivateOptionalInSubClass = blade; + setterInjectedPkgPrivateOptionalInSubClassInjectedCount++; + } + + @Injection.Inject + void setBladeList(List blades) { + setterInjectedPkgPrivateListInSubClass = blades; + setterInjectedPkgPrivateListInSubClassInjectedCount++; + } +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/LittleHammer.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/LittleHammer.java new file mode 100644 index 00000000000..9e5c55253c7 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/LittleHammer.java @@ -0,0 +1,33 @@ +/* + * 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.tests.inject.toolbox.impl; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.Hammer; + +@Injection.Singleton +@Injection.Named(LittleHammer.NAME) +public class LittleHammer implements Hammer { + + public static final String NAME = "little"; + + @Override + public String name() { + return NAME + " hammer"; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/MainToolBox.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/MainToolBox.java new file mode 100644 index 00000000000..1c80be1e943 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/MainToolBox.java @@ -0,0 +1,100 @@ +/* + * 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.tests.inject.toolbox.impl; + +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; +import io.helidon.service.tests.inject.toolbox.Hammer; +import io.helidon.service.tests.inject.toolbox.Preferred; +import io.helidon.service.tests.inject.toolbox.Tool; +import io.helidon.service.tests.inject.toolbox.ToolBox; + +@SuppressWarnings("unused") +@Injection.Singleton +public class MainToolBox implements ToolBox { + + private final List> allTools; + private final List> allHammers; + private final Supplier bigHammer; + private final Screwdriver screwdriver; + public int postConstructCallCount; + public int preDestroyCallCount; + public int setterCallCount; + @Injection.Inject + @Preferred + Supplier preferredHammer; + private Supplier setPreferredHammer; + + @Injection.Inject + MainToolBox(List> allTools, + Screwdriver screwdriver, + @Injection.Named("big") Supplier bigHammer, + List> allHammers) { + this.allTools = Objects.requireNonNull(allTools); + this.screwdriver = Objects.requireNonNull(screwdriver); + this.bigHammer = bigHammer; + this.allHammers = allHammers; + } + + @Override + public List> toolsInBox() { + return allTools; + } + + @Override + public Supplier preferredHammer() { + return preferredHammer; + } + + public List> allHammers() { + return allHammers; + } + + public Supplier bigHammer() { + return bigHammer; + } + + public Screwdriver screwdriver() { + return screwdriver; + } + + @Injection.Inject + void setScrewdriver(Screwdriver screwdriver) { + assert (this.screwdriver == screwdriver); + setterCallCount++; + } + + @Injection.Inject + void setPreferredHammer(@Preferred Supplier hammer) { + this.setPreferredHammer = hammer; + } + + @Service.PostConstruct + void postConstruct() { + postConstructCallCount++; + } + + @Service.PreDestroy + void preDestroy() { + preDestroyCallCount++; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/Screwdriver.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/Screwdriver.java new file mode 100644 index 00000000000..5edba839c24 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/Screwdriver.java @@ -0,0 +1,33 @@ +/* + * 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.tests.inject.toolbox.impl; + +import java.io.Serializable; + +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.SomeOtherLocalNonContractInterface1; +import io.helidon.service.tests.inject.toolbox.Tool; + +@Injection.Singleton +public class Screwdriver implements Tool, SomeOtherLocalNonContractInterface1, Serializable { + + @Override + public String name() { + return "screwdriver"; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/SledgeHammer.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/SledgeHammer.java new file mode 100644 index 00000000000..4358b130160 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/SledgeHammer.java @@ -0,0 +1,36 @@ +/* + * 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.tests.inject.toolbox.impl; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.tests.inject.toolbox.Hammer; + +@Injection.Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 2) +//@Named(SledgeHammer.NAME) +public class SledgeHammer implements Hammer { + + public static final String NAME = "sledge"; + + @Override + public String name() { + return NAME + " hammer"; + } + +} diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/package-info.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/package-info.java new file mode 100644 index 00000000000..a70d42e0eb4 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/impl/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Testing. + */ +package io.helidon.service.tests.inject.toolbox.impl; diff --git a/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/package-info.java b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/package-info.java new file mode 100644 index 00000000000..e895c13941c --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/io/helidon/service/tests/inject/toolbox/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Testing. + */ +package io.helidon.service.tests.inject.toolbox; diff --git a/service/tests/inject/toolbox/src/main/java/module-info.java b/service/tests/inject/toolbox/src/main/java/module-info.java new file mode 100644 index 00000000000..983e668d0e5 --- /dev/null +++ b/service/tests/inject/toolbox/src/main/java/module-info.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +module io.helidon.service.tests.inject.toolbox { + requires io.helidon.service.registry; + requires io.helidon.service.inject.api; + + // we use Application + requires io.helidon.service.inject; + requires io.helidon.config; + requires io.helidon.config.yaml; + requires io.helidon.http; + requires io.helidon.common.context; + requires io.helidon.metrics.api; + + exports io.helidon.service.tests.inject.toolbox; + exports io.helidon.service.tests.inject.toolbox.impl to io.helidon.service.registry; +} \ No newline at end of file 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 new file mode 100644 index 00000000000..5e9b9a6a023 --- /dev/null +++ b/service/tests/inject/toolbox/src/test/java/io/helidon/service/tests/inject/toolbox/ToolBoxTest.java @@ -0,0 +1,210 @@ +/* + * 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.tests.inject.toolbox; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.types.TypeName; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.FactoryType; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.InjectServiceInfo; +import io.helidon.service.inject.api.Lookup; +import io.helidon.service.tests.inject.toolbox.impl.BigHammer; +import io.helidon.service.tests.inject.toolbox.impl.MainToolBox; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Expectation here is that the annotation processor ran, and we can use standard injection and di registry services, etc. + */ +class ToolBoxTest { + private static final MeterRegistry METER_REGISTRY = Metrics.globalRegistry(); + + private InjectRegistryManager registryManager; + private InjectRegistry registry; + + @BeforeEach + void setUp() { + this.registryManager = InjectRegistryManager.create(); + this.registry = registryManager.registry(); + } + + @AfterEach + void tearDown() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void sanity() { + assertNotNull(registryManager); + assertNotNull(registry); + } + + @Test + void toolbox() { + List blanks = registry.lookupServices(Lookup.create(Awl.class)); + assertThat(blanks, hasSize(1)); + + List allToolBoxes = registry.lookupServices(Lookup.create(ToolBox.class)); + assertThat(allToolBoxes, hasSize(1)); + + ToolBox toolBox = registry.get(ToolBox.class); + assertThat(toolBox.getClass(), equalTo(MainToolBox.class)); + MainToolBox mtb = (MainToolBox) toolBox; + assertThat(mtb.postConstructCallCount, equalTo(1)); + assertThat(mtb.preDestroyCallCount, equalTo(0)); + assertThat(mtb.setterCallCount, equalTo(1)); + + List> allTools = mtb.toolsInBox(); + assertThat(allTools, hasSize(7)); + assertThat(mtb.screwdriver(), notNullValue()); + + Supplier hammer = Objects.requireNonNull(toolBox.preferredHammer()); + assertThat(hammer.get(), notNullValue()); + assertThat(hammer.get(), sameInstance(hammer.get())); + assertThat(hammer.get(), instanceOf(BigHammer.class)); + + List toolTypes = allTools.stream() + .map(Supplier::get) + .map(Object::getClass) + .map(Class::getSimpleName) + .toList(); + assertThat(toolTypes, contains("SledgeHammer", // weight + 2, tbox.impl.SledgeHammer + "BigHammer", // weight + 1, tbox.impl.BigHammer + "TableSaw", // tbox.TableSaw + "AwlImpl", // tbox.impl.AwlImpl + "HandSaw", // tbox.impl.HandSaw + "Screwdriver", // tbox.impl.Screwdriver + "LittleHammer" // tbox.impl.LittleHammer, has qualifier Named + )); + + List hammers = mtb.allHammers() + .stream() + .map(Supplier::get) + .map(Object::getClass) + .map(Class::getSimpleName) + .toList(); + + assertThat(hammers, + contains("SledgeHammer", + "BigHammer", + "LittleHammer")); + } + + /** + * Targets {@link AbstractSaw} with derived classes of + * {@link io.helidon.service.tests.inject.toolbox.impl.HandSaw} and {@link TableSaw} found in different packages. + */ + @Test + void hierarchyOfInjections() { + List saws = registry.all(AbstractSaw.class); + assertThat(saws, hasSize(2)); + List desc = toSimpleClassNames(saws); + // note that order matters here + assertThat(desc, + contains("TableSaw", "HandSaw")); + + for (AbstractSaw saw : saws) { + saw.verifyState(); + } + } + + /** + * 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(); + + List allServices = registry.lookupServices(Lookup.EMPTY); + // one lookup on previous line + long postLookupCount = counter.count(); + assertThat((postLookupCount - initialCount), is(1L)); + + // now lookup of each service should increase the counter by exactly one (i.e. no lookups for injection points) + for (InjectServiceInfo service : allServices) { + postLookupCount++; + try { + registry.supply(service.serviceType()) + .get(); + } catch (Exception ignored) { + // injection point providers will throw an exception, as they cannot resolve injection point + // still should not lookup + } + assertThat("Activation should not have triggered any lookups (for singletons): " + + service.descriptorType().fqName() + " triggered lookups", counter.count(), + equalTo(postLookupCount)); + } + } + + @Test + void knownIpProviders() { + List services = this.registry.lookupServices(Lookup.builder() + .addFactoryType(FactoryType.INJECTION_POINT) + .build()); + List desc = toSimpleTypes(services); + + // this list must only InjectionPointProviders + assertThat(desc, + contains("BladeProvider")); + } + + private List toSimpleClassNames(List listOfObjects) { + return listOfObjects.stream() + .map(Object::getClass) + .map(Class::getSimpleName) + .toList(); + } + + private List toSimpleTypes(List providers) { + return providers.stream() + .map(InjectServiceInfo::serviceType) + .map(TypeName::className) + .toList(); + } + + private Counter lookupCounter() { + Optional counterMeter = METER_REGISTRY.counter("io.helidon.inject.lookups", List.of()); + assertThat(counterMeter, optionalPresent()); + return counterMeter.get(); + } +} diff --git a/service/tests/pom.xml b/service/tests/pom.xml index 245ccea4b69..73e02f741c6 100644 --- a/service/tests/pom.xml +++ b/service/tests/pom.xml @@ -45,6 +45,7 @@ registry codegen + inject diff --git a/service/tests/registry/src/test/java/io/helidon/service/test/registry/CyclicDependencyTest.java b/service/tests/registry/src/test/java/io/helidon/service/test/registry/CyclicDependencyTest.java index 9d0e07e1c27..a90993b3526 100644 --- a/service/tests/registry/src/test/java/io/helidon/service/test/registry/CyclicDependencyTest.java +++ b/service/tests/registry/src/test/java/io/helidon/service/test/registry/CyclicDependencyTest.java @@ -20,10 +20,12 @@ import java.util.Set; import io.helidon.common.GenericType; +import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.service.registry.Dependency; import io.helidon.service.registry.DependencyContext; import io.helidon.service.registry.GeneratedService; +import io.helidon.service.registry.ServiceDescriptor; import io.helidon.service.registry.ServiceRegistry; import io.helidon.service.registry.ServiceRegistryConfig; import io.helidon.service.registry.ServiceRegistryException; @@ -68,7 +70,7 @@ private static class Service2 { } } - private static class Descriptor1 implements GeneratedService.Descriptor { + private static class Descriptor1 implements ServiceDescriptor { private static final TypeName TYPE = TypeName.create(Descriptor1.class); private static final Dependency DEP = Dependency.builder() @@ -102,12 +104,12 @@ public List dependencies() { } @Override - public Set contracts() { - return Set.of(SERVICE_1); + public Set contracts() { + return Set.of(ResolvedType.create(SERVICE_1)); } } - private static class Descriptor2 implements GeneratedService.Descriptor { + private static class Descriptor2 implements ServiceDescriptor { private static final TypeName TYPE = TypeName.create(Descriptor2.class); private static final Dependency DEP = Dependency.builder() @@ -141,8 +143,8 @@ public List dependencies() { } @Override - public Set contracts() { - return Set.of(SERVICE_2); + public Set contracts() { + return Set.of(ResolvedType.create(SERVICE_2)); } } } diff --git a/tests/integration/packaging/inject/pom.xml b/tests/integration/packaging/inject/pom.xml new file mode 100644 index 00000000000..8ff5638e904 --- /dev/null +++ b/tests/integration/packaging/inject/pom.xml @@ -0,0 +1,222 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.2.0-SNAPSHOT + ../../../../applications/se/pom.xml + + io.helidon.tests.integration.packaging + helidon-tests-integration-packaging-inject + Helidon Tests Integration Packaging Inject + + + io.helidon.tests.integration.packaging.inject.Main + true + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webclient + helidon-webclient + + + io.helidon.config + helidon-config-yaml + + + io.helidon.service.inject + helidon-service-inject-api + + + io.helidon.service.inject + helidon-service-inject + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.tests.integration + helidon-tests-integration-harness + ${project.version} + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT + + + + ${project.build.outputDirectory}/logging.properties + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + ${project.build.outputDirectory}/logging.properties + + + ${redirectTestOutputToFile} + + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service.inject + helidon-service-inject-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + + + + jar-image + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*Jar*IT + + + + + + + + native-image + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*Native*IT + + + + + + + + jlink-image + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*Jlink*IT + + + + + + + + diff --git a/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/GreetFeature.java b/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/GreetFeature.java new file mode 100644 index 00000000000..04936bb8e48 --- /dev/null +++ b/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/GreetFeature.java @@ -0,0 +1,85 @@ +/* + * 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.tests.integration.packaging.inject; + +import io.helidon.service.inject.api.Configuration; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; +import io.helidon.webserver.http.HttpFeature; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * A simple service to greet you. + */ +@Injection.Singleton +@Service.ExternalContracts(HttpFeature.class) +class GreetFeature implements HttpFeature { + + /** + * The config value for the key {@code greeting}. + */ + private final String greeting; + + GreetFeature(@Configuration.Value("app.greeting:Ciao") String greetingValue) { + this.greeting = greetingValue; + } + + @Override + public void setup(HttpRouting.Builder routing) { + routing.register("/ws", this::routing); + } + + void routing(HttpRules rules) { + rules + .get("/greet", this::getDefaultMessageHandler) + .get("/greet/{name}", this::getMessageHandler); + + } + + /** + * Return a worldly greeting message. + * + * @param request the server request + * @param response the server response + */ + private void getDefaultMessageHandler(ServerRequest request, + ServerResponse response) { + sendResponse(response, "World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param request the server request + * @param response the server response + */ + private void getMessageHandler(ServerRequest request, + ServerResponse response) { + String name = request.path().pathParameters().get("name"); + + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting, name); + + response.send(msg); + } +} diff --git a/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/Main.java b/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/Main.java new file mode 100644 index 00000000000..c778ebfdd66 --- /dev/null +++ b/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/Main.java @@ -0,0 +1,49 @@ +/* + * 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.tests.integration.packaging.inject; + +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; +import io.helidon.logging.common.LogConfig; +import io.helidon.service.inject.InjectRegistryManager; +import io.helidon.service.inject.api.InjectRegistry; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.inject.api.Lookup; + +/** + * We must provide a main class when using modularized jar file with main class attribute, + * as we cannot use a main class from another module (at least not easily). + */ +public class Main { + static { + LogConfig.initClass(); + } + + public static void main(String[] args) { + LogConfig.configureRuntime(); + + // makes sure global config is initialized + Config config = GlobalConfig.config(); + GlobalConfig.config(() -> config); + + InjectRegistry registry = InjectRegistryManager.create() + .registry(); + registry.get(Lookup.builder() + .runLevel(Injection.RunLevel.SERVER) + .build()); + } +} diff --git a/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/ServerStarter.java b/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/ServerStarter.java new file mode 100644 index 00000000000..810666011d0 --- /dev/null +++ b/tests/integration/packaging/inject/src/main/java/io/helidon/tests/integration/packaging/inject/ServerStarter.java @@ -0,0 +1,60 @@ +/* + * 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.tests.integration.packaging.inject; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.common.config.Config; +import io.helidon.service.inject.api.Injection; +import io.helidon.service.registry.Service; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpFeature; + +/** + * This will eventually be part of Helidon WebServer + */ +@Injection.Singleton +@Injection.RunLevel(Injection.RunLevel.SERVER) +class ServerStarter { + private final Supplier config; + private final Supplier> features; + + private volatile WebServer server; + + @Injection.Inject + ServerStarter(Supplier config, Supplier> features) { + this.config = config; + this.features = features; + } + + @Service.PostConstruct + void start() { + server = WebServer.builder() + .config(config.get().get("server")) + .routing(it -> it.update(routing -> features.get().forEach(routing::addFeature))) + .build() + .start(); + } + + @Service.PreDestroy() + void stop() { + if (server != null) { + server.stop(); + } + } +} diff --git a/tests/integration/packaging/inject/src/main/java/module-info.java b/tests/integration/packaging/inject/src/main/java/module-info.java new file mode 100644 index 00000000000..7fe479296d7 --- /dev/null +++ b/tests/integration/packaging/inject/src/main/java/module-info.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. + */ + +module io.helidon.tests.integration.packaging.inject { + requires io.helidon.webserver; + requires io.helidon; + requires io.helidon.service.inject; + requires io.helidon.logging.common; + + exports io.helidon.tests.integration.packaging.inject; +} \ No newline at end of file diff --git a/tests/integration/packaging/inject/src/main/resources/application.yaml b/tests/integration/packaging/inject/src/main/resources/application.yaml new file mode 100644 index 00000000000..0d5362ba36d --- /dev/null +++ b/tests/integration/packaging/inject/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# +# 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. +# + +app: + greeting: "Hello" + +server: + port: 7076 + host: 0.0.0.0 diff --git a/tests/integration/packaging/inject/src/main/resources/logging.properties b/tests/integration/packaging/inject/src/main/resources/logging.properties new file mode 100644 index 00000000000..fc912b62edf --- /dev/null +++ b/tests/integration/packaging/inject/src/main/resources/logging.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s: %5$s%6$s%n + +.level=INFO diff --git a/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectJarClassPathTestIT.java b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectJarClassPathTestIT.java new file mode 100644 index 00000000000..74cbc3ac374 --- /dev/null +++ b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectJarClassPathTestIT.java @@ -0,0 +1,38 @@ +/* + * 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.tests.integration.packaging.inject; + +import io.helidon.tests.integration.harness.ProcessRunner.ExecMode; + +import org.junit.jupiter.api.Test; + +class InjectJarClassPathTestIT extends InjectPackagingTestIT { + + @Override + ExecMode execMode() { + return ExecMode.CLASS_PATH; + } + + @Test + void testExitOnStarted() { + doTestExitOnStarted(); + } + + @Test + void testWebClientService() { + doTestWebClientService(); + } +} diff --git a/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectJlinkTestIT.java b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectJlinkTestIT.java new file mode 100644 index 00000000000..8516a4bfd15 --- /dev/null +++ b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectJlinkTestIT.java @@ -0,0 +1,38 @@ +/* + * 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.tests.integration.packaging.inject; + +import io.helidon.tests.integration.harness.ProcessRunner.ExecMode; + +import org.junit.jupiter.api.Test; + +class InjectJlinkTestIT extends InjectPackagingTestIT { + + @Override + ExecMode execMode() { + return ExecMode.JLINK_CLASS_PATH; + } + + @Test + void testExitOnStarted() { + doTestExitOnStarted(); + } + + @Test + void testWebClientService() { + doTestWebClientService(); + } +} diff --git a/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectNativeTestIT.java b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectNativeTestIT.java new file mode 100644 index 00000000000..270b4c44a31 --- /dev/null +++ b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectNativeTestIT.java @@ -0,0 +1,38 @@ +/* + * 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.tests.integration.packaging.inject; + +import io.helidon.tests.integration.harness.ProcessRunner.ExecMode; + +import org.junit.jupiter.api.Test; + +class InjectNativeTestIT extends InjectPackagingTestIT { + + @Override + ExecMode execMode() { + return ExecMode.NATIVE; + } + + @Test + void testExitOnStarted() { + doTestExitOnStarted(); + } + + @Test + void testWebClientService() { + doTestWebClientService(); + } +} diff --git a/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectPackagingTestIT.java b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectPackagingTestIT.java new file mode 100644 index 00000000000..539715d2ae1 --- /dev/null +++ b/tests/integration/packaging/inject/src/test/java/io/helidon/tests/integration/packaging/inject/InjectPackagingTestIT.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.tests.integration.packaging.inject; + +import java.util.Map; + +import io.helidon.http.Status; +import io.helidon.tests.integration.harness.ProcessMonitor; +import io.helidon.tests.integration.harness.ProcessRunner; +import io.helidon.tests.integration.harness.ProcessRunner.ExecMode; +import io.helidon.tests.integration.harness.WaitStrategy; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.api.WebClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +abstract class InjectPackagingTestIT { + + abstract ExecMode execMode(); + + void doTestExitOnStarted() { + try (ProcessMonitor process = process(Map.of("exit.on.started", "!")) + .await(WaitStrategy.waitForCompletion())) { + + assertThat(process.get().exitValue(), is(0)); + } + } + + void doTestWebClientService() { + try (ProcessMonitor process = process(Map.of()) + .await(WaitStrategy.waitForPort())) { + + WebClient client = WebClient.builder() + .baseUri("http://localhost:" + process.port()) + .build(); + ClientResponseTyped response = client.get("/ws/greet").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.entity(), is("Hello World!")); + + response = client.get("/ws/greet/Helidon").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.entity(), is("Hello Helidon!")); + } + } + + private ProcessMonitor process(Map properties) { + return ProcessRunner.of(execMode()) + .finalName("helidon-tests-integration-packaging-inject") + .properties(properties) + .port(0) + .start(); + } +} diff --git a/tests/integration/packaging/pom.xml b/tests/integration/packaging/pom.xml index 04b8081e3ae..e6feb1b2f6f 100644 --- a/tests/integration/packaging/pom.xml +++ b/tests/integration/packaging/pom.xml @@ -40,5 +40,6 @@ mp-2 mp-3 se-1 + inject diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java b/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java index ee5e28c6f6b..8644e6f5d26 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java @@ -37,6 +37,7 @@ import io.helidon.common.Version; import io.helidon.common.Weights; import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; import io.helidon.common.features.HelidonFeatures; import io.helidon.common.features.api.HelidonFlavor; import io.helidon.common.tls.Tls; @@ -226,7 +227,15 @@ private void startIt() { if ("!".equals(System.getProperty(EXIT_ON_STARTED_KEY))) { LOGGER.log(System.Logger.Level.INFO, String.format("Exiting, -D%s set.", EXIT_ON_STARTED_KEY)); - System.exit(0); + // we need to run the system exit on a different thread, to correctly finish whatever was happening on main + // all shutdown hooks run on that thread + var ctx = Contexts.context().orElseGet(Contexts::globalContext); + Thread.ofPlatform() + .daemon(false) + .name("Helidon system exit thread") + .start(() -> { + Contexts.runInContext(ctx, () -> System.exit(0)); + }); } }