diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java index ef967fe8aa7..71c872e9c7f 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java @@ -315,7 +315,8 @@ static Optional toTypeInfo(TypeElement element, try { Set annotations = createAnnotationAndValueSet(elementUtils.getTypeElement(genericTypeName.name())); - Map> referencedAnnotations = toMetaAnnotations(annotations, processingEnv); + Map> referencedAnnotations = + new LinkedHashMap<>(toMetaAnnotations(annotations, processingEnv)); List elementsWeCareAbout = new ArrayList<>(); List otherElements = new ArrayList<>(); element.getEnclosedElements().stream() @@ -328,6 +329,9 @@ static Optional toTypeInfo(TypeElement element, } else { otherElements.add(it); } + merge(referencedAnnotations, toMetaAnnotations(it.annotations(), processingEnv)); + it.parameterArguments().forEach(arg -> merge(referencedAnnotations, + toMetaAnnotations(arg.annotations(), processingEnv))); }); TypeInfoDefault.Builder builder = TypeInfoDefault.builder() .typeName(fqTypeName) @@ -412,6 +416,11 @@ static Optional toTypeInfo(TypeElement element, } } + private static void merge(Map> result, + Map> metaAnnotations) { + metaAnnotations.forEach((key1, value) -> result.computeIfAbsent(key1, (key) -> new ArrayList<>()).addAll(value)); + } + /** * Determines if the given type element is defined in the module being processed. If so then the return value is set to * {@code true} and the moduleName is cleared out. If not then the return value is set to {@code false} and the @@ -720,44 +729,49 @@ public static String extractValue(AnnotationMirror am, public static Optional createTypedElementNameFromElement(Element v, Elements elements) { TypeName type = createTypeNameFromElement(v).orElse(null); - List componentTypeNames = null; + TypeMirror typeMirror = null; String defaultValue = null; - List elementTypeAnnotations = List.of(); - Set modifierNames = Set.of(); List params = List.of(); + List componentTypeNames = List.of(); + List elementTypeAnnotations = List.of(); + Set modifierNames = v.getModifiers().stream() + .map(Modifier::toString) + .collect(Collectors.toSet()); + if (v instanceof ExecutableElement) { ExecutableElement ee = (ExecutableElement) v; - TypeMirror returnType = ee.getReturnType(); + typeMirror = Objects.requireNonNull(ee.getReturnType()); + params = ee.getParameters().stream() + .map(it -> createTypedElementNameFromElement(it, elements).orElseThrow()) + .collect(Collectors.toList()); + AnnotationValue annotationValue = ee.getDefaultValue(); + defaultValue = (annotationValue == null) ? null + : annotationValue.accept(new ToStringAnnotationValueVisitor() + .mapBooleanToNull(true) + .mapVoidToNull(true) + .mapBlankArrayToNull(true) + .mapEmptyStringToNull(true) + .mapToSourceDeclaration(true), null); + } else if (v instanceof VariableElement) { + VariableElement ve = (VariableElement) v; + typeMirror = Objects.requireNonNull(ve.asType()); + } + + if (typeMirror != null) { if (type == null) { - type = createTypeNameFromMirror(returnType).orElse(createFromGenericDeclaration(returnType.toString())); + type = createTypeNameFromMirror(typeMirror).orElse(createFromGenericDeclaration(typeMirror.toString())); } - if (returnType instanceof DeclaredType) { - List args = ((DeclaredType) returnType).getTypeArguments(); + if (typeMirror instanceof DeclaredType) { + List args = ((DeclaredType) typeMirror).getTypeArguments(); componentTypeNames = args.stream() .map(BuilderTypeTools::createTypeNameFromMirror) .filter(Optional::isPresent) .map(Optional::orElseThrow) .collect(Collectors.toList()); elementTypeAnnotations = - createAnnotationAndValueListFromElement(((DeclaredType) returnType).asElement(), elements); + createAnnotationAndValueListFromElement(((DeclaredType) typeMirror).asElement(), elements); } - AnnotationValue annotationValue = ee.getDefaultValue(); - defaultValue = annotationValue == null - ? null - : annotationValue.accept(new ToStringAnnotationValueVisitor() - .mapBooleanToNull(true) - .mapVoidToNull(true) - .mapBlankArrayToNull(true) - .mapEmptyStringToNull(true) - .mapToSourceDeclaration(true), null); - modifierNames = ee.getModifiers().stream() - .map(Modifier::toString) - .collect(Collectors.toSet()); - params = ee.getParameters().stream() - .map(it -> createTypedElementNameFromElement(it, elements).orElseThrow()) - .collect(Collectors.toList()); } - componentTypeNames = (componentTypeNames == null) ? List.of() : componentTypeNames; TypedElementNameDefault.Builder builder = TypedElementNameDefault.builder() .typeName(type) @@ -767,7 +781,7 @@ public static Optional createTypedElementNameFromElement(Eleme .annotations(createAnnotationAndValueListFromElement(v, elements)) .elementTypeAnnotations(elementTypeAnnotations) .modifierNames(modifierNames) - .parameterArgumentss(params); + .parameterArguments(params); createTypeNameFromElement(v.getEnclosingElement()).ifPresent(builder::enclosingTypeName); Optional.ofNullable(defaultValue).ifPresent(builder::defaultValue); diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java b/common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java index 4984c711f24..0e69011daba 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java @@ -356,7 +356,7 @@ public Builder enclosingTypeName(Class val) { * @param val the parameter values * @return this fluent builder */ - public Builder parameterArgumentss(List val) { + public Builder parameterArguments(List val) { Objects.requireNonNull(val); this.parameters.clear(); this.parameters.addAll(val); diff --git a/pico/api/src/main/java/io/helidon/pico/api/QualifierAndValueDefault.java b/pico/api/src/main/java/io/helidon/pico/api/QualifierAndValueDefault.java index ae69558c237..6f9c432d3e1 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/QualifierAndValueDefault.java +++ b/pico/api/src/main/java/io/helidon/pico/api/QualifierAndValueDefault.java @@ -17,6 +17,7 @@ package io.helidon.pico.api; import java.lang.annotation.Annotation; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -129,10 +130,17 @@ public static QualifierAndValue convert(AnnotationAndValue annotationAndValue) { return (QualifierAndValue) annotationAndValue; } - return (QualifierAndValue) builder() + // qualifiers should not have any blank values + Map values = annotationAndValue.values(); + String val = values.get("value"); + if ("".equals(val)) { + values = new LinkedHashMap<>(values); + values.remove("value"); + } + + return builder() .typeName(annotationAndValue.typeName()) - .values(annotationAndValue.values()) - .update(it -> annotationAndValue.value().ifPresent(it::value)) + .values(values) .build(); } diff --git a/pico/api/src/main/java/io/helidon/pico/api/RunLevel.java b/pico/api/src/main/java/io/helidon/pico/api/RunLevel.java index a1df72c15f4..59c04310d3f 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/RunLevel.java +++ b/pico/api/src/main/java/io/helidon/pico/api/RunLevel.java @@ -17,7 +17,6 @@ package io.helidon.pico.api; import java.lang.annotation.Documented; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -25,12 +24,12 @@ import static java.lang.annotation.ElementType.TYPE; /** - * Indicates the desired startup sequence for a service class. + * Indicates the desired startup sequence for a service class. This is not used internally by Pico, but is available as a + * convenience to the caller in support for a specific startup sequence for service activations. */ @Documented @Retention(RetentionPolicy.CLASS) @Target(TYPE) -@Inherited public @interface RunLevel { /** diff --git a/pico/configdriven/processor/pom.xml b/pico/configdriven/processor/pom.xml index 3021ba57be5..1042ce63293 100644 --- a/pico/configdriven/processor/pom.xml +++ b/pico/configdriven/processor/pom.xml @@ -36,21 +36,17 @@ io.helidon.builder helidon-builder-config - - io.helidon.pico.configdriven - helidon-pico-configdriven-api - io.helidon.pico helidon-pico-api - io.helidon.pico.configdriven - helidon-pico-configdriven-runtime + io.helidon.pico + helidon-pico-processor io.helidon.pico - helidon-pico-processor + helidon-pico-tools io.helidon.builder diff --git a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java new file mode 100644 index 00000000000..9918283c47e --- /dev/null +++ b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.configdriven.processor; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.processor.PicoAnnotationProcessor; +import io.helidon.pico.tools.ActivatorCreatorProvider; +import io.helidon.pico.tools.ServicesToProcess; + +import static io.helidon.common.types.TypeNameDefault.createFromGenericDeclaration; +import static io.helidon.common.types.TypeNameDefault.createFromTypeName; +import static io.helidon.common.types.TypeNameDefault.toBuilder; +import static io.helidon.pico.configdriven.processor.ConfiguredByProcessorUtils.createExtraActivatorClassComments; +import static io.helidon.pico.configdriven.processor.ConfiguredByProcessorUtils.createExtraCodeGen; +import static io.helidon.pico.tools.TypeNames.PICO_ABSTRACT_CONFIGURED_SERVICE_PROVIDER; +import static io.helidon.pico.tools.TypeNames.PICO_CONFIGURED_BY; + +/** + * Extension to {@link PicoAnnotationProcessor} that will handle {@code io.helidon.pico.configdriven.api.ConfiguredBy} services. + */ +public class ConfiguredByAnnotationProcessor extends PicoAnnotationProcessor { + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public ConfiguredByAnnotationProcessor() { + super(true); + } + + @Override + protected Set supportedServiceClassTargetAnnotations() { + Set supported = new HashSet<>(super.supportedServiceClassTargetAnnotations()); + supported.add(PICO_CONFIGURED_BY); + return supported; + } + + @Override + protected void processExtensions(ServicesToProcess services, + TypeInfo service, + Set serviceTypeNamesToCodeGenerate, + Collection allElementsOfInterest) { + Optional configuredByAnno = findFirst(PICO_CONFIGURED_BY, service.annotations()); + if (configuredByAnno.isEmpty()) { + return; + } + + Map configuredByAttributes = configuredByAnno.get().values(); + TypeName configBeanType = createFromTypeName(configuredByAttributes.get("value")); + TypeInfo parent = service.superTypeInfo().orElse(null); + boolean hasParent = (parent != null); + TypeName serviceTypeName = service.typeName(); + TypeName parentServiceTypeName = (hasParent) ? parent.typeName() : null; + TypeName activatorImplTypeName = activatorCreator().toActivatorImplTypeName(serviceTypeName); + TypeName genericCB = createFromGenericDeclaration("CB"); + TypeName genericExtendsCB = createFromGenericDeclaration("CB extends " + configBeanType.name()); + + if (hasParent && findFirst(PICO_CONFIGURED_BY, parent.annotations()).isEmpty()) { + // we treat this as a regular configured service, since its parent is NOT a configured service + hasParent = false; + parentServiceTypeName = null; + } + + if (hasParent) { + // we already know our parent, but we need to morph it with our activator and new CB reference + TypeName parentActivatorImplTypeName = ActivatorCreatorProvider.instance() + .toActivatorImplTypeName(parentServiceTypeName); + parentServiceTypeName = toBuilder(parentActivatorImplTypeName) + .typeArguments(List.of(genericCB)) + .build(); + } else { + List typeArgs = List.of(serviceTypeName, genericCB); + parentServiceTypeName = createFromTypeName(PICO_ABSTRACT_CONFIGURED_SERVICE_PROVIDER).toBuilder() + .typeArguments(typeArgs) + .build(); + } + + List extraCodeGen = createExtraCodeGen(activatorImplTypeName, configBeanType, hasParent, configuredByAttributes); + + boolean accepted = services.addParentServiceType(serviceTypeName, parentServiceTypeName, Optional.of(true)); + assert (accepted); + services.addActivatorGenericDecl(serviceTypeName, "<" + genericExtendsCB.fqName() + ">"); + extraCodeGen.forEach(fn -> services.addExtraCodeGen(serviceTypeName, fn)); + + List extraActivatorClassComments = createExtraActivatorClassComments(); + extraActivatorClassComments.forEach(fn -> services.addExtraActivatorClassComments(serviceTypeName, fn)); + } + +} diff --git a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessor.java b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessor.java deleted file mode 100644 index 5d989a17e08..00000000000 --- a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessor.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.pico.configdriven.processor; - -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 javax.annotation.processing.RoundEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; - -import io.helidon.builder.AttributeVisitor; -import io.helidon.builder.Builder; -import io.helidon.builder.config.ConfigBean; -import io.helidon.builder.config.spi.ConfigBeanInfo; -import io.helidon.builder.processor.tools.BuilderTypeTools; -import io.helidon.common.config.Config; -import io.helidon.common.types.AnnotationAndValue; -import io.helidon.common.types.AnnotationAndValueDefault; -import io.helidon.common.types.TypeName; -import io.helidon.pico.configdriven.api.ConfiguredBy; -import io.helidon.pico.configdriven.runtime.AbstractConfiguredServiceProvider; -import io.helidon.pico.processor.ServiceAnnotationProcessor; -import io.helidon.pico.tools.ActivatorCreatorProvider; -import io.helidon.pico.tools.ServicesToProcess; -import io.helidon.pico.tools.ToolsException; -import io.helidon.pico.tools.TypeTools; - -import static io.helidon.builder.processor.tools.BuilderTypeTools.createTypeNameFromElement; -import static io.helidon.builder.processor.tools.BuilderTypeTools.extractValues; -import static io.helidon.builder.processor.tools.BuilderTypeTools.findAnnotationMirror; -import static io.helidon.builder.processor.tools.BuilderTypeTools.toTypeElement; -import static io.helidon.common.types.TypeNameDefault.create; -import static io.helidon.common.types.TypeNameDefault.createFromGenericDeclaration; -import static io.helidon.common.types.TypeNameDefault.createFromTypeName; -import static io.helidon.common.types.TypeNameDefault.toBuilder; - -/** - * Processor for @{@link io.helidon.pico.configdriven.api.ConfiguredBy} type annotations. - */ -public class ConfiguredByProcessor extends ServiceAnnotationProcessor { - private final System.Logger logger = System.getLogger(getClass().getName()); - private final LinkedHashSet elementsProcessed = new LinkedHashSet<>(); - - static final String TAG_OVERRIDE_BEAN = "overrideBean"; - - /** - * Service loader based constructor. - * - * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly - */ - @Deprecated - public ConfiguredByProcessor() { - } - - @Override - public Set getSupportedAnnotationTypes() { - Set supported = new LinkedHashSet<>(super.getSupportedAnnotationTypes()); - supported.add(ConfiguredBy.class.getName()); - return supported; - } - - @Override - public Set contraAnnotations() { - return Set.of(); - } - - @Override - public boolean process(Set annotations, - RoundEnvironment roundEnv) { - super.process(annotations, roundEnv); - - if (roundEnv.processingOver()) { - elementsProcessed.clear(); - // we claim this annotation! - return true; - } - - try { - Set typesToProcess = roundEnv.getElementsAnnotatedWith(ConfiguredBy.class); - for (Element element : typesToProcess) { - if (!elementsProcessed.add(element)) { - continue; - } - - try { - process(element); - } catch (Throwable e) { - throw new ToolsException("Failed while processing " + element + "; " + e.getMessage(), e); - } - } - } catch (Throwable e) { - logger.log(System.Logger.Level.ERROR, e.getMessage(), e); - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage()); - throw new RuntimeException(e.getMessage(), e); - } - - // we need to claim this annotation! - return true; - } - - void process(Element element) { - if (!(element instanceof TypeElement)) { - throw new ToolsException("Expected " + element + " to be processed as a TypeElement"); - } - - AnnotationMirror am = findAnnotationMirror(ConfiguredBy.class.getName(), element.getAnnotationMirrors()).orElseThrow(); - Map configuredByAttributes = extractValues(am, processingEnv.getElementUtils()); - TypeName configBeanType = createFromTypeName(configuredByAttributes.get("value")); - TypeName serviceTypeName = createTypeNameFromElement(element).orElseThrow(); - TypeElement parent = toTypeElement(((TypeElement) element).getSuperclass()).orElse(null); - TypeName parentServiceTypeName = (parent == null) ? null : createTypeNameFromElement(parent).orElseThrow(); - TypeName activatorImplTypeName = ActivatorCreatorProvider.instance().toActivatorImplTypeName(serviceTypeName); - TypeName genericCB = createFromGenericDeclaration("CB"); - TypeName genericExtendsCB = createFromGenericDeclaration("CB extends " + configBeanType.name()); - boolean hasParent = (parentServiceTypeName != null); - - if (hasParent && !isConfiguredService(parent)) { - // we treat this as a regular configured service, since its parent is NOT a configured service - hasParent = false; - parentServiceTypeName = null; - } - - if (hasParent) { - // we already know our parent, but we need to morph it with our activator and new CB reference - TypeName parentActivatorImplTypeName = ActivatorCreatorProvider.instance() - .toActivatorImplTypeName(parentServiceTypeName); - parentServiceTypeName = toBuilder(parentActivatorImplTypeName) - .typeArguments(List.of(genericCB)) - .build(); - } else { - List typeArgs = List.of(serviceTypeName, genericCB); - parentServiceTypeName = create(AbstractConfiguredServiceProvider.class).toBuilder() - .typeArguments(typeArgs) - .build(); - } - - validate((TypeElement) element, configBeanType, serviceTypeName, parentServiceTypeName); - - List extraCodeGen = createExtraCodeGen(activatorImplTypeName, configBeanType, hasParent, configuredByAttributes); - - ServicesToProcess servicesToProcess = ServicesToProcess.servicesInstance(); - boolean accepted = servicesToProcess.addParentServiceType(serviceTypeName, parentServiceTypeName, Optional.of(true)); - assert (accepted); - servicesToProcess.addActivatorGenericDecl(serviceTypeName, "<" + genericExtendsCB.fqName() + ">"); - extraCodeGen.forEach(fn -> servicesToProcess.addExtraCodeGen(serviceTypeName, fn)); - - List extraActivatorClassComments = createExtraActivatorClassComments(); - extraActivatorClassComments.forEach(fn -> servicesToProcess.addExtraActivatorClassComments(serviceTypeName, fn)); - - processServiceType(serviceTypeName, (TypeElement) element); - } - - boolean isConfiguredService(TypeElement te) { - Set annotations = TypeTools.createAnnotationAndValueSet(te); - Optional configuredByAnno = - AnnotationAndValueDefault.findFirst(ConfiguredBy.class.getName(), annotations); - return configuredByAnno.isPresent(); - } - - void validate(TypeElement element, - TypeName configBeanType, - TypeName serviceTypeName, - TypeName parentServiceTypeName) { - assertNoAnnotation(create(jakarta.inject.Singleton.class), element); - validateBeanType(configBeanType); - validateServiceType(serviceTypeName, parentServiceTypeName); - } - - void assertNoAnnotation(TypeName annoType, - TypeElement element) { - Set annos = TypeTools.createAnnotationAndValueSet(element); - Optional anno = AnnotationAndValueDefault.findFirst(annoType.name(), annos); - if (anno.isPresent()) { - throw new IllegalStateException(annoType + " cannot be used in conjunction with " - + ConfiguredBy.class + " on " + element); - } - } - - void validateBeanType(TypeName configBeanType) { - TypeElement typeElement = (configBeanType == null) - ? null : processingEnv.getElementUtils().getTypeElement(configBeanType.name()); - if (typeElement == null) { - throw new ToolsException("Unknown config bean type: " + configBeanType); - } - - if (typeElement.getKind() != ElementKind.INTERFACE) { - throw new ToolsException("The config bean must be an interface: " + typeElement); - } - - Optional cfgBean = BuilderTypeTools - .findAnnotationMirror(ConfigBean.class.getName(), typeElement.getAnnotationMirrors()); - if (cfgBean.isEmpty()) { - throw new ToolsException("The config bean must be annotated with @" + ConfigBean.class.getSimpleName() - + ": " + configBeanType); - } - } - - void validateServiceType(TypeName serviceTypeName, - TypeName ignoredParentServiceTypeName) { - TypeElement typeElement = (serviceTypeName == null) - ? null : processingEnv.getElementUtils().getTypeElement(serviceTypeName.name()); - if (typeElement == null) { - throw new ToolsException("Unknown service type: " + serviceTypeName); - } - - if (typeElement.getKind() != ElementKind.CLASS) { - throw new ToolsException("The configured service must be a concrete class: " + typeElement); - } - } - - List createExtraCodeGen(TypeName activatorImplTypeName, - TypeName configBeanType, - boolean hasParent, - Map configuredByAttributes) { - List result = new ArrayList<>(); - TypeName configBeanImplName = toDefaultImpl(configBeanType); - - String comment = "\n\t/**\n" - + "\t * Config-driven service constructor.\n" - + "\t * \n" - + "\t * @param configBean config bean\n" - + "\t */"; - if (hasParent) { - result.add(comment + "\n\tprotected " + activatorImplTypeName.className() + "(" + configBeanType + " configBean) {\n" - + "\t\tsuper(configBean);\n" - + "\t\tserviceInfo(serviceInfo);\n" - + "\t}\n"); - } else { - result.add("\n\tprivate " + configBeanType + " configBean;\n"); - result.add(comment + "\n\tprotected " + activatorImplTypeName.className() + "(" + configBeanType + " configBean) {\n" - + "\t\tthis.configBean = Objects.requireNonNull(configBean);\n" - + "\t\tassertIsRootProvider(false, true);\n" - + "\t\tserviceInfo(serviceInfo);\n" - + "\t}\n"); - } - - comment = "\n\t/**\n" - + "\t * Creates an instance given a config bean.\n" - + "\t * \n" - + "\t * @param configBean config bean\n" - + "\t */\n"; - result.add(comment + "\t@Override\n" - + "\tprotected " + activatorImplTypeName + " createInstance(Object configBean) {\n" - + "\t\treturn new " + activatorImplTypeName.className() + "((" + configBeanType + ") configBean);\n" - + "\t}\n"); - - if (!hasParent) { - result.add("\t@Override\n" - + "\tpublic Optional configBean() {\n" - + "\t\treturn Optional.ofNullable((CB) configBean);\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic Optional<" + Config.class.getName() + "> rawConfig() {\n" - + "\t\tif (configBean == null) {\n" - + "\t\t\treturn Optional.empty();\n" - + "\t\t}\n" - + "\t\treturn ((" + configBeanImplName + ") configBean).__config();\n" - + "\t}\n"); - } - - result.add("\t@Override\n" - + "\tpublic Class configBeanType() {\n" - + "\t\treturn " + configBeanType + ".class;\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic Map> configBeanAttributes() {\n" - + "\t\treturn " + configBeanImplName + ".__metaAttributes();\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic void visitAttributes(CB configBean, " + AttributeVisitor.class.getName() - + " visitor, R userDefinedCtx) {\n" - + "\t\t" + AttributeVisitor.class.getName() + " beanVisitor = visitor::visit;\n" - + "\t\t((" + configBeanImplName + ") configBean).visitAttributes(beanVisitor, userDefinedCtx);\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic CB toConfigBean(" + Config.class.getName() + " config) {\n" - + "\t\treturn (CB) " + configBeanImplName + "\n\t\t\t.toBuilder(config)\n\t\t\t.build();\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic " + configBeanImplName + ".Builder " - + "toConfigBeanBuilder(" + Config.class.getName() + " config) {\n" - + "\t\treturn " + configBeanImplName + ".toBuilder(config);\n" - + "\t}\n"); - - if (!hasParent) { - result.add("\t@Override\n" - + "\tprotected CB acceptConfig(io.helidon.common.config.Config config) {\n" - + "\t\tthis.configBean = (CB) super.acceptConfig(config);\n" - + "\t\treturn (CB) this.configBean;\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic String toConfigBeanInstanceId(CB configBean) {\n" - + "\t\treturn ((" + configBeanImplName - + ") configBean).__instanceId();\n" - + "\t}\n"); - result.add("\t@Override\n" - + "\tpublic void configBeanInstanceId(CB configBean, String val) {\n" - + "\t\t((" + configBeanImplName + ") configBean).__instanceId(val);\n" - + "\t}\n"); - } - - String overridesEnabledStr = configuredByAttributes.get(TAG_OVERRIDE_BEAN); - if (Boolean.parseBoolean(overridesEnabledStr)) { - String drivesActivationStr = configuredByAttributes.get(ConfigBeanInfo.TAG_DRIVES_ACTIVATION); - result.add("\t@Override\n" - + "\tprotected boolean drivesActivation() {\n" - + "\t\treturn " + Boolean.parseBoolean(drivesActivationStr) + ";\n" - + "\t}\n"); - } - - return result; - } - - List createExtraActivatorClassComments() { - return List.of("@param the config bean type"); - } - - TypeName toDefaultImpl(TypeName configBeanType) { - return create(configBeanType.packageName(), - Builder.DEFAULT_IMPL_PREFIX + configBeanType.className() + Builder.DEFAULT_SUFFIX); - } - -} diff --git a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessorUtils.java b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessorUtils.java new file mode 100644 index 00000000000..57d52a85dba --- /dev/null +++ b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessorUtils.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.configdriven.processor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.helidon.builder.AttributeVisitor; +import io.helidon.builder.Builder; +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.common.config.Config; +import io.helidon.common.types.TypeName; + +import static io.helidon.common.types.TypeNameDefault.create; + +final class ConfiguredByProcessorUtils { + + static final String TAG_OVERRIDE_BEAN = "overrideBean"; + + private ConfiguredByProcessorUtils() { + } + + /** + * Returns the additive code generated methods (source form) to add over what the base + * {@link io.helidon.pico.processor.PicoAnnotationProcessor} provides - thereby making this a "config driven service". + * + * @param activatorImplTypeName the activator implementation type name + * @param configBeanType the config bean type + * @param hasParent flag indicating whether the implementation has another config-driven parent activator + * @param configuredByAttributes meta attributes for the configuredBy annotation + * @return the list of extra source code to generate + * @see ConfiguredByAnnotationProcessor + */ + static List createExtraCodeGen(TypeName activatorImplTypeName, + TypeName configBeanType, + boolean hasParent, + Map configuredByAttributes) { + List result = new ArrayList<>(); + TypeName configBeanImplName = toDefaultImpl(configBeanType); + + String comment = "\n\t/**\n" + + "\t * Config-driven service constructor.\n" + + "\t * \n" + + "\t * @param configBean config bean\n" + + "\t */"; + if (hasParent) { + result.add(comment + "\n\tprotected " + activatorImplTypeName.className() + "(" + configBeanType + " configBean) {\n" + + "\t\tsuper(configBean);\n" + + "\t\tserviceInfo(serviceInfo);\n" + + "\t}\n"); + } else { + result.add("\n\tprivate " + configBeanType + " configBean;\n"); + result.add(comment + "\n\tprotected " + activatorImplTypeName.className() + "(" + configBeanType + " configBean) {\n" + + "\t\tthis.configBean = Objects.requireNonNull(configBean);\n" + + "\t\tassertIsRootProvider(false, true);\n" + + "\t\tserviceInfo(serviceInfo);\n" + + "\t}\n"); + } + + comment = "\n\t/**\n" + + "\t * Creates an instance given a config bean.\n" + + "\t * \n" + + "\t * @param configBean config bean\n" + + "\t */\n"; + result.add(comment + "\t@Override\n" + + "\tprotected " + activatorImplTypeName + " createInstance(Object configBean) {\n" + + "\t\treturn new " + activatorImplTypeName.className() + "((" + configBeanType + ") configBean);\n" + + "\t}\n"); + + if (!hasParent) { + result.add("\t@Override\n" + + "\tpublic Optional configBean() {\n" + + "\t\treturn Optional.ofNullable((CB) configBean);\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic Optional<" + Config.class.getName() + "> rawConfig() {\n" + + "\t\tif (configBean == null) {\n" + + "\t\t\treturn Optional.empty();\n" + + "\t\t}\n" + + "\t\treturn ((" + configBeanImplName + ") configBean).__config();\n" + + "\t}\n"); + } + + result.add("\t@Override\n" + + "\tpublic Class configBeanType() {\n" + + "\t\treturn " + configBeanType + ".class;\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic Map> configBeanAttributes() {\n" + + "\t\treturn " + configBeanImplName + ".__metaAttributes();\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic void visitAttributes(CB configBean, " + AttributeVisitor.class.getName() + + " visitor, R userDefinedCtx) {\n" + + "\t\t" + AttributeVisitor.class.getName() + " beanVisitor = visitor::visit;\n" + + "\t\t((" + configBeanImplName + ") configBean).visitAttributes(beanVisitor, userDefinedCtx);\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic CB toConfigBean(" + Config.class.getName() + " config) {\n" + + "\t\treturn (CB) " + configBeanImplName + "\n\t\t\t.toBuilder(config)\n\t\t\t.build();\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic " + configBeanImplName + ".Builder " + + "toConfigBeanBuilder(" + Config.class.getName() + " config) {\n" + + "\t\treturn " + configBeanImplName + ".toBuilder(config);\n" + + "\t}\n"); + + if (!hasParent) { + result.add("\t@Override\n" + + "\tprotected CB acceptConfig(io.helidon.common.config.Config config) {\n" + + "\t\tthis.configBean = (CB) super.acceptConfig(config);\n" + + "\t\treturn (CB) this.configBean;\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic String toConfigBeanInstanceId(CB configBean) {\n" + + "\t\treturn ((" + configBeanImplName + + ") configBean).__instanceId();\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic void configBeanInstanceId(CB configBean, String val) {\n" + + "\t\t((" + configBeanImplName + ") configBean).__instanceId(val);\n" + + "\t}\n"); + } + + String overridesEnabledStr = configuredByAttributes.get(TAG_OVERRIDE_BEAN); + if (Boolean.parseBoolean(overridesEnabledStr)) { + String drivesActivationStr = configuredByAttributes.get(ConfigBeanInfo.TAG_DRIVES_ACTIVATION); + result.add("\t@Override\n" + + "\tprotected boolean drivesActivation() {\n" + + "\t\treturn " + Boolean.parseBoolean(drivesActivationStr) + ";\n" + + "\t}\n"); + } + + return result; + } + + static List createExtraActivatorClassComments() { + return List.of("@param the config bean type"); + } + + static TypeName toDefaultImpl(TypeName configBeanType) { + return create(configBeanType.packageName(), + Builder.DEFAULT_IMPL_PREFIX + configBeanType.className() + Builder.DEFAULT_SUFFIX); + } + +} diff --git a/pico/configdriven/processor/src/main/java/module-info.java b/pico/configdriven/processor/src/main/java/module-info.java index 39a4aa0dc66..1493edfa4d1 100644 --- a/pico/configdriven/processor/src/main/java/module-info.java +++ b/pico/configdriven/processor/src/main/java/module-info.java @@ -26,15 +26,14 @@ requires io.helidon.builder.processor.tools; requires io.helidon.common.types; requires io.helidon.pico.api; - requires io.helidon.pico.configdriven.api; - requires io.helidon.pico.configdriven.runtime; requires transitive io.helidon.builder.config; requires transitive io.helidon.builder.processor; requires transitive io.helidon.builder.processor.spi; requires transitive io.helidon.pico.processor; + requires transitive io.helidon.pico.tools; exports io.helidon.pico.configdriven.processor; provides javax.annotation.processing.Processor with - io.helidon.pico.configdriven.processor.ConfiguredByProcessor; + io.helidon.pico.configdriven.processor.ConfiguredByAnnotationProcessor; } diff --git a/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java b/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java index a2b554c04be..67e89488df7 100644 --- a/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java +++ b/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java @@ -116,7 +116,7 @@ static Comparator configBeanComparator() { static BindableConfigBeanRegistry resolveConfigBeanRegistry() { HelidonConfigBeanRegistry cbr = ConfigBeanRegistryHolder.configBeanRegistry().orElse(null); if (cbr == null) { - LOGGER.log(System.Logger.Level.INFO, "Config-Driven Services disabled (config bean registry not found"); + LOGGER.log(System.Logger.Level.INFO, "Config-Driven Services disabled (config bean registry not found)"); return null; } @@ -371,7 +371,7 @@ public List> serviceProviders(ServiceInfoCriteria criteria, if (isRootProvider()) { Set qualifiers = criteria.qualifiers(); Optional configuredByQualifier = QualifierAndValueDefault - .findFirst(EMPTY_CONFIGURED_BY.typeName().name(), qualifiers); + .findFirst(EMPTY_CONFIGURED_BY.typeName(), qualifiers); boolean hasValue = configuredByQualifier.isPresent() && hasValue(configuredByQualifier.get().value().orElse(null)); boolean blankCriteria = qualifiers.isEmpty() && isBlank(criteria); diff --git a/pico/configdriven/tests/configuredby-application/pom.xml b/pico/configdriven/tests/configuredby-application/pom.xml index 8a900366644..ac2586ab683 100644 --- a/pico/configdriven/tests/configuredby-application/pom.xml +++ b/pico/configdriven/tests/configuredby-application/pom.xml @@ -98,7 +98,6 @@ -Apico.debug=${pico.debug} - -Apico.autoAddNonContractInterfaces=true -Apico.application.pre.create=true -Apico.mapApplicationToSingletonScope=true @@ -143,9 +142,6 @@ -Apico.debug=${pico.debug} -Apico.autoAddNonContractInterfaces=true -Apico.application.pre.create=true - - - NAMED diff --git a/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java b/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java index 7bcc2a17dd9..8f9e7b72d89 100644 --- a/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java +++ b/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java @@ -41,10 +41,10 @@ class ApplicationConfiguredByTest extends AbstractConfiguredByTest { /** - * In application mode, we should not have any lookups recorded. + * In application mode, we should not have many lookups recorded. */ @Test - void verifyNoLookups() { + void verifyMinimalLookups() { resetWith(io.helidon.config.Config.builder(createBasicTestingConfigSource(), createRootDefault8080TestingConfigSource()) .disableEnvironmentVariablesSource() .disableSystemPropertiesSource() @@ -52,18 +52,31 @@ void verifyNoLookups() { Metrics metrics = picoServices.metrics().orElseThrow(); Set criteriaSearchLog = picoServices.lookups().orElseThrow(); - Set contractSearchLog = criteriaSearchLog.stream().flatMap(it -> it.contractsImplemented().stream()) + Set contractSearchLog = criteriaSearchLog.stream() + .flatMap(it -> it.contractsImplemented().stream()) .collect(Collectors.toCollection(LinkedHashSet::new)); - - assertThat(contractSearchLog, + Set servicesSearchLog = criteriaSearchLog.stream() + .flatMap(it -> it.serviceTypeName().stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Set searchLog = new LinkedHashSet<>(contractSearchLog); + searchLog.addAll(servicesSearchLog); + + // we expect three classes of lookups here: + // 1) Any and all config beans (like FakeServerConfig). + // 2) Any *Optional* unknown services (like the FakeTracer). + // 3) Any intercepted service (like ZImpl). + assertThat(searchLog, containsInAnyOrder( // config beans are always looked up "io.helidon.builder.config.testsubjects.fakes.FakeServerConfig", // tracer doesn't really exist, so it is looked up out of best-effort (as an optional injection dep) - "io.helidon.builder.config.testsubjects.fakes.FakeTracer")); + "io.helidon.builder.config.testsubjects.fakes.FakeTracer", + // ZImpl is intercepted + "io.helidon.pico.configdriven.interceptor.test.ZImpl" + )); assertThat("lookup log: " + criteriaSearchLog, metrics.lookupCount().orElseThrow(), - is(2)); + is(3)); } @Test diff --git a/pico/configdriven/tests/configuredby/pom.xml b/pico/configdriven/tests/configuredby/pom.xml index 9058f31d0d6..8ccf21d8212 100644 --- a/pico/configdriven/tests/configuredby/pom.xml +++ b/pico/configdriven/tests/configuredby/pom.xml @@ -55,8 +55,8 @@ helidon-config - io.helidon.pico - helidon-pico-runtime + io.helidon.pico.configdriven + helidon-pico-configdriven-runtime jakarta.inject diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java index 89e6d8c7c55..3877c1ebada 100644 --- a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java @@ -270,7 +270,9 @@ protected void innerExecute() { // retrieves all the services in the registry List> allServices = services - .lookupAll(ServiceInfoCriteriaDefault.builder().build(), false); + .lookupAll(ServiceInfoCriteriaDefault.builder() + .includeIntercepted(true) + .build(), false); if (allServices.isEmpty()) { warn("no services to process"); return; @@ -343,7 +345,8 @@ protected void innerExecute() { getLog().error("failed to process", res.error().orElse(null)); } } catch (Exception e) { - throw new ToolsException("An error occurred creating the " + PicoServicesConfig.NAME + " Application", e); + throw new ToolsException("An error occurred creating the " + PicoServicesConfig.NAME + + " Application in " + getClass().getName(), e); } finally { Thread.currentThread().setContextClassLoader(prev); } diff --git a/pico/processor/etc/spotbugs/exclude.xml b/pico/processor/etc/spotbugs/exclude.xml index c559ec8db28..0ad17e3a215 100644 --- a/pico/processor/etc/spotbugs/exclude.xml +++ b/pico/processor/etc/spotbugs/exclude.xml @@ -24,13 +24,18 @@ - + - + + + + + + diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java b/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java new file mode 100644 index 00000000000..f6572b8ae23 --- /dev/null +++ b/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor; + +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import io.helidon.builder.processor.spi.TypeInfoCreatorProvider; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.tools.Messager; +import io.helidon.pico.tools.ModuleInfoDescriptor; +import io.helidon.pico.tools.Options; +import io.helidon.pico.tools.ServicesToProcess; + +import static io.helidon.pico.processor.GeneralProcessorUtils.toPath; +import static io.helidon.pico.tools.ModuleUtils.REAL_MODULE_INFO_JAVA_NAME; +import static io.helidon.pico.tools.ModuleUtils.inferSourceOrTest; + +/** + * Carries methods that are relative only during active APT processing. + * + * @see GeneralProcessorUtils + */ +final class ActiveProcessorUtils implements Messager { + static final boolean MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR = false; + static final String TARGET_DIR = "/target/"; + static final String SRC_MAIN_JAVA_DIR = "/src/main/java"; + + private final System.Logger logger; + private final ProcessingEnvironment processingEnv; + private final TypeInfoCreatorProvider typeInfoCreatorProvider; + private RoundEnvironment roundEnv; + + ActiveProcessorUtils(AbstractProcessor processor, + ProcessingEnvironment processingEnv) { + this.logger = System.getLogger(processor.getClass().getName()); + this.processingEnv = Objects.requireNonNull(processingEnv); + this.typeInfoCreatorProvider = HelidonServiceLoader.create( + ServiceLoader.load(TypeInfoCreatorProvider.class, TypeInfoCreatorProvider.class.getClassLoader())) + .asList() + .stream() + .findFirst() + .orElseThrow(() -> new IllegalStateException("No available " + TypeInfoCreatorProvider.class)); + + Options.init(processingEnv); + debug("*** Processing " + processor.getClass().getSimpleName() + " ***"); + } + + @Override + public void debug(String message, + Throwable t) { + if (Options.isOptionEnabled(Options.TAG_DEBUG)) { + out(System.Logger.Level.DEBUG, Diagnostic.Kind.OTHER, message, t); + } + } + + @Override + public void debug(String message) { + if (Options.isOptionEnabled(Options.TAG_DEBUG)) { + out(System.Logger.Level.DEBUG, Diagnostic.Kind.OTHER, message, null); + } + } + + @Override + public void log(String message) { + if (Options.isOptionEnabled(Options.TAG_DEBUG)) { + out(System.Logger.Level.INFO, Diagnostic.Kind.NOTE, message, null); + } + } + + @Override + public void warn(String message, + Throwable t) { + out(System.Logger.Level.WARNING, Diagnostic.Kind.WARNING, message, t); + } + + @Override + public void warn(String message) { + out(System.Logger.Level.WARNING, Diagnostic.Kind.WARNING, message, null); + } + + @Override + public void error(String message, + Throwable t) { + out(System.Logger.Level.ERROR, Diagnostic.Kind.ERROR, message, null); + } + + void out(System.Logger.Level level, Diagnostic.Kind kind, String message, Throwable t) { + if (logger.isLoggable(level)) { + logger.log(level, getClass().getSimpleName() + ": " + message, t); + } + + if (processingEnv != null && processingEnv.getMessager() != null) { + processingEnv.getMessager().printMessage(kind, message); + } + } + + /** + * Attempts to load the {@link ModuleInfoDescriptor} for the (src or test) module being processed. + * + * @param typeSuffix this function will populate this with an empty string for src and "test" for test + * @param moduleInfoFile this function will populate this with the file path to the module-info source file + * @param srcPath this function will populate this with the source path + * @return the module info descriptor if the module being processed has one available + */ + // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here + Optional thisModuleDescriptor(AtomicReference typeSuffix, + AtomicReference moduleInfoFile, + AtomicReference srcPath) { + return tryFindModuleInfoTheConventionalWay(StandardLocation.SOURCE_OUTPUT, typeSuffix, moduleInfoFile, srcPath) + .or(() -> tryFindModuleInfoTheConventionalWay(StandardLocation.SOURCE_PATH, typeSuffix, moduleInfoFile, srcPath)) + // attempt to retrieve from src/main/java if we can't recover to this point + .or(() -> tryFindModuleInfoTheUnconventionalWayFromSourceMain(moduleInfoFile, srcPath)); + } + + /** + * Determines the module being processed, and relays module-info information into the provided services to process. + * + * @param servicesToProcess the services to process instance + */ + void relayModuleInfoToServicesToProcess(ServicesToProcess servicesToProcess) { + // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here + AtomicReference typeSuffix = new AtomicReference<>(); + AtomicReference moduleInfoFile = new AtomicReference<>(); + AtomicReference srcPath = new AtomicReference<>(); + ModuleInfoDescriptor thisModuleDescriptor = thisModuleDescriptor(typeSuffix, moduleInfoFile, srcPath).orElse(null); + if (thisModuleDescriptor != null) { + servicesToProcess.lastKnownModuleInfoDescriptor(thisModuleDescriptor); + } else { + String thisModuleName = Options.getOption(Options.TAG_MODULE_NAME).orElse(null); + if (thisModuleName == null) { + servicesToProcess.clearModuleName(); + } else { + servicesToProcess.moduleName(thisModuleName); + } + } + if (typeSuffix.get() != null) { + servicesToProcess.lastKnownTypeSuffix(typeSuffix.get()); + } + if (srcPath.get() != null) { + servicesToProcess.lastKnownSourcePathBeingProcessed(srcPath.get().toPath()); + } + if (moduleInfoFile.get() != null) { + servicesToProcess.lastKnownModuleInfoFilePath(moduleInfoFile.get().toPath()); + } + } + + /** + * Converts the provided element and mirror into a {@link TypeInfo} structure. + * + * @param element the element of the target service + * @param mirror the type mirror of the target service + * @param isOneWeCareAbout a predicate filter that is used to determine the elements of particular interest (e.g., injectable) + * @return the type info for the target + */ + Optional toTypeInfo(TypeElement element, + TypeMirror mirror, + Predicate isOneWeCareAbout) { + return typeInfoCreatorProvider.createTypeInfo(element, mirror, processingEnv, isOneWeCareAbout); + } + + System.Logger.Level loggerLevel() { + return (Options.isOptionEnabled(Options.TAG_DEBUG)) ? System.Logger.Level.INFO : System.Logger.Level.DEBUG; + } + + RoundEnvironment roundEnv() { + return roundEnv; + } + + void roundEnv(RoundEnvironment roundEnv) { + this.roundEnv = roundEnv; + } + + // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here + private Optional tryFindModuleInfoTheUnconventionalWayFromSourceMain( + AtomicReference moduleInfoFile, + AtomicReference srcPath) { + if (srcPath != null && srcPath.get() != null && srcPath.get().getPath().contains(TARGET_DIR)) { + String path = srcPath.get().getPath(); + int pos = path.indexOf(TARGET_DIR); + path = path.substring(0, pos); + File srcRoot = new File(path, SRC_MAIN_JAVA_DIR); + File file = new File(srcRoot, REAL_MODULE_INFO_JAVA_NAME); + if (file.exists()) { + try { + return Optional.of( + ModuleInfoDescriptor.create(file.toPath(), ModuleInfoDescriptor.Ordering.NATURAL_PRESERVE_COMMENTS)); + } catch (Exception e) { + debug("unable to read source module-info.java from: " + srcRoot + "; " + e.getMessage(), e); + } + + if (moduleInfoFile != null) { + moduleInfoFile.set(file); + } + } + } + + debug("unable to find module-info.java from: " + srcPath); + return Optional.empty(); + } + + // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here + private Optional tryFindModuleInfoTheConventionalWay(StandardLocation location, + AtomicReference typeSuffix, + AtomicReference moduleInfoFile, + AtomicReference srcPath) { + Filer filer = processingEnv.getFiler(); + try { + FileObject f = filer.getResource(location, "", REAL_MODULE_INFO_JAVA_NAME); + URI uri = f.toUri(); + Path filePath = toPath(uri).orElse(null); + if (filePath != null) { + Path parent = filePath.getParent(); + if (srcPath != null) { + srcPath.set(parent.toFile()); + } + if (typeSuffix != null) { + String type = inferSourceOrTest(parent); + typeSuffix.set(type); + } + if (filePath.toFile().exists()) { + if (moduleInfoFile != null) { + moduleInfoFile.set(filePath.toFile()); + } + return Optional.of(ModuleInfoDescriptor.create(filePath)); + } + } + } catch (Exception e) { + debug("unable to retrieve " + location + " from filer: " + e.getMessage()); + } + + return Optional.empty(); + } + +} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/BaseAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/BaseAnnotationProcessor.java index c2bfd94389a..ccd18e01c11 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/BaseAnnotationProcessor.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/BaseAnnotationProcessor.java @@ -16,109 +16,24 @@ package io.helidon.pico.processor; -import java.io.IOException; -import java.lang.System.Logger.Level; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.Stack; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.ModuleElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.TypeParameterElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; -import javax.tools.JavaFileObject; -import io.helidon.common.Weight; -import io.helidon.common.types.AnnotationAndValue; -import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypeNameDefault; -import io.helidon.pico.api.Contract; -import io.helidon.pico.api.ExternalContracts; -import io.helidon.pico.api.InjectionPointInfo; -import io.helidon.pico.api.QualifierAndValue; -import io.helidon.pico.api.RunLevel; -import io.helidon.pico.api.ServiceInfoBasics; -import io.helidon.pico.api.ServiceInfoDefault; -import io.helidon.pico.tools.AbstractFilerMessager; -import io.helidon.pico.tools.ActivatorCreatorCodeGen; -import io.helidon.pico.tools.ActivatorCreatorConfigOptionsDefault; -import io.helidon.pico.tools.ActivatorCreatorDefault; -import io.helidon.pico.tools.ActivatorCreatorProvider; -import io.helidon.pico.tools.ActivatorCreatorRequest; -import io.helidon.pico.tools.ActivatorCreatorResponse; -import io.helidon.pico.tools.CodeGenFiler; -import io.helidon.pico.tools.GeneralCreatorRequest; -import io.helidon.pico.tools.GeneralCreatorRequestDefault; -import io.helidon.pico.tools.InterceptionPlan; -import io.helidon.pico.tools.InterceptorCreatorProvider; -import io.helidon.pico.tools.Messager; -import io.helidon.pico.tools.Options; -import io.helidon.pico.tools.ServicesToProcess; -import io.helidon.pico.tools.ToolsException; -import io.helidon.pico.tools.TypeNames; -import io.helidon.pico.tools.TypeTools; -import io.helidon.pico.tools.spi.ActivatorCreator; -import io.helidon.pico.tools.spi.InterceptorCreator; - -import com.sun.source.util.TreePath; -import com.sun.source.util.Trees; - -import static io.helidon.builder.processor.tools.BuilderTypeTools.createTypeNameFromElement; -import static io.helidon.builder.processor.tools.BuilderTypeTools.extractValue; -import static io.helidon.builder.processor.tools.BuilderTypeTools.extractValues; -import static io.helidon.builder.processor.tools.BuilderTypeTools.findAnnotationMirror; -import static io.helidon.pico.processor.ProcessorUtils.rootStackTraceElementOf; -import static io.helidon.pico.processor.ProcessorUtils.toList; -import static io.helidon.pico.processor.ProcessorUtils.toPath; -import static io.helidon.pico.tools.ModuleUtils.toSourcePath; -import static io.helidon.pico.tools.TypeTools.createAnnotationAndValueListFromElement; -import static io.helidon.pico.tools.TypeTools.createAnnotationAndValueSet; -import static io.helidon.pico.tools.TypeTools.createQualifierAndValueSet; -import static io.helidon.pico.tools.TypeTools.createTypeNameFromMirror; -import static io.helidon.pico.tools.TypeTools.isAbstract; -import static io.helidon.pico.tools.TypeTools.isProviderType; -import static io.helidon.pico.tools.TypeTools.needToDeclareModuleUsage; -import static io.helidon.pico.tools.TypeTools.oppositeOf; -import static io.helidon.pico.tools.TypeTools.toAccess; -import static javax.tools.Diagnostic.Kind; /** * Abstract base for all Helidon Pico annotation processing. - * - * @param the type handled by this anno processor */ -abstract class BaseAnnotationProcessor extends AbstractProcessor implements Messager { - static final boolean MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR = false; - static final String TARGET_DIR = "/target/"; - static final String SRC_MAIN_JAVA_DIR = "/src/main/java"; - +abstract class BaseAnnotationProcessor extends AbstractProcessor { private final System.Logger logger = System.getLogger(getClass().getName()); - private final ServicesToProcess services; - private final InterceptorCreator interceptorCreator; - private final Map deferredMoves = new LinkedHashMap<>(); - private RoundEnvironment roundEnv; + + private ActiveProcessorUtils utils; /** * Service loader based constructor. @@ -127,25 +42,6 @@ abstract class BaseAnnotationProcessor extends AbstractProcessor implements M */ @Deprecated protected BaseAnnotationProcessor() { - try { - this.services = ServicesToProcess.servicesInstance(); - this.interceptorCreator = InterceptorCreatorProvider.instance(); - } catch (Throwable t) { - logger().log(Level.ERROR, "failed to initialize: " + t.getMessage(), t); - throw new ToolsException("Failed to initialize: " + t.getMessage(), t); - } - } - - static boolean hasValue(String val) { - return (val != null && !val.isBlank()); - } - - @Override - public void init(ProcessingEnvironment processingEnv) { - this.processingEnv = processingEnv; - Options.init(processingEnv); - debug("*** Processing " + getClass().getSimpleName() + " ***"); - super.init(processingEnv); } @Override @@ -154,701 +50,24 @@ public SourceVersion getSupportedSourceVersion() { } @Override - public Set getSupportedAnnotationTypes() { - return annoTypes(); - } - - @Override - public boolean process(Set annotations, - RoundEnvironment roundEnv) { - this.roundEnv = roundEnv; - - try { - ServicesToProcess.onBeginProcessing(this, annotations, roundEnv); - - if (!roundEnv.processingOver()) { - for (String annoType : annoTypes()) { - // annotation may not be on the classpath, in such a case just ignore it - TypeName annoName = TypeNameDefault.createFromTypeName(annoType); - Optional annoTypeElement = toTypeElement(annoName); - if (annoTypeElement.isPresent()) { - Set typesToProcess = roundEnv.getElementsAnnotatedWith(annoTypeElement.get()); - Set contraAnnotations = contraAnnotations(); - if (!contraAnnotations.isEmpty()) { - // filter out the ones we will not do... - typesToProcess = typesToProcess.stream() - .filter(it -> !containsAnyAnnotation(it, contraAnnotations)) - .collect(Collectors.toSet()); - } - - doBulkInner(typesToProcess, null, null); - } - } - } - - boolean claimedResult = doFiler(roundEnv); - ServicesToProcess.onEndProcessing(this, annotations, roundEnv); - return claimedResult; - } catch (Throwable t) { - error(getClass().getSimpleName() + " error during processing; " + t + " @ " - + rootStackTraceElementOf(t), t); - // we typically will not even get to this next line since the messager.error() call will trigger things to halt - throw new ToolsException("Error during processing: " + t - + " @ " + rootStackTraceElementOf(t) - + " in " + getClass().getSimpleName(), t); - } - } - - @Override - public void debug(String message, - Throwable t) { - if (Options.isOptionEnabled(Options.TAG_DEBUG)) { - if (logger.isLoggable(loggerLevel())) { - logger.log(loggerLevel(), getClass().getSimpleName() + ": Debug: " + message, t); - } - } - if (processingEnv != null && processingEnv.getMessager() != null) { - processingEnv.getMessager().printMessage(Kind.OTHER, message); - } - } - - @Override - public void debug(String message) { - if (Options.isOptionEnabled(Options.TAG_DEBUG)) { - if (logger.isLoggable(loggerLevel())) { - logger.log(loggerLevel(), getClass().getSimpleName() + ": Debug: " + message); - } - } - if (processingEnv != null && processingEnv.getMessager() != null) { - processingEnv.getMessager().printMessage(Kind.OTHER, message); - } - } - - @Override - public void log(String message) { - // logger.log(getLevel(), getClass().getSimpleName() + ": Note: " + message); - if (processingEnv != null && processingEnv.getMessager() != null) { - processingEnv.getMessager().printMessage(Kind.NOTE, message); - } - } - - @Override - public void warn(String message, - Throwable t) { - if (Options.isOptionEnabled(Options.TAG_DEBUG) && t != null) { - logger.log(Level.WARNING, getClass().getSimpleName() + ": Warning: " + message, t); - t.printStackTrace(); - } - if (processingEnv != null && processingEnv.getMessager() != null) { - processingEnv.getMessager().printMessage(Kind.WARNING, message); - } - } - - @Override - public void warn(String message) { - if (Options.isOptionEnabled(Options.TAG_DEBUG)) { - logger.log(Level.WARNING, getClass().getSimpleName() + ": Warning: " + message); - } - if (processingEnv != null && processingEnv.getMessager() != null) { - processingEnv.getMessager().printMessage(Kind.WARNING, message); - } + public void init(ProcessingEnvironment processingEnv) { + this.utils = new ActiveProcessorUtils(this, processingEnv); + super.init(processingEnv); } @Override - public void error(String message, - Throwable t) { - logger.log(Level.ERROR, getClass().getSimpleName() + ": Error: " + message, t); - if (processingEnv != null && processingEnv.getMessager() != null) { - processingEnv.getMessager().printMessage(Kind.ERROR, message); - } - } - - /** - * The annotation types we handle. - * - * @return annotation types we handle - */ - protected abstract Set annoTypes(); - - /** - * If these annotation type names are found on the target then do not process. - * - * @return the set of annotation type names that should result in skipped processing for this processor type - */ - protected Set contraAnnotations() { - return Set.of(); - } - - ServicesToProcess servicesToProcess() { - return services; - } - - int doBulkInner(Set typesToProcess, - TypeName serviceTypeName, - B builder) { - int injectedCtorCount = 0; - - try { - for (Element typeToProcess : typesToProcess) { - assert (typeToProcess != null); - if (serviceTypeName != null && !typeToProcess.getEnclosingElement().toString() - .equals(serviceTypeName.name())) { - continue; - } - - if (typeToProcess instanceof TypeElement) { - doInner((TypeElement) typeToProcess, builder); - } else if (typeToProcess instanceof ExecutableElement) { - doInner((ExecutableElement) typeToProcess, builder); - if (typeToProcess.getKind() == ElementKind.CONSTRUCTOR) { - injectedCtorCount++; - } - } else if (typeToProcess instanceof VariableElement) { - doInner(null, - (VariableElement) typeToProcess, - builder, - null, - 0, - null, - null, - null, - null); - } - } - } catch (Throwable t) { - throw new ToolsException("Error detected while processing: " + typesToProcess - + " for " + serviceTypeName + ": " + t, t); - } - - return injectedCtorCount; - } - - void doInner(ExecutableElement method, - B builder) { - // NOP - } - - @SuppressWarnings("checkstyle:ParameterNumber") - void doInner(String serviceTypeName, - VariableElement var, - B builder, - String elemName, - int elemArgs, - Integer elemOffset, - InjectionPointInfo.ElementKind elemKind, - InjectionPointInfo.Access access, - Boolean isStaticAlready) { - // NOP - } - - void doInner(TypeElement type, - B builder) { - createTypeNameFromElement(type).ifPresent(serviceTypeName -> processServiceType(serviceTypeName, type)); - } - - /** - * Called to process the service type definition to add the basic service info constructs. - * - * @param serviceTypeName the service type name - * @param type the type element - */ - protected void processServiceType(TypeName serviceTypeName, - TypeElement type) { - maybeSetBasicsForServiceType(serviceTypeName, type); - maybeSetContractsAndModulesForServiceType(serviceTypeName, type); - maybeSetInterceptorPlanForServiceType(serviceTypeName, type); - } - - void maybeSetInterceptorPlanForServiceType(TypeName serviceTypeName, - TypeElement ignoredType) { - if (services.hasVisitedInterceptorPlanFor(serviceTypeName)) { - // if we processed it already then there is no reason to check again - return; - } - - // note: it is important to use this class' CL since maven will not give us the "right" one. - ServiceInfoBasics interceptedServiceInfo = toInterceptedServiceInfoFor(serviceTypeName); - InterceptorCreator.InterceptorProcessor processor = interceptorCreator.createInterceptorProcessor( - interceptedServiceInfo, - interceptorCreator, - Optional.of(processingEnv)); - Set annotationTypeTriggers = processor.allAnnotationTypeTriggers(); - if (annotationTypeTriggers.isEmpty()) { - services.addInterceptorPlanFor(serviceTypeName, Optional.empty()); - return; - } - - Optional plan = processor.createInterceptorPlan(annotationTypeTriggers); - if (plan.isEmpty()) { - warn("unable to produce an interception plan for: " + serviceTypeName); - } - services.addInterceptorPlanFor(serviceTypeName, plan); - } - - ServiceInfoBasics toInterceptedServiceInfoFor(TypeName serviceTypeName) { - return ServiceInfoDefault.builder() - .serviceTypeName(serviceTypeName.name()) - .declaredWeight(Optional.ofNullable(services.weightedPriorities().get(serviceTypeName))) - .declaredRunLevel(Optional.ofNullable(services.runLevels().get(serviceTypeName))) - .scopeTypeNames(services.scopeTypeNames().getOrDefault(serviceTypeName, Set.of())) - .build(); - } - - void maybeSetContractsAndModulesForServiceType(TypeName serviceTypeName, - TypeElement type) { - if (services.hasContractsFor(serviceTypeName)) { - return; - } - - Set providerForSet = new LinkedHashSet<>(); - Set externalModuleNamesRequired = new LinkedHashSet<>(); - Set contracts = toContracts(type, providerForSet); - Set externalContracts = toExternalContracts(type, externalModuleNamesRequired); - adjustContractsForExternals(contracts, externalContracts, externalModuleNamesRequired); - Set allContracts = new LinkedHashSet<>(); - allContracts.addAll(contracts); - allContracts.addAll(externalContracts); - allContracts.addAll(providerForSet); - - debug("found contracts " + allContracts + " for " + serviceTypeName); - for (TypeName contract : allContracts) { - boolean isExternal = externalContracts.contains(contract); - services.addTypeForContract(serviceTypeName, contract, isExternal); - } - if (!providerForSet.isEmpty()) { - services.addProviderFor(serviceTypeName, providerForSet); - } - if (!externalModuleNamesRequired.isEmpty()) { - services.addExternalRequiredModules(serviceTypeName, externalModuleNamesRequired); - } - } - - void maybeSetBasicsForServiceType(TypeName serviceTypeName, - TypeElement type) { - if (services.hasHierarchyFor(serviceTypeName)) { - return; - } - - debug("processing service type basics for " + serviceTypeName); - if (type == null) { - type = processingEnv.getElementUtils().getTypeElement(serviceTypeName.name()); - if (type == null) { - warn("expected to find a typeElement for " + serviceTypeName); - return; - } - } - - TypeElement superTypeElement = TypeTools.toTypeElement(type.getSuperclass()).orElse(null); - TypeName parentServiceTypeName = (superTypeElement == null) - ? null : createTypeNameFromElement(superTypeElement).orElseThrow(); - services.addParentServiceType(serviceTypeName, parentServiceTypeName); - services.addAccessLevel(serviceTypeName, toAccess(type)); - services.addIsAbstract(serviceTypeName, isAbstract(type)); - services.addServiceTypeHierarchy(serviceTypeName, toServiceTypeHierarchy(type, true)); - - AnnotationMirror runLevel = findAnnotationMirror(RunLevel.class.getName(), type.getAnnotationMirrors()) - .orElse(null); - if (runLevel != null) { - String val = extractValue(runLevel, processingEnv.getElementUtils()); - services.addDeclaredRunLevel(serviceTypeName, Integer.parseInt(val)); - } - - List scopeAnnotations = annotationsWithAnnotationOf(type, TypeNames.JAKARTA_SCOPE); - if (scopeAnnotations.isEmpty()) { - scopeAnnotations = annotationsWithAnnotationOf(type, TypeNames.JAKARTA_CDI_NORMAL_SCOPE); - } - scopeAnnotations.forEach(scope -> services.addScopeTypeName(serviceTypeName, scope)); - if (Options.isOptionEnabled(Options.TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE) - && (scopeAnnotations.contains(TypeNames.JAVAX_APPLICATION_SCOPED) - || scopeAnnotations.contains(TypeNames.JAKARTA_APPLICATION_SCOPED))) { - services.addScopeTypeName(serviceTypeName, TypeNames.JAKARTA_SINGLETON); - } - - Set qualifiers = createQualifierAndValueSet(type); - if (!qualifiers.isEmpty()) { - services.addQualifiers(serviceTypeName, qualifiers); - } - - AnnotationMirror weight = findAnnotationMirror(Weight.class.getName(), type.getAnnotationMirrors()) - .orElse(null); - if (weight != null) { - String val = extractValue(weight, processingEnv.getElementUtils()); - services.addDeclaredWeight(serviceTypeName, Double.parseDouble(val)); - } else { - processPriority(serviceTypeName, type); - } - } - - boolean processPriority(TypeName serviceTypeName, - TypeElement type) { - Optional mirror = findAnnotationMirror(TypeNames.JAKARTA_PRIORITY, - type.getAnnotationMirrors()); - if (mirror.isEmpty()) { - mirror = findAnnotationMirror(TypeNames.JAVAX_PRIORITY, type.getAnnotationMirrors()); - } - - if (mirror.isEmpty()) { - // no priority defined - return false; - } - - String priorityString = extractValues(mirror.get().getElementValues()).get("value"); - if (priorityString == null) { - return false; - } - int priority = Integer.parseInt(priorityString); - services.addDeclaredWeight(serviceTypeName, (double) priority); - return true; - } - - List toServiceTypeHierarchy(TypeElement type, - boolean includeSelf) { - List result = new ArrayList<>(); - if (!includeSelf) { - TypeMirror mirror = type.getSuperclass(); - type = TypeTools.toTypeElement(mirror).orElse(null); - } - while (type != null) { - result.add(0, createTypeNameFromElement(type).orElseThrow()); - TypeMirror mirror = type.getSuperclass(); - type = TypeTools.toTypeElement(mirror).orElse(null); - } - return result; - } - - void adjustContractsForExternals(Set contracts, - Set externalContracts, - Set externalModuleNamesRequired) { - AtomicReference externalModuleName = new AtomicReference<>(); - for (TypeName contract : contracts) { - Optional typeElement = toTypeElement(contract); - if (typeElement.isPresent() - && !isInThisModule(typeElement.get(), externalModuleName)) { - maybeAddExternalModule(externalModuleName.get(), externalModuleNamesRequired); - externalContracts.add(contract); - } - } - - for (TypeName externalContract : externalContracts) { - Optional typeElement = toTypeElement(externalContract); - if (typeElement.isPresent() - && isInThisModule(typeElement.get(), externalModuleName)) { - warn(externalContract + " is actually in this module and therefore should not be labelled as external.", null); - maybeAddExternalModule(externalModuleName.get(), externalModuleNamesRequired); - } - } - - contracts.removeAll(externalContracts); - } - - void maybeAddExternalModule(String externalModuleName, - Set externalModuleNamesRequired) { - if (needToDeclareModuleUsage(externalModuleName)) { - externalModuleNamesRequired.add(externalModuleName); - } - } - - boolean isInThisModule(TypeElement type, - AtomicReference moduleName) { - if (roundEnv.getRootElements().contains(type)) { - return true; - } - - moduleName.set(null); - // if there is no module-info in use we need to try to find the type is in our source path and if - // not found then assume it is external - try { - Trees trees = Trees.instance(processingEnv); - TreePath path = trees.getPath(type); - if (path == null) { - return false; - } - JavaFileObject sourceFile = path.getCompilationUnit().getSourceFile(); - Optional filePath = toPath(sourceFile.toUri()); - filePath.flatMap(it -> toSourcePath(it, type)) - .ifPresent(services::lastKnownSourcePathBeingProcessed); - return true; - } catch (Throwable t) { - debug("unable to determine if contract is external: " + type + "; " + t.getMessage(), t); - } - - ModuleElement module = processingEnv.getElementUtils().getModuleOf(type); - if (!module.isUnnamed()) { - String name = module.getQualifiedName().toString(); - moduleName.set(name); - } - - // assumed external, but unknown module name - return false; - } - - List annotationsWithAnnotationOf(TypeElement type, - String annotation) { - List list = annotationsWithAnnotationsOfNoOpposite(type, annotation); - if (list.isEmpty()) { - return annotationsWithAnnotationsOfNoOpposite(type, oppositeOf(annotation)); - } - - return list; - } - - CodeGenFiler createCodeGenFiler() { - AbstractFilerMessager filer = AbstractFilerMessager.createAnnotationBasedFiler(processingEnv, this); - return CodeGenFiler.create(filer); - } - - boolean doFiler(RoundEnvironment roundEnv) { - // don't do filer until very end of the round - boolean isProcessingOver = roundEnv.processingOver(); - ActivatorCreator creator = ActivatorCreatorProvider.instance(); - CodeGenFiler filer = createCodeGenFiler(); - - Map interceptionPlanMap = services.interceptorPlans(); - if (!interceptionPlanMap.isEmpty()) { - GeneralCreatorRequest req = GeneralCreatorRequestDefault.builder() - .filer(filer); - creator.codegenInterceptors(req, interceptionPlanMap); - services.clearInterceptorPlans(); - } - - if (!isProcessingOver) { - return MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; - } - - ActivatorCreatorCodeGen codeGen = ActivatorCreatorDefault.createActivatorCreatorCodeGen(services).orElse(null); - if (codeGen == null) { - return MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; - } - - ActivatorCreatorConfigOptionsDefault configOptions = ActivatorCreatorConfigOptionsDefault.builder() - .applicationPreCreated(Options.isOptionEnabled(Options.TAG_APPLICATION_PRE_CREATE)) - .moduleCreated(isProcessingOver) - .build(); - ActivatorCreatorRequest req = ActivatorCreatorDefault - .createActivatorCreatorRequest(services, codeGen, configOptions, filer, false); - - try { - services.lastGeneratedPackageName(req.packageName().orElseThrow()); - ActivatorCreatorResponse res = creator.createModuleActivators(req); - if (!res.success()) { - throw new ToolsException("error during codegen", res.error().orElse(null)); - } - deferredMoves.putAll(filer.deferredMoves()); - } catch (Exception te) { - Object hierarchy = codeGen.serviceTypeHierarchy(); - if (hierarchy == null) { - warn("expected to have a known service type hierarchy in the context"); - } else { - debug("service type hierarchy is " + hierarchy); - } - - ToolsException revisedTe = new ToolsException("Error detected while processing " + req.serviceTypeNames(), te); - error(revisedTe.getMessage(), revisedTe); - } finally { - if (isProcessingOver) { - handleDeferredMoves(); - processingEnv.getMessager().printMessage(Kind.OTHER, getClass().getSimpleName() - + ": processing is over - resetting"); - services.reset(false); - } - } - - // allow other processors to also process these same annotations? - return MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; - } - - Set toContracts(TypeElement type, - Set providerForSet) { - Set result = new LinkedHashSet<>(); - - Set processed = new LinkedHashSet<>(); - gatherContractsToBeProcessed(processed, type); - - for (TypeMirror possibleContract : processed) { - TypeElement teContract = TypeTools.toTypeElement(possibleContract).orElse(null); - if (teContract == null) { - continue; - } - - TypeName parentTe = createTypeNameFromElement(teContract).orElse(null); - if (findAnnotationMirror(Contract.class.getName(), teContract.getAnnotationMirrors()).isPresent()) { - result.add(parentTe); - continue; - } else if (Options.isOptionEnabled(Options.TAG_AUTO_ADD_NON_CONTRACT_INTERFACES) - && (ElementKind.INTERFACE == teContract.getKind())) { - result.add(parentTe); - // fall in the next section, skip continue here - } else if (services.serviceTypeNames().contains(parentTe)) { - result.add(parentTe); - } - - String potentialProviderClassName = (1 == teContract.getTypeParameters().size()) - ? teContract.getQualifiedName().toString() : null; - boolean isProviderType = (potentialProviderClassName != null) && isProviderType(potentialProviderClassName); - if (!isProviderType) { - continue; - } - - TypeParameterElement tpe = teContract.getTypeParameters().get(0); - if (1 != tpe.getBounds().size()) { - continue; - } - - TypeMirror gType = ((DeclaredType) possibleContract).getTypeArguments().get(0); - if (gType == null) { - continue; - } - - TypeName gTypeName = createTypeNameFromMirror(gType).orElse(null); - if (gTypeName == null) { - continue; - } - - // if we made it here then this provider qualifies, and we take what it provides into the result - TypeName teContractName = createTypeNameFromElement(teContract).orElseThrow(); - result.add(teContractName); - if (isProviderType(teContractName.name())) { - result.add(TypeNameDefault.createFromTypeName(TypeNames.JAKARTA_PROVIDER)); - } - if (!gTypeName.generic()) { - providerForSet.add(gTypeName); - } - } - - if (!result.isEmpty()) { - debug("Contracts for " + type + " was " + result + " w/ providerSet " + providerForSet); - } - return result; - } - - void gatherContractsToBeProcessed(Set processed, - TypeElement typeElement) { - if (typeElement == null) { - return; - } - - typeElement.getInterfaces().forEach(tm -> { - processed.add(tm); - gatherContractsToBeProcessed(processed, TypeTools.toTypeElement(tm).orElse(null)); - }); - - toServiceTypeHierarchy(typeElement, false).stream() - .map(te -> toTypeElement(te).orElseThrow().asType()) - .forEach(tm -> { - processed.add(tm); - gatherContractsToBeProcessed(processed, TypeTools.toTypeElement(tm).orElse(null)); - }); - } - - Set toExternalContracts(TypeElement type, - Set externalModulesRequired) { - Set result = new LinkedHashSet<>(); - - Stack stack = new Stack<>(); - stack.push(type.asType()); - stack.addAll(type.getInterfaces()); - stack.add(type.getSuperclass()); - - TypeMirror iface; - while (!stack.isEmpty()) { - iface = stack.pop(); - TypeElement teContract = TypeTools.toTypeElement(iface).orElse(null); - if (teContract == null) { - continue; - } - - stack.addAll(teContract.getInterfaces()); - stack.add(teContract.getSuperclass()); - - AnnotationMirror externalContracts = findAnnotationMirror(ExternalContracts.class.getName(), - teContract.getAnnotationMirrors()).orElse(null); - if (externalContracts != null) { - Collection annotations = createAnnotationAndValueSet(teContract); - Optional annotation = AnnotationAndValueDefault - .findFirst(ExternalContracts.class.getName(), annotations); - List values = (annotation.isPresent() && annotation.get().value().isPresent()) - ? toList(annotation.get().value().get()) : List.of(); - for (String externalContract : values) { - result.add(TypeNameDefault.createFromTypeName(externalContract)); - } - Map map = extractValues(externalContracts, processingEnv.getElementUtils()); - String moduleNames = map.get("moduleNames"); - if (hasValue(moduleNames)) { - externalModulesRequired.addAll(Arrays.asList(moduleNames.split(",[ ]*"))); - } - continue; - } - - String potentialProviderClassName = (1 == teContract.getTypeParameters().size()) - ? teContract.getQualifiedName().toString() : null; - boolean isProviderType = (potentialProviderClassName != null) && isProviderType(potentialProviderClassName); - if (!isProviderType) { - continue; - } - - TypeParameterElement tpe = teContract.getTypeParameters().get(0); - if (1 != tpe.getBounds().size()) { - continue; - } - - TypeMirror gType = ((DeclaredType) iface).getTypeArguments().get(0); - if (gType != null) { - stack.add(gType); - } - } - - if (!result.isEmpty()) { - debug("ExternalContracts for " + type + " was " + result + " w/ modulesRequired " + externalModulesRequired); - } - return result; - } - - Optional toTypeElement(TypeName typeName) { - return Optional.ofNullable(processingEnv.getElementUtils().getTypeElement(typeName.name())); - } + public abstract Set getSupportedAnnotationTypes(); System.Logger logger() { return logger; } - Level loggerLevel() { - return (Options.isOptionEnabled(Options.TAG_DEBUG)) ? Level.INFO : Level.DEBUG; + ActiveProcessorUtils utils() { + return Objects.requireNonNull(utils); } - private boolean containsAnyAnnotation(Element element, - Set contraAnnotations) { - List annotationAndValues = - createAnnotationAndValueListFromElement(element, processingEnv.getElementUtils()); - Optional annotation = annotationAndValues.stream() - .filter(it -> contraAnnotations.contains(it.typeName().name())) - .findFirst(); - return annotation.isPresent(); - } - - private List annotationsWithAnnotationsOfNoOpposite(TypeElement type, - String annotation) { - List list = new ArrayList<>(); - type.getAnnotationMirrors() - .forEach(am -> findAnnotationMirror(annotation, - am.getAnnotationType().asElement() - .getAnnotationMirrors()) - .ifPresent(it -> list.add(am.getAnnotationType().asElement().toString()))); - return list; - } - - private void handleDeferredMoves() { - if (logger.isLoggable(Level.INFO) && !deferredMoves.isEmpty()) { - logger.log(Level.INFO, "handling deferred moves: " + deferredMoves); - } - - try { - for (Map.Entry e : deferredMoves.entrySet()) { - Files.move(e.getKey(), e.getValue()); - } - } catch (IOException e) { - throw new ToolsException(e.getMessage(), e); - } - deferredMoves.clear(); + Optional toTypeElement(TypeName typeName) { + return Optional.ofNullable(processingEnv.getElementUtils().getTypeElement(typeName.name())); } } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ContractAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/ContractAnnotationProcessor.java deleted file mode 100644 index 546d8ea93aa..00000000000 --- a/pico/processor/src/main/java/io/helidon/pico/processor/ContractAnnotationProcessor.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.pico.processor; - -import java.util.Set; - -import javax.lang.model.element.TypeElement; - -import io.helidon.pico.tools.TypeNames; - -/** - * Handles {@code @Contract} annotations. - */ -public class ContractAnnotationProcessor extends BaseAnnotationProcessor { - - /** - * Service loader based constructor. - * - * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly - */ - @Deprecated - public ContractAnnotationProcessor() { - } - - @Override - public Set annoTypes() { - return Set.of(TypeNames.PICO_CONTRACT); - } - - @Override - public void doInner(TypeElement type, - Void ignored) { - // NOP (disable base processing) - } - -} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/CreatorHandler.java b/pico/processor/src/main/java/io/helidon/pico/processor/CreatorHandler.java new file mode 100644 index 00000000000..c69fbc010e8 --- /dev/null +++ b/pico/processor/src/main/java/io/helidon/pico/processor/CreatorHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor; + +import java.util.Objects; + +import javax.annotation.processing.ProcessingEnvironment; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.tools.AbstractFilerMessager; +import io.helidon.pico.tools.ActivatorCreatorProvider; +import io.helidon.pico.tools.ActivatorCreatorRequest; +import io.helidon.pico.tools.ActivatorCreatorResponse; +import io.helidon.pico.tools.CodeGenFiler; +import io.helidon.pico.tools.CodeGenInterceptorRequest; +import io.helidon.pico.tools.InterceptorCreatorResponse; +import io.helidon.pico.tools.Messager; +import io.helidon.pico.tools.spi.ActivatorCreator; + +/** + * Provides wrapping of the {@link ActivatorCreator}}. + */ +class CreatorHandler implements ActivatorCreator { + private final String name; + private final CodeGenFiler filer; + private final Messager messager; + + CreatorHandler(String name, + ProcessingEnvironment processingEnv, + Messager messager) { + this.name = Objects.requireNonNull(name); + this.filer = CodeGenFiler.create(AbstractFilerMessager.createAnnotationBasedFiler(processingEnv, messager)); + this.messager = Objects.requireNonNull(messager); + } + + // note: overrides ActivatorCreator + @Override + public ActivatorCreatorResponse createModuleActivators(ActivatorCreatorRequest request) { + messager.debug(name + ": createModuleActivators: " + request); + return ActivatorCreatorProvider.instance().createModuleActivators(request); + } + + // note: overrides ActivatorCreator + @Override + public InterceptorCreatorResponse codegenInterceptors(CodeGenInterceptorRequest request) { + messager.debug(name + ": codegenInterceptors(): " + request); + return ActivatorCreatorProvider.instance().codegenInterceptors(request); + } + + // note: overrides ActivatorCreator + @Override + public TypeName toActivatorImplTypeName(TypeName activatorTypeName) { + return ActivatorCreatorProvider.instance() + .toActivatorImplTypeName(activatorTypeName); + } + + CodeGenFiler filer() { + return filer; + } + +} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java index db9ff86edc4..065c353e1e9 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java @@ -35,7 +35,6 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; -import io.helidon.builder.processor.spi.TypeInfoCreatorProvider; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; @@ -43,8 +42,6 @@ import io.helidon.common.types.TypedElementName; import io.helidon.pico.api.ServiceInfoBasics; import io.helidon.pico.tools.AbstractFilerMessager; -import io.helidon.pico.tools.ActivatorCreatorCodeGen; -import io.helidon.pico.tools.ActivatorCreatorDefault; import io.helidon.pico.tools.CodeGenFiler; import io.helidon.pico.tools.CustomAnnotationTemplateRequest; import io.helidon.pico.tools.CustomAnnotationTemplateRequestDefault; @@ -53,6 +50,9 @@ import io.helidon.pico.tools.ToolsException; import io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator; +import static io.helidon.pico.processor.ActiveProcessorUtils.MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; +import static io.helidon.pico.processor.GeneralProcessorUtils.hasValue; +import static io.helidon.pico.processor.GeneralProcessorUtils.rootStackTraceElementOf; import static io.helidon.pico.tools.TypeTools.createTypeNameFromElement; import static io.helidon.pico.tools.TypeTools.createTypedElementNameFromElement; import static io.helidon.pico.tools.TypeTools.isStatic; @@ -62,7 +62,7 @@ /** * Processor for all {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator}'s. */ -public class CustomAnnotationProcessor extends BaseAnnotationProcessor { +public class CustomAnnotationProcessor extends BaseAnnotationProcessor { private static final Map> PRODUCERS_BY_ANNOTATION = new ConcurrentHashMap<>(); private static final Set ALL_ANNO_TYPES_HANDLED = new CopyOnWriteArraySet<>(); private static final List PRODUCERS = initialize(); @@ -114,29 +114,29 @@ public boolean process(Set annotations, RoundEnvironment roundEnv) { try { if (!roundEnv.processingOver()) { - for (String annoType : annoTypes()) { + for (String annoType : getSupportedAnnotationTypes()) { TypeName annoName = TypeNameDefault.createFromTypeName(annoType); Optional annoElement = toTypeElement(annoName); if (annoElement.isEmpty()) { continue; } Set typesToProcess = roundEnv.getElementsAnnotatedWith(annoElement.get()); - doInner(annoName, typesToProcess, roundEnv); + doInner(annoName, typesToProcess); } } return MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; } catch (Throwable t) { - error(getClass().getSimpleName() + " error during processing; " + t + " @ " - + ProcessorUtils.rootStackTraceElementOf(t), t); + utils().error(getClass().getSimpleName() + " error during processing; " + t + " @ " + + rootStackTraceElementOf(t), t); // we typically will not even get to this next line since the messager.error() call will trigger things to halt throw new ToolsException("Error while processing: " + t + " @ " - + ProcessorUtils.rootStackTraceElementOf(t), t); + + rootStackTraceElementOf(t), t); } } @Override - protected Set annoTypes() { + public Set getSupportedAnnotationTypes() { return Set.copyOf(ALL_ANNO_TYPES_HANDLED); } @@ -146,8 +146,7 @@ Set producersForType(TypeName annoTypeName) { } void doInner(TypeName annoTypeName, - Set typesToProcess, - RoundEnvironment roundEnv) { + Set typesToProcess) { if (typesToProcess.isEmpty()) { return; } @@ -167,7 +166,7 @@ void doInner(TypeName annoTypeName, CustomAnnotationTemplateResponseDefault.Builder res = CustomAnnotationTemplateResponseDefault.builder() .request(req); for (CustomAnnotationTemplateCreator producer : producers) { - req.genericTemplateCreator(new DefaultGenericTemplateCreator(producer.getClass(), this)); + req.genericTemplateCreator(new GenericTemplateCreatorDefault(producer.getClass(), utils())); CustomAnnotationTemplateResponse producerResponse = process(producer, req); if (producerResponse != null) { res = CustomAnnotationTemplateResponse.aggregate(req, res, producerResponse); @@ -178,13 +177,13 @@ void doInner(TypeName annoTypeName, doFiler(response); } } catch (Throwable t) { - throw new ToolsException("Error detected while processing: " + typesToProcess + t, t); + throw new ToolsException("Error while processing: " + typesToProcess + t, t); } } } void doFiler(CustomAnnotationTemplateResponse response) { - AbstractFilerMessager filer = AbstractFilerMessager.createAnnotationBasedFiler(processingEnv, this); + AbstractFilerMessager filer = AbstractFilerMessager.createAnnotationBasedFiler(processingEnv, utils()); CodeGenFiler codegen = CodeGenFiler.create(filer); response.generatedSourceCode().forEach(codegen::codegenJavaFilerOut); response.generatedResources().forEach((typedElementName, resourceBody) -> { @@ -192,8 +191,7 @@ void doFiler(CustomAnnotationTemplateResponse response) { if (!hasValue(fileType)) { fileType = ".generated"; } - codegen.codegenResourceFilerOut(toFilePath(typedElementName.typeName(), fileType), - resourceBody, Optional.empty()); + codegen.codegenResourceFilerOut(toFilePath(typedElementName.typeName(), fileType), resourceBody); }); } @@ -209,13 +207,13 @@ CustomAnnotationTemplateResponse process(CustomAnnotationTemplateCreator produce res.generatedSourceCode().entrySet().forEach(entry -> { TypeNameDefault.ensureIsFQN(entry.getKey()); if (!hasValue(entry.getValue())) { - throw new ToolsException("expected to have valid code for: " + req + " for " + entry); + throw new ToolsException("Expected to have valid code for: " + req + " for " + entry); } }); } return res; } catch (Throwable t) { - throw new ToolsException("failed in producer: " + producer + "; " + t, t); + throw new ToolsException("Failed in producer: " + producer + "; " + t, t); } } @@ -226,38 +224,27 @@ CustomAnnotationTemplateRequestDefault.Builder toRequestBuilder(TypeName annoTyp if (enclosingClassTypeName == null) { return null; } - ServiceInfoBasics siInfo = toBasicServiceInfo(enclosingClassTypeName); + + TypeInfo enclosingClassTypeInfo = utils() + .toTypeInfo(enclosingClassType, enclosingClassType.asType(), (typedElement) -> true) + .orElseThrow(); + ServiceInfoBasics siInfo = GeneralProcessorUtils.toBasicServiceInfo(enclosingClassTypeInfo); if (siInfo == null) { return null; } - TypeInfoCreatorProvider tools = HelidonServiceLoader.create( - ServiceLoader.load(TypeInfoCreatorProvider.class, TypeInfoCreatorProvider.class.getClassLoader())) - .asList() - .stream() - .findFirst() - .orElseThrow(); - TypeInfo enclosingClassTypeInfo = tools - .createTypeInfo(enclosingClassType, enclosingClassType.asType(), processingEnv, (typedMethod) -> true) - .orElseThrow(); + Elements elements = processingEnv.getElementUtils(); return CustomAnnotationTemplateRequestDefault.builder() .filerEnabled(true) .annoTypeName(annoTypeName) .serviceInfo(siInfo) .targetElement(createTypedElementNameFromElement(typeToProcess, elements).orElseThrow()) + .enclosingTypeInfo(enclosingClassTypeInfo) + // the following are duplicates that should be removed - get them from the enclosingTypeInfo instead + // see https://github.com/helidon-io/helidon/issues/6773 .targetElementArgs(toArgs(typeToProcess)) .targetElementAccess(toAccess(typeToProcess)) - .elementStatic(isStatic(typeToProcess)) - .enclosingTypeInfo(enclosingClassTypeInfo); - } - - ServiceInfoBasics toBasicServiceInfo(TypeName enclosingClassType) { - ActivatorCreatorCodeGen codeGen = - ActivatorCreatorDefault.createActivatorCreatorCodeGen(servicesToProcess()).orElse(null); - if (codeGen == null) { - return null; - } - return ActivatorCreatorDefault.toServiceInfo(enclosingClassType, codeGen); + .elementStatic(isStatic(typeToProcess)); } List toArgs(Element typeToProcess) { @@ -273,7 +260,7 @@ List toArgs(Element typeToProcess) { return result; } - TypeElement toEnclosingClassTypeElement(Element typeToProcess) { + private static TypeElement toEnclosingClassTypeElement(Element typeToProcess) { while (typeToProcess != null && !(typeToProcess instanceof TypeElement)) { typeToProcess = typeToProcess.getEnclosingElement(); } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java b/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java new file mode 100644 index 00000000000..cbcd4b77600 --- /dev/null +++ b/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor; + +import java.lang.annotation.Annotation; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.helidon.common.Weight; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.AnnotationAndValueDefault; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.api.QualifierAndValue; +import io.helidon.pico.api.QualifierAndValueDefault; +import io.helidon.pico.api.RunLevel; +import io.helidon.pico.api.ServiceInfoBasics; +import io.helidon.pico.api.ServiceInfoDefault; +import io.helidon.pico.tools.Options; +import io.helidon.pico.tools.TypeNames; +import io.helidon.pico.tools.TypeTools; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Qualifier; +import jakarta.inject.Singleton; + +/** + * Carries static methods that are agnostic to the active processing environment. + * + * @see ActiveProcessorUtils + */ +final class GeneralProcessorUtils { + private GeneralProcessorUtils() { + } + + /** + * Determines the root throwable stack trace element from a chain of throwable causes. + * + * @param t the throwable + * @return the root throwable error stack trace element + */ + static StackTraceElement rootStackTraceElementOf(Throwable t) { + while (t.getCause() != null && t.getCause() != t) { + t = t.getCause(); + } + return t.getStackTrace()[0]; + } + + /** + * Converts a collection to a comma delimited string. + * + * @param coll the collection + * @return the concatenated, delimited string value + */ + static String toString(Collection coll) { + return toString(coll, null, null); + } + + /** + * Provides specialization in concatenation, allowing for a function to be called for each element as well as to + * use special separators. + * + * @param coll the collection + * @param fnc the optional function to translate the collection item to a string + * @param separator the optional separator + * @param the type held by the collection + * @return the concatenated, delimited string value + */ + static String toString(Collection coll, + Function fnc, + String separator) { + Function fn = (fnc == null) ? String::valueOf : fnc; + separator = (separator == null) ? ", " : separator; + return coll.stream().map(fn).collect(Collectors.joining(separator)); + } + + /** + * Splits given using a comma-delimiter, and returns a trimmed list of string for each item. + * + * @param str the string to split + * @return the list of string values + */ + static List toList(String str) { + return toList(str, ","); + } + + /** + * Splits a string given a delimiter, and returns a trimmed list of string for each item. + * + * @param str the string to split + * @param delim the delimiter + * @return the list of string values + */ + static List toList(String str, + String delim) { + String[] split = str.split(delim); + return Arrays.stream(split).map(String::trim).collect(Collectors.toList()); + } + + /** + * Will return non-empty File if the uri represents a local file on the fs. + * + * @param uri the uri of the artifact + * @return the file instance, or empty if not local + */ + static Optional toPath(URI uri) { + if (uri.getHost() != null) { + return Optional.empty(); + } + return Optional.of(Paths.get(uri)); + } + + /** + * Attempts to resolve the {@link RunLevel} value assigned to the provided service. + * + * @param service the service + * @return the declared run level if available + */ + static Optional toRunLevel(TypeInfo service) { + AnnotationAndValue runLevelAnno = + AnnotationAndValueDefault.findFirst(RunLevel.class, service.annotations()).orElse(null); + if (runLevelAnno != null) { + return Optional.of(Integer.valueOf(runLevelAnno.value().orElseThrow())); + } + + // RunLevel is not inheritable - we will therefore not search up the hierarchy +// if (service.superTypeInfo().isPresent()) { +// return toRunLevel(service.superTypeInfo().get()); +// } + + return Optional.empty(); + } + + /** + * Attempts to resolve the {@link Weight} value assigned to the provided service. + * + * @param service the service + * @return the declared weight if available + */ + static Optional toWeight(TypeInfo service) { + AnnotationAndValue weightAnno = + AnnotationAndValueDefault.findFirst(Weight.class, service.annotations()).orElse(null); + if (weightAnno != null) { + return Optional.of(Double.valueOf(weightAnno.value().orElseThrow())); + } + + // Weight is not inheritable - we will therefore not search up the hierarchy +// if (service.superTypeInfo().isPresent()) { +// return toWeight(service.superTypeInfo().get()); +// } + + return Optional.empty(); + } + + /** + * Attempts to resolve the {@link PostConstruct} method name assigned to the provided service. + * + * @param service the service + * @return the post construct method if available + */ + static Optional toPostConstructMethod(TypeInfo service) { + List postConstructs = service.elementInfo().stream() + .filter(it -> { + AnnotationAndValue anno = findFirst(PostConstruct.class, it.annotations()).orElse(null); + return (anno != null); + }) + .map(TypedElementName::elementName) + .toList(); + if (postConstructs.size() == 1) { + return Optional.of(postConstructs.get(0)); + } else if (postConstructs.size() > 1) { + throw new IllegalStateException("There can be at most one " + + PostConstruct.class.getName() + + " annotated method per type: " + service.typeName()); + } + + // PostConstruct is not inheritable - we will therefore not search up the hierarchy +// if (service.superTypeInfo().isPresent()) { +// return toPostConstructMethod(service.superTypeInfo().get()); +// } + + return Optional.empty(); + } + + /** + * Attempts to resolve the {@link PreDestroy} method name assigned to the provided service. + * + * @param service the service + * @return the pre destroy method if available + */ + static Optional toPreDestroyMethod(TypeInfo service) { + List preDestroys = service.elementInfo().stream() + .filter(it -> { + AnnotationAndValue anno = findFirst(PreDestroy.class, it.annotations()).orElse(null); + return (anno != null); + }) + .map(TypedElementName::elementName) + .toList(); + if (preDestroys.size() == 1) { + return Optional.of(preDestroys.get(0)); + } else if (preDestroys.size() > 1) { + throw new IllegalStateException("There can be at most one " + + PreDestroy.class.getName() + + " annotated method per type: " + service.typeName()); + } + + // PreDestroy is not inheritable - we will therefore not search up the hierarchy +// if (service.superTypeInfo().isPresent()) { +// return toPreDestroyMethod(service.superTypeInfo().get()); +// } + + return Optional.empty(); + } + + /** + * Attempts to resolve the scope names of the provided service. + * + * @param service the service + * @return the set of declared scope names + */ + static Set toScopeNames(TypeInfo service) { + Set scopeAnnotations = new LinkedHashSet<>(); + + service.referencedTypeNamesToAnnotations() + .forEach((typeName, listOfAnnotations) -> { + if (listOfAnnotations.stream() + .map(it -> it.typeName().name()) + .anyMatch(it -> it.equals(TypeNames.JAKARTA_SCOPE))) { + scopeAnnotations.add(typeName.name()); + } + }); + + if (Options.isOptionEnabled(Options.TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE)) { + boolean hasApplicationScope = findFirst(TypeNames.JAKARTA_APPLICATION_SCOPED, service.annotations()).isPresent(); + if (hasApplicationScope) { + scopeAnnotations.add(Singleton.class.getName()); + scopeAnnotations.add(TypeNames.JAKARTA_APPLICATION_SCOPED); + } + } + + return scopeAnnotations; + } + + /** + * Returns the type hierarchy of the provided service type info. + * + * @param service the service + * @return the type hierarchy + */ + static List toServiceTypeHierarchy(TypeInfo service) { + List result = new ArrayList<>(); + result.add(service.typeName()); + service.superTypeInfo().ifPresent(it -> result.addAll(toServiceTypeHierarchy(it))); + return result; + } + + /** + * Returns the qualifiers assigned to the provided service type info. + * + * @param service the service + * @return the qualifiers of the service + */ + static Set toQualifiers(TypeInfo service) { + Set result = new LinkedHashSet<>(); + + for (AnnotationAndValue anno : service.annotations()) { + List metaAnnotations = service.referencedTypeNamesToAnnotations().get(anno.typeName()); + Optional qual = findFirst(Qualifier.class, metaAnnotations); + if (qual.isPresent()) { + result.add(QualifierAndValueDefault.convert(anno)); + } + } + + // note: should qualifiers be inheritable? Right now we assume not to support the jsr-330 spec. + // service.superTypeInfo().ifPresent(it -> result.addAll(toQualifiers(it))); + // service.interfaceTypeInfo().forEach(it -> result.addAll(toQualifiers(it))); + + return result; + } + + /** + * Returns the qualifiers assigned to the provided typed element belonging to the associated service. + * + * @param element the typed element (e.g., field, method, or constructor) + * @param service the service for which the typed element belongs + * @return the qualifiers associated with the provided element + */ + static Set toQualifiers(TypedElementName element, + TypeInfo service) { + Set result = new LinkedHashSet<>(); + + for (AnnotationAndValue anno : element.annotations()) { + List metaAnnotations = service.referencedTypeNamesToAnnotations().get(anno.typeName()); + Optional qual = (metaAnnotations == null) + ? Optional.empty() : findFirst(Qualifier.class, metaAnnotations); + if (qual.isPresent()) { + result.add(QualifierAndValueDefault.convert(anno)); + } + } + + // note: should qualifiers be inheritable? Right now we assume not to support the jsr-330 spec (see note above). + + return result; + } + + /** + * Returns true if the provided type name is a {@code Provider<>} type. + * + * @param typeName the type name to check + * @return true if the provided type is a provider type. + */ + static boolean isProviderType(TypeName typeName) { + String name = typeName.name(); + return (name.equals(TypeNames.JAKARTA_PROVIDER) + || name.equals(TypeNames.JAVAX_PROVIDER) + || name.equals(TypeNames.PICO_INJECTION_POINT_PROVIDER)); + } + + /** + * Simple check to see the passed String value is non-null and non-blank. + * + * @param val the value to check + * @return true if non-null and non-blank + */ + static boolean hasValue(String val) { + return (val != null && !val.isBlank()); + } + + /** + * Looks for either a jakarta or javax annotation. + * + * @param jakartaAnno the jakarta annotation class type + * @param annotations the set of annotations to look in + * @return the optional annotation if there is a match + */ + static Optional findFirst(Class jakartaAnno, + Collection annotations) { + return findFirst(jakartaAnno.getName(), annotations); + } + + static Optional findFirst(String jakartaAnnoName, + Collection annotations) { + if (annotations == null) { + return Optional.empty(); + } + + Optional anno = AnnotationAndValueDefault.findFirst(jakartaAnnoName, annotations); + if (anno.isPresent()) { + return anno; + } + + return AnnotationAndValueDefault.findFirst(TypeTools.oppositeOf(jakartaAnnoName), annotations); + } + + static ServiceInfoBasics toBasicServiceInfo(TypeInfo service) { + return ServiceInfoDefault.builder() + .serviceTypeName(service.typeName().name()) + .declaredWeight(toWeight(service)) + .declaredRunLevel(toRunLevel(service)) + .scopeTypeNames(toScopeNames(service)) + .build(); + } + +} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/DefaultGenericTemplateCreator.java b/pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java similarity index 94% rename from pico/processor/src/main/java/io/helidon/pico/processor/DefaultGenericTemplateCreator.java rename to pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java index e9ec5c2eef1..56300256648 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/DefaultGenericTemplateCreator.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java @@ -35,7 +35,7 @@ /** * Default implementation for {@link GenericTemplateCreator}. */ -class DefaultGenericTemplateCreator implements GenericTemplateCreator { +class GenericTemplateCreatorDefault implements GenericTemplateCreator { private final Class generator; private final Messager messager; @@ -44,8 +44,8 @@ class DefaultGenericTemplateCreator implements GenericTemplateCreator { * * @param generator the class type for the generator */ - DefaultGenericTemplateCreator(Class generator) { - this(generator, new MessagerToLogAdapter(System.getLogger(DefaultGenericTemplateCreator.class.getName()))); + GenericTemplateCreatorDefault(Class generator) { + this(generator, new MessagerToLogAdapter(System.getLogger(GenericTemplateCreatorDefault.class.getName()))); } /** @@ -54,7 +54,7 @@ class DefaultGenericTemplateCreator implements GenericTemplateCreator { * @param generator the class type for the generator * @param messager the messager and error handler */ - DefaultGenericTemplateCreator(Class generator, + GenericTemplateCreatorDefault(Class generator, Messager messager) { this.generator = Objects.requireNonNull(generator); this.messager = messager; @@ -121,7 +121,7 @@ Map gatherSubstitutions(GenericTemplateCreatorRequest genericReq substitutions.put("elementAnnotations", req.targetElement().annotations()); substitutions.put("elementEnclosingTypeName", req.targetElement().typeName()); substitutions.put("elementArgs", req.targetElementArgs()); - substitutions.put("elementArgs-declaration", ProcessorUtils.toString(req.targetElementArgs())); + substitutions.put("elementArgs-declaration", GeneralProcessorUtils.toString(req.targetElementArgs())); substitutions.putAll(genericRequest.overrideProperties()); return substitutions; } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/InjectAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/InjectAnnotationProcessor.java deleted file mode 100644 index fb1d6604c84..00000000000 --- a/pico/processor/src/main/java/io/helidon/pico/processor/InjectAnnotationProcessor.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.pico.processor; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; - -import io.helidon.common.types.TypeName; -import io.helidon.pico.api.DependenciesInfo; -import io.helidon.pico.api.DependencyInfo; -import io.helidon.pico.api.InjectionPointInfo; -import io.helidon.pico.api.QualifierAndValue; -import io.helidon.pico.runtime.Dependencies; -import io.helidon.pico.tools.ToolsException; -import io.helidon.pico.tools.TypeNames; -import io.helidon.pico.tools.TypeTools; - -import static io.helidon.pico.tools.TypeTools.createQualifierAndValueSet; -import static io.helidon.pico.tools.TypeTools.createTypeNameFromElement; -import static io.helidon.pico.tools.TypeTools.isStatic; -import static io.helidon.pico.tools.TypeTools.toAccess; - -/** - * Handles {@code @Inject} annotations on fields and methods. - */ -public class InjectAnnotationProcessor extends BaseAnnotationProcessor { - private static final Set SUPPORTED_TARGETS = Set.of(TypeNames.JAKARTA_INJECT, - TypeNames.JAVAX_INJECT); - - /** - * Service loader based constructor. - * - * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly - */ - @Deprecated - public InjectAnnotationProcessor() { - } - - @Override - protected Set annoTypes() { - return SUPPORTED_TARGETS; - } - - @Override - protected Set contraAnnotations() { - return Set.of(TypeNames.PICO_CONFIGURED_BY); - } - - @Override - int doBulkInner(Set typesToProcess, - TypeName typeName, - Dependencies.BuilderContinuation builder) { - if (typesToProcess.isEmpty()) { - return 0; - } - - assert (typeName == null); - assert (builder == null); - - Set serviceTypeNames = typesToProcess.stream() - .map(element -> createTypeNameFromElement(element.getEnclosingElement()).orElseThrow()) - .collect(Collectors.toSet()); - assert (!serviceTypeNames.isEmpty()); - for (TypeName serviceTypeName : serviceTypeNames) { - builder = Dependencies.builder(serviceTypeName.name()); - int ctorCount = super.doBulkInner(typesToProcess, serviceTypeName, builder); - if (ctorCount > 1) { - throw new ToolsException("There can be max of 1 injectable constructor for " + serviceTypeName); - } - DependenciesInfo dependencies = builder.build(); - servicesToProcess().addDependencies(dependencies); - maybeSetBasicsForServiceType(serviceTypeName, null); - } - - return 0; - } - - @Override - void doInner(String serviceTypeName, - VariableElement varType, - Dependencies.BuilderContinuation continuation, - String elemName, - int elemArgs, - Integer elemOffset, - InjectionPointInfo.ElementKind elemKind, - InjectionPointInfo.Access access, - Boolean isStaticAlready) { - boolean isStatic = (isStaticAlready != null && isStaticAlready); - if (access == null) { - if (varType.getKind() != ElementKind.FIELD) { - throw new ToolsException("Unsupported element kind " + varType.getEnclosingElement() - + "." + varType + " with " + varType.getKind()); - } - - access = toAccess(varType); - elemKind = InjectionPointInfo.ElementKind.FIELD; - isStatic = isStatic(varType); - } - - serviceTypeName = (serviceTypeName != null) ? serviceTypeName : varType.getEnclosingElement().toString(); - elemName = (elemName != null) ? elemName : varType.getSimpleName().toString(); - - AtomicReference isProvider = new AtomicReference<>(); - AtomicReference isOptional = new AtomicReference<>(); - AtomicReference isList = new AtomicReference<>(); - String varTypeName = TypeTools.extractInjectionPointTypeInfo(varType, isProvider, isList, isOptional); - Set qualifiers = createQualifierAndValueSet(varType.getAnnotationMirrors()); - - assert (elemKind != null && elemName != null); - continuation = continuation.add(serviceTypeName, elemName, varTypeName, elemKind, elemArgs, access) - .elemOffset(elemOffset) - .qualifiers(qualifiers) - .providerWrapped(isProvider.get()) - .listWrapped(isList.get()) - .optionalWrapped(isOptional.get()) - .staticDeclaration(isStatic); - DependencyInfo ipInfo = continuation.commitLastDependency().orElseThrow(); - debug("dependency for " + varType + " was " + ipInfo); - } - - @Override - void doInner(ExecutableElement method, - Dependencies.BuilderContinuation builder) { - if (method.getKind() != ElementKind.METHOD - && method.getKind() != ElementKind.CONSTRUCTOR) { - throw new ToolsException("Unsupported element kind " + method.getEnclosingElement() - + "." + method + " with " + method.getKind()); - } - - final InjectionPointInfo.Access access = toAccess(method); - final boolean isStatic = isStatic(method); - - List params = method.getParameters(); - if (params.isEmpty() && method.getKind() != ElementKind.CONSTRUCTOR) { - throw new ToolsException("Unsupported element kind " + method.getEnclosingElement() - + "." + method + " with " + method.getKind()); - } - - String serviceTypeName = method.getEnclosingElement().toString(); - String methodTypeName = (ElementKind.CONSTRUCTOR == method.getKind()) - ? InjectionPointInfo.CONSTRUCTOR - : method.getSimpleName().toString(); - InjectionPointInfo.ElementKind elemKind = (ElementKind.CONSTRUCTOR == method.getKind()) - ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD; - if (!params.isEmpty()) { - int elemOffset = 0; - for (VariableElement varType : params) { - doInner(serviceTypeName, varType, builder, methodTypeName, - params.size(), ++elemOffset, elemKind, access, isStatic); - } - } - } - - @Override - public void doInner(TypeElement type, - Dependencies.BuilderContinuation builder) { - throw new IllegalStateException(); // should never be here - } - -} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java new file mode 100644 index 00000000000..bed22ff111d --- /dev/null +++ b/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java @@ -0,0 +1,731 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.AnnotationAndValueDefault; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.api.Activator; +import io.helidon.pico.api.Contract; +import io.helidon.pico.api.DependenciesInfo; +import io.helidon.pico.api.ElementInfo; +import io.helidon.pico.api.ExternalContracts; +import io.helidon.pico.api.PicoServicesConfig; +import io.helidon.pico.api.QualifierAndValue; +import io.helidon.pico.api.ServiceInfoBasics; +import io.helidon.pico.runtime.Dependencies; +import io.helidon.pico.tools.ActivatorCreatorCodeGen; +import io.helidon.pico.tools.ActivatorCreatorConfigOptionsDefault; +import io.helidon.pico.tools.ActivatorCreatorDefault; +import io.helidon.pico.tools.ActivatorCreatorRequest; +import io.helidon.pico.tools.ActivatorCreatorResponse; +import io.helidon.pico.tools.InterceptionPlan; +import io.helidon.pico.tools.InterceptorCreatorProvider; +import io.helidon.pico.tools.Options; +import io.helidon.pico.tools.ServicesToProcess; +import io.helidon.pico.tools.ToolsException; +import io.helidon.pico.tools.TypeNames; +import io.helidon.pico.tools.spi.ActivatorCreator; +import io.helidon.pico.tools.spi.InterceptorCreator; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; + +import static io.helidon.builder.processor.tools.BuilderTypeTools.createTypeNameFromElement; +import static io.helidon.common.types.TypeNameDefault.createFromTypeName; +import static io.helidon.pico.processor.ActiveProcessorUtils.MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; +import static io.helidon.pico.processor.GeneralProcessorUtils.isProviderType; +import static io.helidon.pico.processor.GeneralProcessorUtils.rootStackTraceElementOf; +import static io.helidon.pico.processor.GeneralProcessorUtils.toBasicServiceInfo; +import static io.helidon.pico.processor.GeneralProcessorUtils.toPostConstructMethod; +import static io.helidon.pico.processor.GeneralProcessorUtils.toPreDestroyMethod; +import static io.helidon.pico.processor.GeneralProcessorUtils.toQualifiers; +import static io.helidon.pico.processor.GeneralProcessorUtils.toRunLevel; +import static io.helidon.pico.processor.GeneralProcessorUtils.toScopeNames; +import static io.helidon.pico.processor.GeneralProcessorUtils.toServiceTypeHierarchy; +import static io.helidon.pico.processor.GeneralProcessorUtils.toWeight; +import static io.helidon.pico.tools.TypeTools.createTypedElementNameFromElement; +import static io.helidon.pico.tools.TypeTools.toAccess; +import static java.util.Objects.requireNonNull; + +/** + * An annotation processor that will find everything needing to be processed related to core Pico conde generation. + */ +public class PicoAnnotationProcessor extends BaseAnnotationProcessor { + private static boolean disableBaseProcessing; + + private static final Set SUPPORTED_SERVICE_CLASS_TARGET_ANNOTATIONS = Set.of( + TypeNames.JAKARTA_SINGLETON, + TypeNames.JAVAX_SINGLETON, + TypeNames.JAKARTA_APPLICATION_SCOPED, + TypeNames.JAVAX_APPLICATION_SCOPED, + TypeNames.PICO_EXTERNAL_CONTRACTS, + TypeNames.PICO_INTERCEPTED); + + private static final Set SUPPORTED_CONTRACT_CLASS_TARGET_ANNOTATIONS = Set.of( + TypeNames.PICO_CONTRACT); + + private static final Set SUPPORTED_ELEMENT_TARGET_ANNOTATIONS = Set.of( + TypeNames.JAKARTA_INJECT, + TypeNames.JAVAX_INJECT, + TypeNames.JAKARTA_PRE_DESTROY, + TypeNames.JAKARTA_POST_CONSTRUCT, + TypeNames.JAVAX_PRE_DESTROY, + TypeNames.JAVAX_POST_CONSTRUCT); + + private final Set allElementsOfInterestInThisModule = new LinkedHashSet<>(); + private final Map typeInfoToCreateActivatorsForInThisModule = new LinkedHashMap<>(); + private CreatorHandler creator; + private boolean autoAddInterfaces; + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public PicoAnnotationProcessor() { + } + + /** + * Any overriding APT processor can optionally pass {@code false} in order to prevent duplicate base processing. + * + * @param disableBaseProcessing set to true to disable base processing + */ + protected PicoAnnotationProcessor(boolean disableBaseProcessing) { + if (disableBaseProcessing) { + PicoAnnotationProcessor.disableBaseProcessing = true; + } + } + + @Override + public Set getSupportedAnnotationTypes() { + return Stream.of(supportedServiceClassTargetAnnotations(), + supportedContractClassTargetAnnotations(), + supportedElementTargetAnnotations()) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + } + + @Override + public void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.autoAddInterfaces = Options.isOptionEnabled(Options.TAG_AUTO_ADD_NON_CONTRACT_INTERFACES); + this.creator = new CreatorHandler(getClass().getSimpleName(), processingEnv, utils()); +// if (BaseAnnotationProcessor.ENABLED) { +// // we are is simulation mode when the base one is operating... +// this.creator.activateSimulationMode(); +// } + } + + @Override + public boolean process(Set annotations, + RoundEnvironment roundEnv) { + utils().roundEnv(roundEnv); + + if (disableBaseProcessing && getClass() == PicoAnnotationProcessor.class) { + return false; + } + + ServicesToProcess.onBeginProcessing(utils(), getSupportedAnnotationTypes(), roundEnv); +// ServicesToProcess.addOnDoneRunnable(CreatorHandler.reporting()); + + try { + // build the model + Set elementsOfInterestInThisRound = gatherElementsOfInterestInThisModule(); + validate(elementsOfInterestInThisRound); + allElementsOfInterestInThisModule.addAll(elementsOfInterestInThisRound); + + // cumulatively collect the types to process in the module + gatherTypeInfosToProcessInThisModule(typeInfoToCreateActivatorsForInThisModule, allElementsOfInterestInThisModule); + + // optionally intercept and validate the model + Set filtered = interceptorAndValidate(typeInfoToCreateActivatorsForInThisModule.values()); + + // code generate the model + if (!filtered.isEmpty()) { + ServicesToProcess services = toServicesToProcess(filtered, allElementsOfInterestInThisModule); + doFiler(services); + } + + return MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; + } catch (Throwable t) { + ToolsException exc = new ToolsException("Error while processing: " + t + + " @ " + rootStackTraceElementOf(t) + + " in " + getClass().getSimpleName(), t); + utils().error(exc.getMessage(), t); + // we typically will not even get to this next line since the messager.error() call above will trigger things to halt + throw exc; + } finally { + ServicesToProcess.onEndProcessing(utils(), getSupportedAnnotationTypes(), roundEnv); + if (roundEnv.processingOver()) { + allElementsOfInterestInThisModule.clear(); + typeInfoToCreateActivatorsForInThisModule.clear(); + } + utils().roundEnv(null); + } + } + + /** + * Returns the activator creator in use. + * + * @return the activator creator in use + */ + protected ActivatorCreator activatorCreator() { + return creator; + } + + /** + * The annotation types we handle that will trigger activator creation. + * + * @return annotation types we handle on services + */ + protected Set supportedServiceClassTargetAnnotations() { + return SUPPORTED_SERVICE_CLASS_TARGET_ANNOTATIONS; + } + + /** + * The annotation types we handle that will be advertised as contracts. + * + * @return annotation types we handle on contracts + */ + protected Set supportedContractClassTargetAnnotations() { + return SUPPORTED_CONTRACT_CLASS_TARGET_ANNOTATIONS; + } + + /** + * The annotation types we expect to see on method and field type elements. + * + * @return annotation types we handle on elements + */ + protected Set supportedElementTargetAnnotations() { + return SUPPORTED_ELEMENT_TARGET_ANNOTATIONS; + } + + /** + * Code generate these {@link io.helidon.pico.api.Activator}'s ad {@link io.helidon.pico.api.ModuleComponent}'s. + * + * @param services the services to code generate + */ + protected void doFiler(ServicesToProcess services) { + ActivatorCreatorCodeGen codeGen = ActivatorCreatorDefault.createActivatorCreatorCodeGen(services).orElse(null); + if (codeGen == null) { + return; + } + + boolean processingOver = utils().roundEnv().processingOver(); + ActivatorCreatorConfigOptionsDefault configOptions = ActivatorCreatorConfigOptionsDefault.builder() + .applicationPreCreated(Options.isOptionEnabled(Options.TAG_APPLICATION_PRE_CREATE)) + .moduleCreated(processingOver) + .build(); + ActivatorCreatorRequest req = ActivatorCreatorDefault + .createActivatorCreatorRequest(services, codeGen, configOptions, creator.filer(), false); + ActivatorCreatorResponse res = creator.createModuleActivators(req); + if (!res.success()) { + ToolsException exc = new ToolsException("Error during codegen", res.error().orElse(null)); + utils().error(exc.getMessage(), exc); + // should not get here since the error above should halt further processing + throw exc; + } + } + + /** + * These are all of the elements (methods, constructors, methods) that are "interesting" (i.e., has {@code @Inject}, etc.). + * + * @param elementsOfInterest the elements that are eligible for some form of Pico processing + */ + protected void validate(Collection elementsOfInterest) { + validatePerClass(elementsOfInterest, + "There can be max of one injectable constructor per class", + 1, + (it) -> it.elementTypeKind().equals(TypeInfo.KIND_CONSTRUCTOR) + && GeneralProcessorUtils.findFirst(Inject.class, it.annotations()).isPresent()); + validatePerClass(elementsOfInterest, + "There can be max of one PostConstruct method per class", + 1, + (it) -> it.elementTypeKind().equals(TypeInfo.KIND_METHOD) + && GeneralProcessorUtils.findFirst(PostConstruct.class, it.annotations()).isPresent()); + validatePerClass(elementsOfInterest, + "There can be max of one PreDestroy method per class", + 1, + (it) -> it.elementTypeKind().equals(TypeInfo.KIND_METHOD) + && GeneralProcessorUtils.findFirst(PreDestroy.class, it.annotations()).isPresent()); + validatePerClass(elementsOfInterest, + PicoServicesConfig.NAME + " does not currently support static or private elements", + 0, + (it) -> toModifierNames(it.modifierNames()).contains(TypeInfo.MODIFIER_PRIVATE) + || toModifierNames(it.modifierNames()).contains(TypeInfo.MODIFIER_STATIC)); + } + + private void validatePerClass(Collection elementsOfInterest, + String msg, + int maxAllowed, + Predicate matcher) { + Map> allTypeNamesToMatchingElements = new LinkedHashMap<>(); + elementsOfInterest.stream() + .filter(matcher) + .forEach(it -> allTypeNamesToMatchingElements + .computeIfAbsent(it.enclosingTypeName().orElseThrow(), (n) -> new ArrayList<>()).add(it)); + allTypeNamesToMatchingElements.values().stream() + .filter(list -> list.size() > maxAllowed) + .forEach(it -> utils().error(msg + " for " + it.get(0).enclosingTypeName(), null)); + } + + /** + * Provides a means for anyone to validate and intercept the collection of types to process. + * + * @param typesToCreateActivatorsFor the map of types to process (where key is the proposed generated name) + * @return the (possibly revised) set of types to process + */ + protected Set interceptorAndValidate(Collection typesToCreateActivatorsFor) { + return new LinkedHashSet<>(Objects.requireNonNull(typesToCreateActivatorsFor)); + } + + /** + * Called to process a single service that will eventually be code generated. The default implementation will take the + * provided service {@link TypeInfo} and translate that into the {@link ServicesToProcess} instance. Eventually, the + * {@link ServicesToProcess} instance will be fed as request inputs to one or more of the creators (e.g., + * {@link io.helidon.pico.tools.spi.ActivatorCreator}, {@link io.helidon.pico.tools.spi.InterceptorCreator}, etc.). + * + * @param services the services to process builder + * @param service the service type info to process right now + * @param serviceTypeNamesToCodeGenerate the entire set of types that are planned to be code-generated + * @param allElementsOfInterest all of the elements of interest that pico "knows" about + */ + protected void process(ServicesToProcess services, + TypeInfo service, + Set serviceTypeNamesToCodeGenerate, + Collection allElementsOfInterest) { + utils().debug("Code generating" + Activator.class.getSimpleName() + " for: " + service.typeName()); + processBasics(services, service, serviceTypeNamesToCodeGenerate, allElementsOfInterest); + processInterceptors(services, service, serviceTypeNamesToCodeGenerate, allElementsOfInterest); + processExtensions(services, service, serviceTypeNamesToCodeGenerate, allElementsOfInterest); + } + + /** + * Processes the basic Pico service type - its contracts, run level, weight, dependencies, etc. + * + * @param services the services to process builder + * @param service the service type info to process right now + * @param serviceTypeNamesToCodeGenerate the entire set of types that are planned to be code-generated + * @param allElementsOfInterest all of the elements of interest that pico "knows" about + */ + @SuppressWarnings("unused") + protected void processBasics(ServicesToProcess services, + TypeInfo service, + Set serviceTypeNamesToCodeGenerate, + Collection allElementsOfInterest) { + TypeName serviceTypeName = service.typeName(); + TypeInfo superTypeInfo = service.superTypeInfo().orElse(null); + if (superTypeInfo != null) { + TypeName superTypeName = superTypeInfo.typeName(); + services.addParentServiceType(serviceTypeName, superTypeName); + } + Set modifierNames = toModifierNames(service.modifierNames()); + + toRunLevel(service).ifPresent(it -> services.addDeclaredRunLevel(serviceTypeName, it)); + toWeight(service).ifPresent(it -> services.addDeclaredWeight(serviceTypeName, it)); + toScopeNames(service).forEach(it -> services.addScopeTypeName(serviceTypeName, it)); + toPostConstructMethod(service).ifPresent(it -> services.addPostConstructMethod(serviceTypeName, it)); + toPreDestroyMethod(service).ifPresent(it -> services.addPreDestroyMethod(serviceTypeName, it)); + toInjectionDependencies(service, allElementsOfInterest).ifPresent(services::addDependencies); + services.addAccessLevel(serviceTypeName, + toAccess(modifierNames)); + services.addIsAbstract(serviceTypeName, + modifierNames.contains(TypeInfo.MODIFIER_ABSTRACT)); + services.addServiceTypeHierarchy(serviceTypeName, + toServiceTypeHierarchy(service)); + services.addQualifiers(serviceTypeName, + toQualifiers(service)); + gatherContractsIntoServicesToProcess(services, service); + } + + /** + * Process any interception plans. + * + * @param services the services to process builder + * @param service the service type info to process right now + * @param serviceTypeNamesToCodeGenerate the entire set of types that are planned to be code-generated + * @param allElementsOfInterest all of the elements of interest that pico "knows" about + */ + @SuppressWarnings("unused") + private void processInterceptors(ServicesToProcess services, + TypeInfo service, + Set serviceTypeNamesToCodeGenerate, + Collection allElementsOfInterest) { + TypeName serviceTypeName = service.typeName(); + InterceptorCreator interceptorCreator = InterceptorCreatorProvider.instance(); + ServiceInfoBasics interceptedServiceInfo = toBasicServiceInfo(service); + InterceptorCreator.InterceptorProcessor processor = interceptorCreator.createInterceptorProcessor( + interceptedServiceInfo, + interceptorCreator, + Optional.of(processingEnv)); + Set annotationTypeTriggers = processor.allAnnotationTypeTriggers(); + if (annotationTypeTriggers.isEmpty()) { + services.addInterceptorPlanFor(serviceTypeName, Optional.empty()); + return; + } + + Optional plan = processor.createInterceptorPlan(annotationTypeTriggers); + if (plan.isEmpty()) { + utils().log("unable to produce an interception plan for: " + serviceTypeName); + } + services.addInterceptorPlanFor(serviceTypeName, plan); + } + + /** + * Process any extensions (e.g., config-driven) requiring extra processing or any modifications to {@link ServicesToProcess}. + * + * @param services the services to process builder + * @param service the service type info to process right now + * @param serviceTypeNamesToCodeGenerate the entire set of types that are planned to be code-generated + * @param allElementsOfInterest all of the elements of interest that pico "knows" about + */ + @SuppressWarnings("unused") + protected void processExtensions(ServicesToProcess services, + TypeInfo service, + Set serviceTypeNamesToCodeGenerate, + Collection allElementsOfInterest) { + // NOP; expected that derived classes will implement this + } + + /** + * Finds the first jakarta or javax annotation matching the given jakarta annotation class name. + * + * @param jakartaAnnoName the jakarta annotation class name + * @param annotations all of the annotations to search through + * @return the annotation, or empty if not found + */ + protected Optional findFirst(String jakartaAnnoName, + Collection annotations) { + return GeneralProcessorUtils.findFirst(jakartaAnnoName, annotations); + } + + private ServicesToProcess toServicesToProcess(Set typesToCodeGenerate, + Collection allElementsOfInterest) { + ServicesToProcess services = ServicesToProcess.create(); + utils().relayModuleInfoToServicesToProcess(services); + + Set typesNamesToCodeGenerate = typesToCodeGenerate.stream().map(TypeInfo::typeName).collect(Collectors.toSet()); + typesToCodeGenerate.forEach(service -> { + try { + process(services, service, typesNamesToCodeGenerate, allElementsOfInterest); + } catch (Throwable t) { + throw new ToolsException("Error while processing: " + service.typeName(), t); + } + }); + + return services; + } + + private void gatherContractsIntoServicesToProcess(ServicesToProcess services, + TypeInfo service) { + Set contracts = new LinkedHashSet<>(); + Set externalContracts = new LinkedHashSet<>(); + Set providerForSet = new LinkedHashSet<>(); + Set externalModuleNames = new LinkedHashSet<>(); + + gatherContracts(contracts, + externalContracts, + providerForSet, + externalModuleNames, + service, + false); + + TypeName serviceTypeName = service.typeName(); + contracts.forEach(it -> services.addTypeForContract(serviceTypeName, it, false)); + externalContracts.forEach(it -> services.addTypeForContract(serviceTypeName, it, true)); + services.addProviderFor(serviceTypeName, providerForSet); + services.addExternalRequiredModules(serviceTypeName, externalModuleNames); + + utils().debug(serviceTypeName + + ": contracts=" + contracts + + ", providers=" + providerForSet + + ", externalContracts=" + externalContracts + + ", externalModuleNames=" + externalModuleNames); + } + + private void gatherContracts(Set contracts, + Set externalContracts, + Set providerForSet, + Set externalModuleNamesRequired, + TypeInfo typeInfo, + boolean isThisTypeEligibleToBeAContract) { + TypeName fqTypeName = typeInfo.typeName(); + TypeName fqProviderTypeName = null; + if (isProviderType(fqTypeName)) { + fqProviderTypeName = fqTypeName.genericTypeName(); + fqTypeName = requireNonNull(fqTypeName.typeArguments().get(0), fqTypeName.toString()); + } + TypeName genericTypeName = fqTypeName.genericTypeName(); + + if (isThisTypeEligibleToBeAContract && !genericTypeName.wildcard()) { + if (fqProviderTypeName != null) { + if (!genericTypeName.generic()) { + providerForSet.add(genericTypeName); + + Optional moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName)); + moduleName.ifPresent(externalModuleNamesRequired::add); + if (moduleName.isPresent()) { + externalContracts.add(genericTypeName); + } else { + contracts.add(genericTypeName); + } + } + + // if we are dealing with a Provider<> then we should add those too as module dependencies + TypeName genericProviderTypeName = fqProviderTypeName.genericTypeName(); + externalContracts.add(genericProviderTypeName); + filterModuleName(typeInfo.moduleNameOf(genericProviderTypeName)).ifPresent(externalModuleNamesRequired::add); + if (genericProviderTypeName.name().equals(TypeNames.PICO_INJECTION_POINT_PROVIDER)) { + TypeName jakartaProviderTypeName = createFromTypeName(TypeNames.JAKARTA_PROVIDER); + externalContracts.add(jakartaProviderTypeName); + filterModuleName(typeInfo.moduleNameOf(jakartaProviderTypeName)).ifPresent(externalModuleNamesRequired::add); + } + } else { + boolean isTypeAnInterface = typeInfo.typeKind().equals(TypeInfo.KIND_INTERFACE); + boolean isTypeAContract = autoAddInterfaces + || !isTypeAnInterface + || AnnotationAndValueDefault.findFirst(Contract.class, typeInfo.annotations()).isPresent(); + if (isTypeAContract) { + Optional moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName)); + moduleName.ifPresent(externalModuleNamesRequired::add); + if (moduleName.isPresent()) { + externalContracts.add(genericTypeName); + } else { + contracts.add(genericTypeName); + } + } + } + } + + AnnotationAndValue externalContractAnno = AnnotationAndValueDefault + .findFirst(ExternalContracts.class, typeInfo.annotations()) + .orElse(null); + if (externalContractAnno != null) { + String[] externalContractNames = externalContractAnno.value("value").orElse("").split(",[ \t]*"); + for (String externalContractName : externalContractNames) { + TypeName externalContractTypeName = createFromTypeName(externalContractName); + externalContracts.add(externalContractTypeName); + filterModuleName(typeInfo.moduleNameOf(externalContractTypeName)).ifPresent(externalModuleNamesRequired::add); + } + + String[] moduleNames = externalContractAnno.value("moduleNames").orElse("").split(",[ \t]*"); + for (String externalModuleName : moduleNames) { + if (!externalModuleName.isBlank()) { + externalModuleNamesRequired.add(externalModuleName); + } + } + } + + // process parent hierarchy + typeInfo.superTypeInfo().ifPresent(it -> gatherContracts(contracts, + externalContracts, + providerForSet, + externalModuleNamesRequired, + it, + true)); + typeInfo.interfaceTypeInfo().forEach(it -> gatherContracts(contracts, + externalContracts, + providerForSet, + externalModuleNamesRequired, + it, + true)); + } + + private Optional filterModuleName(Optional moduleName) { + String name = moduleName.orElse(null); + if (name != null && (name.startsWith("java.") || name.startsWith("jdk"))) { + return Optional.empty(); + } + return moduleName; + } + + private Optional toInjectionDependencies(TypeInfo service, + Collection allElementsOfInterest) { + Dependencies.BuilderContinuation builder = Dependencies.builder(service.typeName().name()); + gatherInjectionPoints(builder, service, allElementsOfInterest); + DependenciesInfo deps = builder.build(); + return deps.serviceInfoDependencies().isEmpty() ? Optional.empty() : Optional.of(deps); + } + + private void gatherInjectionPoints(Dependencies.BuilderContinuation builder, + TypeInfo service, + Collection allElementsOfInterest) { + List injectableElementsForThisService = allElementsOfInterest.stream() + .filter(it -> GeneralProcessorUtils.findFirst(Inject.class, it.annotations()).isPresent()) + .filter(it -> service.typeName().equals(it.enclosingTypeName().orElseThrow())) + .toList(); + injectableElementsForThisService + .forEach(elem -> gatherInjectionPoints(builder, elem, service, toModifierNames(elem.modifierNames()))); + +// // We expect activators at every level for abstract bases - we will therefore NOT recursive up the hierarchy +// service.superTypeInfo().ifPresent(it -> gatherInjectionPoints(builder, it, allElementsOfInterest, false)); + } + + /** + * Processes all of the injection points for the provided typed element, accumulating the result in the provided builder + * continuation instance. + * + * @param builder the builder continuation instance + * @param typedElement the typed element to convert + * @param service the type info of the backing service + */ + private static void gatherInjectionPoints(Dependencies.BuilderContinuation builder, + TypedElementName typedElement, + TypeInfo service, + Set modifierNames) { + String elemName = typedElement.elementName(); + ElementInfo.Access access = toAccess(modifierNames); + ElementInfo.ElementKind elemKind = ElementInfo.ElementKind.valueOf(typedElement.elementTypeKind()); + boolean isField = (elemKind == ElementInfo.ElementKind.FIELD); + if (isField) { + TypeName typeName = typedElement.typeName(); + boolean isOptional = typeName.isOptional(); + typeName = (isOptional) ? typeName.typeArguments().get(0) : typeName; + boolean isList = typeName.isList(); + typeName = (isList) ? typeName.typeArguments().get(0) : typeName; + boolean isProviderType = isProviderType(typeName); + typeName = (isProviderType) ? typeName.typeArguments().get(0) : typeName; + Set qualifiers = toQualifiers(typedElement, service); + + builder.add(service.typeName().name(), + elemName, + typeName.name(), + elemKind, + 0, + access) + .qualifiers(qualifiers) + .listWrapped(isList) + .providerWrapped(isProviderType) + .optionalWrapped(isOptional); + } else { + int elemArgs = typedElement.parameterArguments().size(); + AtomicInteger elemOffset = new AtomicInteger(); + typedElement.parameterArguments().forEach(it -> { + TypeName typeName = it.typeName(); + boolean isOptional = typeName.isOptional(); + typeName = (isOptional) ? typeName.typeArguments().get(0) : typeName; + boolean isList = typeName.isList(); + typeName = (isList) ? typeName.typeArguments().get(0) : typeName; + boolean isProviderType = isProviderType(typeName); + typeName = (isProviderType) ? typeName.typeArguments().get(0) : typeName; + + int pos = elemOffset.incrementAndGet(); + Set qualifiers = toQualifiers(it, service); + + builder.add(service.typeName().name(), + elemName, + typeName.name(), + elemKind, + elemArgs, + access) + .qualifiers(qualifiers) + .elemOffset(pos) + .listWrapped(isList) + .providerWrapped(isProviderType) + .optionalWrapped(isOptional); + }); + } + } + + private Set gatherElementsOfInterestInThisModule() { + Set result = new LinkedHashSet<>(); + + Elements elementUtils = processingEnv.getElementUtils(); + for (String annoType : supportedElementTargetAnnotations()) { + // annotation may not be on the classpath, in such a case just ignore it + TypeElement annoTypeElement = elementUtils.getTypeElement(annoType); + if (annoTypeElement != null) { + Set typesToProcess = utils().roundEnv().getElementsAnnotatedWith(annoTypeElement); + typesToProcess.forEach(it -> result.add(createTypedElementNameFromElement(it, elementUtils).orElseThrow())); + } + } + + return result; + } + + private void gatherTypeInfosToProcessInThisModule(Map result, + Collection elementsOfInterest) { + // this section gathers based upon the class-level annotations in order to discover what to process + for (String annoType : supportedServiceClassTargetAnnotations()) { + // annotation may not be on the classpath, in such a case just ignore it + Elements elements = processingEnv.getElementUtils(); + TypeElement annoTypeElement = elements.getTypeElement(annoType); + if (annoTypeElement != null) { + Set typesToProcess = utils().roundEnv().getElementsAnnotatedWith(annoTypeElement); + typesToProcess.forEach(it -> { + TypeName typeName = createTypeNameFromElement(it).orElseThrow().genericTypeName(); + if (!result.containsKey(typeName)) { + // first time processing this type name + TypeElement typeElement = (TypeElement) it; + Optional typeInfo = + utils().toTypeInfo(typeElement, typeElement.asType(), elementsOfInterest::contains); + typeInfo.ifPresent(it2 -> result.put(typeName, it2)); + } + }); + } + } + + // this section gathers based upon the element-level annotations in order to discover what to process + Set enclosingElementsOfInterest = elementsOfInterest.stream() + .map(TypedElementName::enclosingTypeName) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + enclosingElementsOfInterest.removeAll(result.keySet()); + + Elements elementUtils = processingEnv.getElementUtils(); + enclosingElementsOfInterest.forEach(it -> { + TypeName typeName = it.genericTypeName(); + if (!result.containsKey(typeName)) { + TypeElement element = requireNonNull(elementUtils.getTypeElement(it.name()), it.name()); + result.put(creator.toActivatorImplTypeName(typeName), + utils().toTypeInfo(element, element.asType(), elementsOfInterest::contains).orElseThrow()); + } + }); + } + + // will be resolved in https://github.com/helidon-io/helidon/issues/6764 + private static Set toModifierNames(Set names) { + return names.stream().map(String::toUpperCase).collect(Collectors.toSet()); + } + +} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/PostConstructPreDestroyAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/PostConstructPreDestroyAnnotationProcessor.java deleted file mode 100644 index eb48b48ef0f..00000000000 --- a/pico/processor/src/main/java/io/helidon/pico/processor/PostConstructPreDestroyAnnotationProcessor.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.pico.processor; - -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; - -import io.helidon.pico.api.InjectionPointInfo; -import io.helidon.pico.tools.ToolsException; -import io.helidon.pico.tools.TypeNames; -import io.helidon.pico.tools.TypeTools; - -import static io.helidon.builder.processor.tools.BuilderTypeTools.findAnnotationMirror; -import static io.helidon.pico.tools.TypeTools.createTypeNameFromElement; - -/** - * Handling for {@link jakarta.annotation.PostConstruct} and {@link jakarta.annotation.PreDestroy}. - */ -public class PostConstructPreDestroyAnnotationProcessor extends BaseAnnotationProcessor { - - private static final Set SUPPORTED_TARGETS = Set.of(TypeNames.JAKARTA_PRE_DESTROY, - TypeNames.JAKARTA_POST_CONSTRUCT, - TypeNames.JAVAX_PRE_DESTROY, - TypeNames.JAVAX_POST_CONSTRUCT); - - /** - * Service loader based constructor. - * - * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly - */ - @Deprecated - public PostConstructPreDestroyAnnotationProcessor() { - } - - @Override - protected Set annoTypes() { - return SUPPORTED_TARGETS; - } - - @Override - protected Set contraAnnotations() { - return Set.of(TypeNames.PICO_CONFIGURED_BY); - } - - @Override - void doInner(ExecutableElement method, - Void builder) { - Element parentClassType = method.getEnclosingElement(); - if (TypeTools.isAbstract(parentClassType)) { - // skipping abstract classes w/ a PostConstruct or PreDestroy annotation - warn("All PostConstruct and PreDestroy methods will be ignored on abstract base classes: " + parentClassType); - return; - } - - if (method.getKind() == ElementKind.CONSTRUCTOR) { - throw new ToolsException("Invalid use of PreDestroy/PostConstruct on " + method.getEnclosingElement() + "." + method); - } - - boolean isStatic = false; - InjectionPointInfo.Access access = InjectionPointInfo.Access.PACKAGE_PRIVATE; - Set modifiers = method.getModifiers(); - if (modifiers != null) { - for (Modifier modifier : modifiers) { - if (Modifier.PUBLIC == modifier) { - access = InjectionPointInfo.Access.PUBLIC; - } else if (Modifier.PROTECTED == modifier) { - access = InjectionPointInfo.Access.PROTECTED; - } else if (Modifier.PRIVATE == modifier) { - access = InjectionPointInfo.Access.PRIVATE; - } else if (Modifier.STATIC == modifier) { - isStatic = true; - } - } - } - - if (isStatic || InjectionPointInfo.Access.PRIVATE == access) { - throw new ToolsException("Invalid use of a private and/or static PreDestroy/PostConstruct method on " - + method.getEnclosingElement() + "." + method); - } - - if (!method.getParameters().isEmpty()) { - throw new ToolsException("Invalid use PreDestroy/PostConstruct method w/ parameters on " - + method.getEnclosingElement() + "." + method); - } - - List annotations = method.getAnnotationMirrors(); - - /* - * Either Jakarta or javax pre-destroy - */ - Optional mirror = findAnnotationMirror(TypeNames.JAKARTA_PRE_DESTROY, annotations); - if (mirror.isPresent()) { - servicesToProcess().addPreDestroyMethod(createTypeNameFromElement(method.getEnclosingElement()).orElseThrow(), - method.getSimpleName().toString()); - } else { - mirror = findAnnotationMirror(TypeNames.JAVAX_PRE_DESTROY, annotations); - if (mirror.isPresent()) { - servicesToProcess().addPreDestroyMethod(createTypeNameFromElement(method.getEnclosingElement()).orElseThrow(), - method.getSimpleName().toString()); - } - } - - /* - * Either Jakarta or javax post-construct - */ - mirror = findAnnotationMirror(TypeNames.JAKARTA_POST_CONSTRUCT, annotations); - if (mirror.isPresent()) { - servicesToProcess().addPostConstructMethod(createTypeNameFromElement(method.getEnclosingElement()).orElseThrow(), - method.getSimpleName().toString()); - } else { - mirror = findAnnotationMirror(TypeNames.JAVAX_POST_CONSTRUCT, annotations); - if (mirror.isPresent()) { - servicesToProcess().addPostConstructMethod(createTypeNameFromElement(method.getEnclosingElement()).orElseThrow(), - method.getSimpleName().toString()); - } - } - } - -} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ProcessorUtils.java b/pico/processor/src/main/java/io/helidon/pico/processor/ProcessorUtils.java deleted file mode 100644 index 3d327cb3220..00000000000 --- a/pico/processor/src/main/java/io/helidon/pico/processor/ProcessorUtils.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.pico.processor; - -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - -final class ProcessorUtils { - private ProcessorUtils() { - } - - /** - * Determines the root throwable stack trace element from a chain of throwable causes. - * - * @param t the throwable - * @return the root throwable error stack trace element - */ - static StackTraceElement rootStackTraceElementOf(Throwable t) { - while (t.getCause() != null && t.getCause() != t) { - t = t.getCause(); - } - return t.getStackTrace()[0]; - } - - /** - * Converts a collection to a comma delimited string. - * - * @param coll the collection - * @return the concatenated, delimited string value - */ - static String toString(Collection coll) { - return toString(coll, null, null); - } - - /** - * Provides specialization in concatenation, allowing for a function to be called for each element as well as to - * use special separators. - * - * @param coll the collection - * @param fnc the optional function to translate the collection item to a string - * @param separator the optional separator - * @param the type held by the collection - * @return the concatenated, delimited string value - */ - static String toString(Collection coll, - Function fnc, - String separator) { - Function fn = (fnc == null) ? String::valueOf : fnc; - separator = (separator == null) ? ", " : separator; - return coll.stream().map(fn).collect(Collectors.joining(separator)); - } - - /** - * Splits given using a comma-delimiter, and returns a trimmed list of string for each item. - * - * @param str the string to split - * @return the list of string values - */ - static List toList(String str) { - return toList(str, ","); - } - - /** - * Splits a string given a delimiter, and returns a trimmed list of string for each item. - * - * @param str the string to split - * @param delim the delimiter - * @return the list of string values - */ - static List toList(String str, - String delim) { - String[] split = str.split(delim); - return Arrays.stream(split).map(String::trim).collect(Collectors.toList()); - } - - /** - * Will return non-empty File if the uri represents a local file on the fs. - * - * @param uri the uri of the artifact - * @return the file instance, or empty if not local - */ - static Optional toPath(URI uri) { - if (uri.getHost() != null) { - return Optional.empty(); - } - return Optional.of(Paths.get(uri)); - } - -} diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ServiceAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/ServiceAnnotationProcessor.java deleted file mode 100644 index 4aafc1feaee..00000000000 --- a/pico/processor/src/main/java/io/helidon/pico/processor/ServiceAnnotationProcessor.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.pico.processor; - -import java.io.File; -import java.net.URI; -import java.nio.file.Path; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - -import javax.annotation.processing.Filer; -import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.element.TypeElement; -import javax.tools.FileObject; -import javax.tools.StandardLocation; - -import io.helidon.pico.tools.ModuleInfoDescriptor; -import io.helidon.pico.tools.Options; -import io.helidon.pico.tools.ToolsException; -import io.helidon.pico.tools.TypeNames; - -import static io.helidon.pico.processor.ProcessorUtils.rootStackTraceElementOf; -import static io.helidon.pico.processor.ProcessorUtils.toPath; -import static io.helidon.pico.tools.ModuleUtils.REAL_MODULE_INFO_JAVA_NAME; -import static io.helidon.pico.tools.ModuleUtils.inferSourceOrTest; - -/** - * Processor for @{@link jakarta.inject.Singleton} type annotations. - */ -public class ServiceAnnotationProcessor extends BaseAnnotationProcessor { - - private static final Set SUPPORTED_TARGETS = Set.of( - TypeNames.JAKARTA_SINGLETON, - TypeNames.PICO_EXTERNAL_CONTRACTS, - TypeNames.PICO_INTERCEPTED, - TypeNames.JAVAX_SINGLETON, - TypeNames.JAKARTA_APPLICATION_SCOPED, - TypeNames.JAVAX_APPLICATION_SCOPED - ); - - /** - * Service loader based constructor. - * - * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly - */ - @Deprecated - public ServiceAnnotationProcessor() { - } - - @Override - protected Set annoTypes() { - return SUPPORTED_TARGETS; - } - - @Override - protected Set contraAnnotations() { - return Set.of(TypeNames.PICO_CONFIGURED_BY); - } - - @Override - public boolean process(Set annotations, - RoundEnvironment roundEnv) { - try { - if (!roundEnv.processingOver() - && (servicesToProcess().moduleName() == null)) { - // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here - AtomicReference typeSuffix = new AtomicReference<>(); - AtomicReference moduleInfoFile = new AtomicReference<>(); - AtomicReference srcPath = new AtomicReference<>(); - ModuleInfoDescriptor thisModuleDescriptor = - getThisModuleDescriptor(typeSuffix, moduleInfoFile, srcPath); - if (thisModuleDescriptor != null) { - servicesToProcess().lastKnownModuleInfoDescriptor(thisModuleDescriptor); - } else { - String thisModuleName = getThisModuleName(null); - if (thisModuleName == null) { - servicesToProcess().clearModuleName(); - } else { - servicesToProcess().moduleName(thisModuleName); - } - } - if (typeSuffix.get() != null) { - servicesToProcess().lastKnownTypeSuffix(typeSuffix.get()); - } - if (srcPath.get() != null) { - servicesToProcess().lastKnownSourcePathBeingProcessed(srcPath.get().toPath()); - } - if (moduleInfoFile.get() != null) { - servicesToProcess().lastKnownModuleInfoFilePath(moduleInfoFile.get().toPath()); - } - } - - return super.process(annotations, roundEnv); - } catch (Throwable t) { - error(getClass().getSimpleName() + " error during processing; " + t - + " @ " + rootStackTraceElementOf(t), t); - // we typically will not even get to this next line since the messager.error() call will trigger things to halt - throw new ToolsException("Error detected while processing: " + t - + " @ " + rootStackTraceElementOf(t), t); - } finally { - if (roundEnv.processingOver()) { - servicesToProcess().clearModuleName(); - } - } - } - - String getThisModuleName(ModuleInfoDescriptor descriptor) { - // user pass it in? if so then that will short-circuit the name calculation part - String moduleName = Options.getOption(Options.TAG_MODULE_NAME).orElse(null); - if (moduleName != null) { - return moduleName; - } - return (descriptor == null) ? null : descriptor.name(); - } - - // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here - ModuleInfoDescriptor getThisModuleDescriptor(AtomicReference typeSuffix, - AtomicReference moduleInfoFile, - AtomicReference srcPath) { - return tryFindModuleInfo(StandardLocation.SOURCE_OUTPUT, typeSuffix, moduleInfoFile, srcPath) - .or(() -> tryFindModuleInfo(StandardLocation.SOURCE_PATH, typeSuffix, moduleInfoFile, srcPath)) - // attempt to retrieve from src/main/java if we can't recover to this point - .or(() -> getThisModuleDescriptorFromSourceMain(moduleInfoFile, srcPath)) - .orElse(null); - } - - // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here - Optional getThisModuleDescriptorFromSourceMain(AtomicReference moduleInfoFile, - AtomicReference srcPath) { - if (srcPath != null && srcPath.get() != null && srcPath.get().getPath().contains(TARGET_DIR)) { - String path = srcPath.get().getPath(); - int pos = path.indexOf(TARGET_DIR); - path = path.substring(0, pos); - File srcRoot = new File(path, SRC_MAIN_JAVA_DIR); - File file = new File(srcRoot, REAL_MODULE_INFO_JAVA_NAME); - if (file.exists()) { - try { - return Optional.of( - ModuleInfoDescriptor.create(file.toPath(), ModuleInfoDescriptor.Ordering.NATURAL_PRESERVE_COMMENTS)); - } catch (Exception e) { - debug("unable to read source module-info.java from: " + srcRoot + "; " + e.getMessage(), e); - } - - if (moduleInfoFile != null) { - moduleInfoFile.set(file); - } - } - } - - debug("unable to find module-info.java from: " + srcPath); - return Optional.empty(); - } - - // note: Atomic here is merely a convenience as a pass-by-reference holder, no async is actually needed here - Optional tryFindModuleInfo(StandardLocation location, - AtomicReference typeSuffix, - AtomicReference moduleInfoFile, - AtomicReference srcPath) { - Filer filer = processingEnv.getFiler(); - try { - FileObject f = filer.getResource(location, "", REAL_MODULE_INFO_JAVA_NAME); - URI uri = f.toUri(); - Path filePath = toPath(uri).orElse(null); - if (filePath != null) { - Path parent = filePath.getParent(); - if (srcPath != null) { - srcPath.set(parent.toFile()); - } - if (typeSuffix != null) { - String type = inferSourceOrTest(parent); - typeSuffix.set(type); - } - if (filePath.toFile().exists()) { - if (moduleInfoFile != null) { - moduleInfoFile.set(filePath.toFile()); - } - return Optional.of(ModuleInfoDescriptor.create(filePath)); - } - } - } catch (Exception e) { - debug("unable to retrieve " + location + " from filer: " + e.getMessage()); - } - - return Optional.empty(); - } - -} diff --git a/pico/processor/src/main/java/module-info.java b/pico/processor/src/main/java/module-info.java index 0d94b4381f2..b9fd9f2393f 100644 --- a/pico/processor/src/main/java/module-info.java +++ b/pico/processor/src/main/java/module-info.java @@ -22,7 +22,6 @@ requires static jakarta.annotation; requires static jdk.jfr; - requires jdk.compiler; requires java.compiler; requires io.helidon.common; @@ -38,10 +37,7 @@ uses io.helidon.builder.processor.spi.TypeInfoCreatorProvider; provides javax.annotation.processing.Processor with - io.helidon.pico.processor.ContractAnnotationProcessor, - io.helidon.pico.processor.InjectAnnotationProcessor, - io.helidon.pico.processor.PostConstructPreDestroyAnnotationProcessor, - io.helidon.pico.processor.ServiceAnnotationProcessor, io.helidon.pico.processor.CustomAnnotationProcessor, - io.helidon.pico.processor.UnsupportedConstructsProcessor; + io.helidon.pico.processor.UnsupportedConstructsProcessor, + io.helidon.pico.processor.PicoAnnotationProcessor; } diff --git a/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java b/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java index fbbb5e6d12f..e8e61db4413 100644 --- a/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java +++ b/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java @@ -55,7 +55,7 @@ class CustomAnnotationProcessorTest { @SuppressWarnings("unchecked") void annotationSupported() { CustomAnnotationProcessor processor = new CustomAnnotationProcessor(); - assertThat(processor.annoTypes(), + assertThat(processor.getSupportedAnnotationTypes(), containsInAnyOrder(ExtensibleGET.class.getName())); } @@ -78,8 +78,8 @@ void extensibleGET() { .elementName("header") .build(); ServiceInfoBasics serviceInfo = ServiceInfoDefault.builder(); - DefaultGenericTemplateCreator genericTemplateCreator = - new DefaultGenericTemplateCreator(ExtensibleGetTemplateProducer.class); + GenericTemplateCreatorDefault genericTemplateCreator = + new GenericTemplateCreatorDefault(ExtensibleGetTemplateProducer.class); CustomAnnotationTemplateRequest req = CustomAnnotationTemplateRequestDefault.builder() .annoTypeName(create(ExtensibleGET.class)) .serviceInfo(serviceInfo) diff --git a/pico/processor/src/test/java/io/helidon/pico/processor/ExtensibleGetTemplateProducer.java b/pico/processor/src/test/java/io/helidon/pico/processor/ExtensibleGetTemplateProducer.java index 8d713335067..39ee4b001d8 100644 --- a/pico/processor/src/test/java/io/helidon/pico/processor/ExtensibleGetTemplateProducer.java +++ b/pico/processor/src/test/java/io/helidon/pico/processor/ExtensibleGetTemplateProducer.java @@ -57,7 +57,7 @@ public Optional create(CustomAnnotationTemplat + request.annoTypeName().className() + "_" + request.targetElement().elementName(); TypeName generatedTypeName = TypeNameDefault.create(enclosingTypeInfo.typeName().packageName(), classname); - DefaultGenericTemplateCreator genericTemplateCreator = new DefaultGenericTemplateCreator(getClass()); + GenericTemplateCreatorDefault genericTemplateCreator = new GenericTemplateCreatorDefault(getClass()); CharSequence template = genericTemplateCreator.supplyFromResources("nima", "extensible-get.hbs"); GenericTemplateCreatorRequest genericCreatorRequest = GenericTemplateCreatorRequestDefault.builder() .customAnnotationTemplateRequest(request) diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java index c005315620f..97f77fa8c54 100644 --- a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java @@ -66,7 +66,7 @@ void setBladeList(List blades) { } @Inject - void setBladeProviders(List> blades) { + public void setBladeProviders(List> blades) { setterInjectedPkgPrivateProviderListInAbstractBase = blades; setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount++; } diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java index 172e5f47ebb..061bacabe74 100644 --- a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java @@ -31,19 +31,21 @@ import jakarta.inject.Provider; import jakarta.inject.Singleton; +@SuppressWarnings("unused") @Singleton public class MainToolBox implements ToolBox { private final List> allTools; private final List> allHammers; private final Provider bigHammer; + private final Screwdriver screwdriver; + + private Provider setPreferredHammer; @Inject @Preferred Provider preferredHammer; - private final Screwdriver screwdriver; - public int postConstructCallCount; public int preDestroyCallCount; public int setterCallCount; @@ -65,6 +67,11 @@ void setScrewdriver(Screwdriver screwdriver) { setterCallCount++; } + @Inject + void setPreferredHammer(@Preferred Provider hammer) { + this.setPreferredHammer = hammer; + } + @Override public List> toolsInBox() { return allTools; @@ -90,6 +97,7 @@ public Screwdriver screwdriver() { @PostConstruct void postConstruct() { postConstructCallCount++; + assert (preferredHammer == setPreferredHammer) : preferredHammer + " and " + setPreferredHammer; } @PreDestroy diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java index 3c2348aff3d..5cb5f2aa486 100644 --- a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java @@ -64,7 +64,6 @@ * Expectation here is that the annotation processor ran, and we can use standard injection and pico-di registry services, etc. */ class ToolBoxTest { - Config config = PicoTestingSupport.basicTestableConfig(); PicoServices picoServices; Services services; diff --git a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ index 20f40237e22..6aef3a52f72 100644 --- a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ +++ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ @@ -1,4 +1,4 @@ -// @Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") +// @Generated(value = "io.helidon.pico.tools.ModuleInfoDescriptorDefault", comments = "version=1") module io.helidon.pico.tests.pico/test { exports io.helidon.pico.tests.pico; // pico module - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") @@ -8,6 +8,7 @@ module io.helidon.pico.tests.pico/test { // pico external contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") uses io.helidon.pico.api.Resettable; uses io.helidon.pico.tests.pico.stacking.Intercepted; + uses io.helidon.pico.tests.pico.stacking.InterceptedImpl; // pico services - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") requires transitive io.helidon.pico.runtime; } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java b/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java index 301b872cf10..7cb22515218 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java @@ -70,6 +70,10 @@ public abstract class AbstractFilerMessager implements Filer, Messager { this.logger = logger; } + System.Logger logger() { + return logger; + } + /** * Create an annotation based filer abstraction. * @@ -206,11 +210,15 @@ static class DirectFilerMessager extends AbstractFilerMessager { this.paths = paths; } + CodeGenPaths codeGenPaths() { + return paths; + } + @Override public FileObject getResource(JavaFileManager.Location location, CharSequence moduleAndPkg, CharSequence relativeName) throws IOException { - return getResource(location, moduleAndPkg, relativeName, true); + return getResource(location, moduleAndPkg, relativeName, false); } private FileObject getResource(JavaFileManager.Location location, diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java index 20048b4e9f9..f2fc6069626 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java @@ -136,7 +136,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req, LazyValue scan) { boolean isApplicationPreCreated = req.configOptions().isApplicationPreCreated(); boolean isModuleCreated = req.configOptions().isModuleCreated(); - CodeGenPaths codeGenPaths = req.codeGenPaths(); + CodeGenPaths codeGenPaths = req.codeGenPaths().orElse(null); Map serviceTypeToIsAbstractType = req.codeGen().serviceTypeIsAbstractTypes(); List activatorTypeNames = new ArrayList<>(); List activatorTypeNamesPutInModule = new ArrayList<>(); @@ -222,7 +222,10 @@ private ModuleDetail toModuleDetail(ActivatorCreatorRequest req, Map> serviceTypeContracts = codeGen.serviceTypeContracts(); Map> externalContracts = codeGen.serviceTypeExternalContracts(); - Optional moduleInfoPath = req.codeGenPaths().moduleInfoPath(); + Optional moduleInfoPath = Optional.empty(); + if (req.codeGenPaths().isPresent()) { + moduleInfoPath = req.codeGenPaths().get().moduleInfoPath(); + } ModuleInfoCreatorRequest moduleCreatorRequest = ModuleInfoCreatorRequestDefault.builder() .name(moduleName) .moduleTypeName(moduleTypeName) @@ -293,14 +296,14 @@ void codegenMetaInfServices(GeneralCreatorRequest req, Map> metaInfServices) { boolean prev = true; if (req.analysisOnly()) { - prev = CodeGenFiler.filerEnabled(false); + prev = CodeGenFiler.filerWriterEnabled(false); } try { req.filer().codegenMetaInfServices(paths, metaInfServices); } finally { if (req.analysisOnly()) { - CodeGenFiler.filerEnabled(prev); + CodeGenFiler.filerWriterEnabled(prev); } } } @@ -309,14 +312,14 @@ void codegenActivatorFilerOut(GeneralCreatorRequest req, ActivatorCodeGenDetail activatorDetail) { boolean prev = true; if (req.analysisOnly()) { - prev = CodeGenFiler.filerEnabled(false); + prev = CodeGenFiler.filerWriterEnabled(false); } try { req.filer().codegenActivatorFilerOut(activatorDetail); } finally { if (req.analysisOnly()) { - CodeGenFiler.filerEnabled(prev); + CodeGenFiler.filerWriterEnabled(prev); } } } @@ -325,14 +328,14 @@ void codegenModuleFilerOut(GeneralCreatorRequest req, ModuleDetail moduleDetail) { boolean prev = true; if (req.analysisOnly()) { - prev = CodeGenFiler.filerEnabled(false); + prev = CodeGenFiler.filerWriterEnabled(false); } try { req.filer().codegenModuleFilerOut(moduleDetail); } finally { if (req.analysisOnly()) { - CodeGenFiler.filerEnabled(prev); + CodeGenFiler.filerWriterEnabled(prev); } } } @@ -342,14 +345,14 @@ void codegenApplicationFilerOut(GeneralCreatorRequest req, String applicationBody) { boolean prev = true; if (req.analysisOnly()) { - prev = CodeGenFiler.filerEnabled(false); + prev = CodeGenFiler.filerWriterEnabled(false); } try { req.filer().codegenApplicationFilerOut(applicationTypeName, applicationBody); } finally { if (req.analysisOnly()) { - CodeGenFiler.filerEnabled(prev); + CodeGenFiler.filerWriterEnabled(prev); } } } @@ -358,28 +361,27 @@ Path codegenModuleInfoFilerOut(GeneralCreatorRequest req, ModuleInfoDescriptor descriptor) { boolean prev = true; if (req.analysisOnly()) { - prev = CodeGenFiler.filerEnabled(false); + prev = CodeGenFiler.filerWriterEnabled(false); } try { return req.filer().codegenModuleInfoFilerOut(descriptor, true).orElse(null); } finally { if (req.analysisOnly()) { - CodeGenFiler.filerEnabled(prev); + CodeGenFiler.filerWriterEnabled(prev); } } } @Override - public InterceptorCreatorResponse codegenInterceptors(GeneralCreatorRequest req, - Map interceptionPlans) { + public InterceptorCreatorResponse codegenInterceptors(CodeGenInterceptorRequest request) { InterceptorCreatorResponseDefault.Builder res = InterceptorCreatorResponseDefault.builder(); - res.interceptionPlans(interceptionPlans); + res.interceptionPlans(request.interceptionPlans()); - for (Map.Entry e : interceptionPlans.entrySet()) { + for (Map.Entry e : request.interceptionPlans().entrySet()) { try { - Path filePath = codegenInterceptorFilerOut(req, null, e.getValue()); - res.addGeneratedFile(e.getKey(), filePath); + Optional filePath = codegenInterceptorFilerOut(request.generalCreatorRequest(), null, e.getValue()); + filePath.ifPresent(it -> res.addGeneratedFile(e.getKey(), it)); } catch (Throwable t) { throw new ToolsException("Failed while processing: " + e.getKey(), t); } @@ -388,9 +390,9 @@ public InterceptorCreatorResponse codegenInterceptors(GeneralCreatorRequest req, return res.build(); } - private Path codegenInterceptorFilerOut(GeneralCreatorRequest req, - ActivatorCreatorResponseDefault.Builder builder, - InterceptionPlan interceptionPlan) { + private Optional codegenInterceptorFilerOut(GeneralCreatorRequest req, + ActivatorCreatorResponseDefault.Builder builder, + InterceptionPlan interceptionPlan) { validate(interceptionPlan); TypeName interceptorTypeName = InterceptorCreatorDefault.createInterceptorSourceTypeName(interceptionPlan); InterceptorCreatorDefault interceptorCreator = new InterceptorCreatorDefault(); @@ -398,7 +400,7 @@ private Path codegenInterceptorFilerOut(GeneralCreatorRequest req, if (builder != null) { builder.addServiceTypeInterceptorPlan(interceptorTypeName, interceptionPlan); } - return req.filer().codegenJavaFilerOut(interceptorTypeName, body).orElseThrow(); + return req.filer().codegenJavaFilerOut(interceptorTypeName, body); } private void validate(InterceptionPlan plan) { @@ -832,10 +834,11 @@ String toCodegenCtorArgList(DependenciesInfo dependencies) { .stream() .filter(dep2 -> InjectionPointInfoDefault.CONSTRUCTOR.equals(dep2.elementName())) .forEach(dep2 -> { - if ((nameRef.get() == null)) { + if (nameRef.get() == null) { nameRef.set(dep2.baseIdentity()); } else { - assert (nameRef.get().equals(dep2.baseIdentity())) : "only 1 ctor can be injectable"; + assert (nameRef.get().equals(dep2.baseIdentity())) + : "only one Constructor can be injectable: " + dependencies.fromServiceTypeName(); } args.add("c" + count.incrementAndGet()); }) @@ -852,7 +855,7 @@ List toCodegenInjectCtorArgs(DependenciesInfo dependencies) { AtomicInteger count = new AtomicInteger(); AtomicReference nameRef = new AtomicReference<>(); List args = new ArrayList<>(); - List allCtorArgs = dependencies.allDependenciesFor(InjectionPointInfoDefault.CONSTRUCTOR); + List allCtorArgs = dependencies.allDependenciesFor(InjectionPointInfo.CONSTRUCTOR); allCtorArgs.forEach(dep1 -> dep1.injectionPointDependencies() .forEach(dep2 -> { if (nameRef.get() == null) { diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java index 0bd0118625e..8ecda2d7bf8 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java @@ -32,10 +32,10 @@ import io.helidon.common.Weight; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypeNameDefault; import io.helidon.pico.api.DependenciesInfo; import io.helidon.pico.api.InjectionPointInfo; import io.helidon.pico.api.ModuleComponent; +import io.helidon.pico.api.PicoException; import io.helidon.pico.api.PicoServices; import io.helidon.pico.api.PicoServicesConfig; import io.helidon.pico.api.ServiceInfoCriteria; @@ -50,6 +50,8 @@ import jakarta.inject.Provider; import jakarta.inject.Singleton; +import static io.helidon.common.types.TypeNameDefault.create; +import static io.helidon.common.types.TypeNameDefault.createFromTypeName; import static io.helidon.pico.api.ServiceInfoBasics.DEFAULT_PICO_WEIGHT; import static io.helidon.pico.runtime.ServiceUtils.isQualifiedInjectionTarget; @@ -62,7 +64,7 @@ public class ApplicationCreatorDefault extends AbstractCreator implements Applic /** * The prefix to add before the generated "Application" class name (i.e., "Pico$$" in the "Pico$$Application"). */ - public static final String NAME_PREFIX = ActivatorCreatorDefault.NAME_PREFIX; + public static final String NAME_PREFIX = AbstractCreator.NAME_PREFIX; /** * The "Application" part of the name. @@ -163,7 +165,10 @@ static boolean isAllowListedProviderName(ApplicationCreatorConfigOptions configO } static ServiceInfoCriteria toServiceInfoCriteria(TypeName typeName) { - return ServiceInfoCriteriaDefault.builder().serviceTypeName(typeName.name()).build(); + return ServiceInfoCriteriaDefault.builder() + .serviceTypeName(typeName.name()) + .includeIntercepted(true) + .build(); } static ServiceProvider toServiceProvider(TypeName typeName, @@ -205,13 +210,17 @@ ApplicationCreatorResponse codegen(ApplicationCreatorRequest req, List serviceTypeNames = new ArrayList<>(); List serviceTypeBindings = new ArrayList<>(); for (TypeName serviceTypeName : req.serviceTypeNames()) { - String injectionPlan = toServiceTypeInjectionPlan(picoServices, serviceTypeName, - serviceTypeBindingTemplate, serviceTypeBindingEmptyTemplate); - if (injectionPlan == null) { - continue; + try { + String injectionPlan = toServiceTypeInjectionPlan(picoServices, serviceTypeName, + serviceTypeBindingTemplate, serviceTypeBindingEmptyTemplate); + if (injectionPlan == null) { + continue; + } + serviceTypeNames.add(serviceTypeName); + serviceTypeBindings.add(injectionPlan); + } catch (Exception e) { + throw new ToolsException("Error during injection plan generation for: " + serviceTypeName, e); } - serviceTypeNames.add(serviceTypeName); - serviceTypeBindings.add(injectionPlan); } TypeName application = toApplicationTypeName(req); @@ -231,7 +240,8 @@ ApplicationCreatorResponse codegen(ApplicationCreatorRequest req, String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_APPLICATION_HBS); String body = templateHelper().applySubstitutions(template, subst, true).trim(); - if (req.codeGenPaths().generatedSourcesPath().isPresent()) { + if (req.codeGenPaths().isPresent() + && req.codeGenPaths().get().generatedSourcesPath().isPresent()) { codegen(picoServices, req, application, body); } @@ -278,7 +288,7 @@ static TypeName toApplicationTypeName(ApplicationCreatorRequest req) { packageName = PicoServicesConfig.NAME; } String className = Objects.requireNonNull(codeGen.className().orElse(null)); - return TypeNameDefault.create(packageName, className); + return create(packageName, className); } static String toModuleName(ApplicationCreatorRequest req) { @@ -376,16 +386,17 @@ void codegen(PicoServices picoServices, ApplicationCreatorRequest req, TypeName applicationTypeName, String body) { - CodeGenFiler filer = createDirectCodeGenFiler(req.codeGenPaths(), req.analysisOnly()); + CodeGenFiler filer = createDirectCodeGenFiler(req.codeGenPaths().orElse(null), req.analysisOnly()); Path applicationJavaFilePath = filer.codegenJavaFilerOut(applicationTypeName, body).orElse(null); - String outputDirectory = req.codeGenPaths().outputPath().orElse(null); + String outputDirectory = req.codeGenPaths().isEmpty() + ? null : req.codeGenPaths().get().outputPath().orElse(null); if (outputDirectory != null) { File outDir = new File(outputDirectory); // setup meta-inf services codegenMetaInfServices(filer, - req.codeGenPaths(), + req.codeGenPaths().orElse(null), Map.of(TypeNames.PICO_APPLICATION, List.of(applicationTypeName.name()))); // setup module-info @@ -417,11 +428,16 @@ void codegen(PicoServices picoServices, } } - static TypeName moduleServiceTypeOf(PicoServices picoServices, - String moduleName) { + static Optional moduleServiceTypeOf(PicoServices picoServices, + String moduleName) { Services services = picoServices.services(); - ServiceProvider serviceProvider = services.lookup(ModuleComponent.class, moduleName); - return TypeNameDefault.createFromTypeName(serviceProvider.serviceInfo().serviceTypeName()); + ServiceProvider serviceProvider; + try { + serviceProvider = services.lookup(ModuleComponent.class, moduleName); + } catch (PicoException e) { + return Optional.empty(); + } + return Optional.of(createFromTypeName(serviceProvider.serviceInfo().serviceTypeName())); } void codegenModuleInfoDescriptor(CodeGenFiler filer, @@ -437,27 +453,31 @@ void codegenModuleInfoDescriptor(CodeGenFiler filer, moduleName = descriptor.name(); } - TypeName moduleTypeName = moduleServiceTypeOf(picoServices, moduleName); - String typePrefix = req.codeGen().classPrefixName(); - ModuleInfoCreatorRequest moduleBuilderRequest = ModuleInfoCreatorRequestDefault.builder() - .name(moduleName) - .moduleTypeName(moduleTypeName) - .applicationTypeName(applicationTypeName) - .moduleInfoPath(picoModuleInfoPath.get().toAbsolutePath().toString()) - .classPrefixName(typePrefix) - .applicationCreated(true) - .moduleCreated(false) - .build(); - descriptor = createModuleInfo(moduleBuilderRequest); - filer.codegenModuleInfoFilerOut(descriptor, true); - } else { - Path realModuleInfoPath = filer.toSourceLocation(ModuleUtils.REAL_MODULE_INFO_JAVA_NAME).orElse(null); - if (realModuleInfoPath != null && !realModuleInfoPath.toFile().exists()) { - throw new ToolsException("Expected to find " + realModuleInfoPath - + ". Did the " + PicoServicesConfig.NAME + " APT run?"); + TypeName moduleTypeName = moduleServiceTypeOf(picoServices, moduleName).orElse(null); + if (moduleTypeName != null) { + String typePrefix = req.codeGen().classPrefixName(); + ModuleInfoCreatorRequest moduleBuilderRequest = ModuleInfoCreatorRequestDefault.builder() + .name(moduleName) + .moduleTypeName(moduleTypeName) + .applicationTypeName(applicationTypeName) + .moduleInfoPath(picoModuleInfoPath.get().toAbsolutePath().toString()) + .classPrefixName(typePrefix) + .applicationCreated(true) + .moduleCreated(false) + .build(); + descriptor = createModuleInfo(moduleBuilderRequest); + filer.codegenModuleInfoFilerOut(descriptor, true); + return; } } + + Path realModuleInfoPath = filer.toSourceLocation(ModuleUtils.REAL_MODULE_INFO_JAVA_NAME).orElse(null); + if (realModuleInfoPath != null && !realModuleInfoPath.toFile().exists()) { + throw new ToolsException("Expected to find " + realModuleInfoPath + + ". Did the " + PicoServicesConfig.NAME + " APT run?"); + } } + void codegenMetaInfServices(CodeGenFiler filer, CodeGenPaths paths, Map> metaInfServices) { diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java index a821ece6789..209a2224927 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java @@ -19,6 +19,7 @@ import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -29,7 +30,7 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -37,7 +38,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import javax.annotation.processing.Filer; @@ -53,7 +53,6 @@ import static io.helidon.pico.tools.ModuleUtils.PICO_MODULE_INFO_JAVA_NAME; import static io.helidon.pico.tools.ModuleUtils.normalizedBaseModuleName; import static io.helidon.pico.tools.ModuleUtils.saveAppPackageName; -import static io.helidon.pico.tools.ModuleUtils.toPath; /** * This class is used to generate the source and resources originating from either annotation processing or maven-plugin @@ -64,13 +63,13 @@ public class CodeGenFiler { private static final boolean FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR = true; private static final boolean FILER_WRITE_ONCE_PER_TYPE = true; private static final Set FILER_TYPES_FILED = new LinkedHashSet<>(); - private static boolean filerWriteIsDisabled; + private static volatile boolean filerWriteEnabled = true; private final AbstractFilerMessager filer; private final Boolean enabled; - private final Map deferredMoves = new LinkedHashMap<>(); - private Path targetOutputPath; - private String scratchPathName; + private final Path targetClassOutputPath; + private final Path scratchBaseOutputPath; + private final Path scratchClassClassOutputPath; /** * Constructor. @@ -91,6 +90,14 @@ public class CodeGenFiler { Boolean enabled) { this.filer = Objects.requireNonNull(filer); this.enabled = enabled; + this.targetClassOutputPath = targetClassOutputPath(filer); + this.scratchClassClassOutputPath = scratchClassOutputPath(targetClassOutputPath); + this.scratchBaseOutputPath = scratchClassClassOutputPath.getParent(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(outputPath=" + targetClassOutputPath + ")"; } /** @@ -110,14 +117,14 @@ public static CodeGenFiler create(AbstractFilerMessager filer) { * @param enabled if disabled, pass false * @return the previous value of this setting */ - static boolean filerEnabled(boolean enabled) { - boolean prev = filerWriteIsDisabled; - filerWriteIsDisabled = enabled; + static boolean filerWriterEnabled(boolean enabled) { + boolean prev = filerWriteEnabled; + filerWriteEnabled = enabled; return prev; } - boolean isFilerWriteEnabled() { - return (enabled != null) ? enabled : !filerWriteIsDisabled; + boolean filerWriterEnabled() { + return (enabled != null) ? enabled : filerWriteEnabled; } AbstractFilerMessager filer() { @@ -128,16 +135,6 @@ Messager messager() { return filer; } - /** - * This map represents any move operations that were not capable at the time of code generation, that must be deferred - * until after the annotation processor has completed its round. - * - * @return map of deferred moves from source to target path locations - */ - public Map deferredMoves() { - return Map.copyOf(deferredMoves); - } - /** * Generate the meta-inf services given the provided map. * @@ -170,8 +167,7 @@ public void codegenMetaInfServices(CodeGenPaths paths, mergedSet.add(line); } } - targetOutputPath(f); - } catch (FilerException | NoSuchFileException x) { + } catch (FilerException | NoSuchFileException | FileNotFoundException x) { // don't show the exception in this case messager.debug(getClass().getSimpleName() + ":" + x.getMessage()); } catch (Exception x) { @@ -191,158 +187,123 @@ public void codegenMetaInfServices(CodeGenPaths paths, } } - codegenResourceFilerOut(outPath, baos.toString(StandardCharsets.UTF_8), Optional.empty()); + codegenResourceFilerOut(outPath, baos.toString(StandardCharsets.UTF_8)); } } - private void targetOutputPath(FileObject f) { - Path path = Path.of(f.toUri()); - Path parent = path.getParent(); - Path gparent = (parent == null) ? null : parent.getParent(); - this.targetOutputPath = gparent; - Path scratchName = (parent == null) ? null : parent.getFileName(); - this.scratchPathName = (scratchName == null) ? null : scratchName.toString(); + private static Path targetClassOutputPath(Filer filer) { + if (filer instanceof AbstractFilerMessager.DirectFilerMessager) { + CodeGenPaths paths = ((AbstractFilerMessager.DirectFilerMessager) filer).codeGenPaths(); + return Path.of(paths.outputPath().orElseThrow()); + } + + try { + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", "___"); + Path path = Path.of(f.toUri()); + if (path.getParent() == null) { + throw new IllegalStateException(path.toString()); + } + return path.getParent(); + } catch (Exception e) { + throw new ToolsException("Unable to determine output path", e); + } } - private Path toScratchPath(boolean wantClassesOrTestClassesRelative) { - Objects.requireNonNull(targetOutputPath); - Objects.requireNonNull(scratchPathName); - Path base = targetOutputPath.resolve(PicoServicesConfig.NAME); - return (wantClassesOrTestClassesRelative) ? base.resolve(scratchPathName) : base; + private static Path scratchClassOutputPath(Path targetOutputPath) { + Path fileName = targetOutputPath.getFileName(); + Path parent = targetOutputPath.getParent(); + if (fileName == null || parent == null) { + throw new IllegalStateException(targetOutputPath.toString()); + } + String name = fileName.toString(); + return parent.resolve(PicoServicesConfig.NAME).resolve(name); + } + + private Filer scratchFiler() throws IOException { + Files.createDirectories(scratchClassClassOutputPath); + CodeGenPaths codeGenPaths = CodeGenPathsDefault.builder() + .outputPath(scratchClassClassOutputPath.toString()) + .build(); + return new AbstractFilerMessager.DirectFilerMessager(codeGenPaths, filer.logger()); } /** - * Code generates a resource, providing the ability to update if the resource already exists. + * Code generates a resource. * * @param outPath the path to output the resource to * @param body the resource body - * @param optFnUpdater the optional updater of the body * @return file path coordinates corresponding to the resource in question, or empty if not generated */ public Optional codegenResourceFilerOut(String outPath, - String body, - Optional> optFnUpdater) { + String body) { + return codegenResourceFilerOut(outPath, body, Optional.empty()); + } + + /** + * Code generates a resource, providing the ability to update the body if the resource already exists. + * + * @param outPath the path to output the resource to + * @param body the resource body + * @param updater the updater of the body + * @return file path coordinates corresponding to the resource in question, or empty if not generated + */ + Optional codegenResourceFilerOut(String outPath, + String body, + Function updater) { + return codegenResourceFilerOut(outPath, body, Optional.of(updater)); + } + + private Optional codegenResourceFilerOut(String outPath, + String body, + Optional> optFnUpdater) { Messager messager = messager(); - if (!isFilerWriteEnabled()) { + if (!filerWriterEnabled()) { messager.log("(disabled) Writing " + outPath + " with:\n" + body); return Optional.empty(); } messager.debug("Writing " + outPath); Filer filer = filer(); - boolean contentsAlreadyVerified = false; Function fnUpdater = optFnUpdater.orElse(null); - AtomicReference fileRef = new AtomicReference<>(); + try { - if (fnUpdater != null) { - // attempt to update it... - try { - FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", outPath); + if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR + && targetClassOutputPath != null + && outPath.equals(PICO_MODULE_INFO_JAVA_NAME)) { + // hack: physically relocate it elsewhere under our scratch output directory + filer = scratchFiler(); + } + + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", outPath); + Path fPath = Path.of(f.toUri()); + if (fPath.toFile().exists()) { + if (fnUpdater != null) { + // update it... try (InputStream is = f.openInputStream()) { - String newBody = fnUpdater.apply(is); - if (newBody != null) { - body = newBody; - } + body = fnUpdater.apply(is); } - } catch (NoSuchFileException e) { - // no-op - } catch (Exception e) { - // messager.debug(getClass().getSimpleName() + ":" + e.getMessage()); - contentsAlreadyVerified = tryToEnsureSameContents(e, body, messager, fileRef); } - } - // write it - FileObject f = filer.createResource(StandardLocation.CLASS_OUTPUT, "", outPath); - try (Writer os = f.openWriter()) { - os.write(body); - } - targetOutputPath(f); - - if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR && outPath.equals(PICO_MODULE_INFO_JAVA_NAME) - && targetOutputPath != null) { - // hack: physically relocate it elsewhere - Path originalPath = Path.of(f.toUri()); - Path newPath = toScratchPath(true).resolve(PICO_MODULE_INFO_JAVA_NAME); - if (originalPath.toFile().exists()) { - Path parent = newPath.getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - Files.move(originalPath, newPath, StandardCopyOption.REPLACE_EXISTING); - return Optional.of(newPath); - } else { - deferredMoves.put(originalPath, newPath); + String actualContents = Files.readString(fPath, StandardCharsets.UTF_8); + if (!actualContents.equals(body)) { + Files.writeString(fPath, body, StandardCharsets.UTF_8); + } + } else { + // file does not exist yet... create it the normal way + f = filer.createResource(StandardLocation.CLASS_OUTPUT, "", outPath); + try (Writer os = f.openWriter()) { + os.write(body); } + fPath = Path.of(f.toUri()); } - return toPath(f.toUri()); - } catch (FilerException x) { - // messager.debug(getClass().getSimpleName() + ":" + x.getMessage(), null); - if (!contentsAlreadyVerified) { - tryToEnsureSameContents(x, body, messager, fileRef); - } + return Optional.of(fPath); } catch (Exception x) { - ToolsException te = new ToolsException("Failed to write resource file: " + x.getMessage(), x); + ToolsException te = new ToolsException("Error writing resource file: " + x.getMessage(), x); messager.error(te.getMessage(), te); + // should not make it here + throw te; } - - return Optional.of(fileRef.get().toPath()); - } - - /** - * Throws an error if the contents being written cannot be written, and the desired content is different from what - * is on disk. - * - * @param e the exception thrown by the filer - * @param expected the expected body of the resource - * @param messager the messager to handle errors and logging - * @param fileRef the reference that will be set to the coordinates of the resource - * @return true if the implementation was able to verify the contents match - */ - boolean tryToEnsureSameContents(Exception e, - String expected, - Messager messager, - AtomicReference fileRef) { - if (!(e instanceof FilerException)) { - return false; - } - - String message = e.getMessage(); - if (message == null) { - return false; - } - - int pos = message.lastIndexOf(' '); - if (pos <= 0) { - return false; - } - - String maybePath = message.substring(pos + 1); - File file = new File(maybePath); - if (!file.exists()) { - return false; - } - if (fileRef != null) { - fileRef.set(file); - } - - try { - String actual = Files.readString(file.toPath(), StandardCharsets.UTF_8); - if (!actual.equals(expected)) { - String error = "expected contents to match for file: " + file - + "\nexpected:\n" + expected - + "\nactual:\n" + actual; - ToolsException te = new ToolsException(error); - messager.error(error, te); - } - } catch (Exception x) { - messager.debug(getClass().getSimpleName() + ": unable to verify contents match: " + file + "; " + x.getMessage(), - null); - return false; - } - - return true; } /** @@ -396,13 +357,13 @@ void codegenActivatorFilerOut(ActivatorCodeGenDetail activatorDetail) { public Optional codegenJavaFilerOut(TypeName typeName, String body) { Messager messager = messager(); - if (!isFilerWriteEnabled()) { + if (!filerWriterEnabled()) { messager.log("(disabled) Writing " + typeName + " with:\n" + body); return Optional.empty(); } if (FILER_WRITE_ONCE_PER_TYPE && !FILER_TYPES_FILED.add(typeName)) { - messager.log(typeName + ": already processed"); + messager.debug(typeName + ": already processed"); return Optional.empty(); } @@ -414,7 +375,7 @@ public Optional codegenJavaFilerOut(TypeName typeName, try (Writer os = javaSrc.openWriter()) { os.write(body); } - return toPath(javaSrc.toUri()); + return Optional.of(Path.of(javaSrc.toUri())); } catch (FilerException x) { messager.log("Failed to write java file: " + x); } catch (Exception x) { @@ -437,7 +398,7 @@ Optional codegenModuleInfoFilerOut(ModuleInfoDescriptor newDeltaDescriptor Messager messager = messager(); String typeName = PICO_MODULE_INFO_JAVA_NAME; - if (!isFilerWriteEnabled()) { + if (!filerWriterEnabled()) { messager.log("(disabled) Writing " + typeName + " with:\n" + newDeltaDescriptor); return Optional.empty(); } @@ -450,7 +411,7 @@ Optional codegenModuleInfoFilerOut(ModuleInfoDescriptor newDeltaDescriptor }; Optional filePath - = codegenResourceFilerOut(typeName, newDeltaDescriptor.contents(), Optional.of(moduleInfoUpdater)); + = codegenResourceFilerOut(typeName, newDeltaDescriptor.contents(), moduleInfoUpdater); if (filePath.isPresent()) { messager.debug("Wrote module-info: " + filePath.get()); } else if (overwriteTargetIfExists) { @@ -458,8 +419,7 @@ Optional codegenModuleInfoFilerOut(ModuleInfoDescriptor newDeltaDescriptor } if (!newDeltaDescriptor.isUnnamed()) { - saveAppPackageName(toScratchPath(false), - normalizedBaseModuleName(newDeltaDescriptor.name())); + saveAppPackageName(scratchBaseOutputPath, normalizedBaseModuleName(newDeltaDescriptor.name())); } return filePath; @@ -472,8 +432,6 @@ Optional codegenModuleInfoFilerOut(ModuleInfoDescriptor newDeltaDescriptor * @return the module-info descriptor, or empty if it doesn't exist */ Optional readModuleInfo(String name) { - Objects.requireNonNull(name); - try { CharSequence body = readResourceAsString(name); return Optional.ofNullable((body == null) ? null : ModuleInfoDescriptor.create(body.toString())); @@ -491,13 +449,13 @@ Optional readModuleInfo(String name) { CharSequence readResourceAsString(String name) { try { FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", name); - targetOutputPath(f); return f.getCharContent(true); } catch (IOException e) { - if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR && name.equals(PICO_MODULE_INFO_JAVA_NAME) - && targetOutputPath != null) { + if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR + && targetClassOutputPath != null + && name.equals(PICO_MODULE_INFO_JAVA_NAME)) { // hack: physically read it from its relocated location - File newPath = new File(targetOutputPath.toFile().getAbsolutePath(), name); + File newPath = new File(scratchClassClassOutputPath.toFile(), name); if (newPath.exists()) { try { return Files.readString(newPath.toPath()); @@ -507,7 +465,7 @@ CharSequence readResourceAsString(String name) { } } - messager().debug("unable to load resource: " + name); + messager().debug("Unable to load resource: " + name); return null; } } @@ -519,13 +477,21 @@ CharSequence readResourceAsString(String name) { * @return the file path coordinates if it can be ascertained, or empty if not possible to ascertain this information */ Optional toResourceLocation(String name) { + // hack: physically read it from its relocated location + if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR + && targetClassOutputPath != null + && name.equals(PICO_MODULE_INFO_JAVA_NAME)) { + return Optional.of(scratchClassClassOutputPath.resolve(name)); + } + try { FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", name); - targetOutputPath(f); - return toPath(f.toUri()); + Path path = Paths.get(f.toUri()); + return Optional.of(path); } catch (IOException e) { messager().debug("unable to load resource: " + name); } + return Optional.empty(); } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenInterceptorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenInterceptorRequest.java new file mode 100644 index 00000000000..70cc34cd15a --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenInterceptorRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.tools; + +import java.util.Map; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; + +/** + * Used by {@link io.helidon.pico.tools.spi.ActivatorCreator#codegenInterceptors}. + */ +@Builder +public interface CodeGenInterceptorRequest { + + /** + * The general creator request. + * + * @return the general creator request + */ + GeneralCreatorRequest generalCreatorRequest(); + + /** + * The interception plan. + * + * @return the interception plan + */ + Map interceptionPlans(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java index 6e16b847def..22f30b7d62b 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java @@ -22,7 +22,7 @@ /** * Code generation utilities. */ -class CodeGenUtils { +final class CodeGenUtils { private CodeGenUtils() { } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorDefault.java index 2e0572814c7..ce63df12ce7 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorDefault.java @@ -23,7 +23,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import io.helidon.common.LazyValue; @@ -58,6 +57,7 @@ import static io.helidon.pico.tools.TypeTools.methodsAnnotatedWith; import static io.helidon.pico.tools.TypeTools.providesContractType; import static io.helidon.pico.tools.TypeTools.toAccess; +import static java.util.function.Predicate.not; /** * The default implementation of {@link io.helidon.pico.tools.spi.ExternalModuleCreator}. @@ -104,11 +104,11 @@ public ExternalModuleCreatorResponse prepareToCreateExternalModule(ExternalModul // process each found service type scan.get().getAllStandardClasses() .stream() - .filter((classInfo) -> packageNames.contains(classInfo.getPackageName())) - .filter((classInfo) -> !classInfo.isInterface()) - .filter((classInfo) -> !classInfo.isExternalClass()) - .filter((classInfo) -> !isPrivate(classInfo.getModifiers())) - .filter((classInfo) -> !classInfo.isInnerClass() || req.innerClassesProcessed()) + .filter(classInfo -> packageNames.contains(classInfo.getPackageName())) + .filter(not(ClassInfo::isInterface)) + .filter(not(ClassInfo::isExternalClass)) + .filter(classInfo -> !isPrivate(classInfo.getModifiers())) + .filter(classInfo -> !classInfo.isInnerClass() || req.innerClassesProcessed()) .forEach(this::processServiceType); ActivatorCreatorCodeGen activatorCreatorCodeGen = ActivatorCreatorDefault @@ -138,8 +138,7 @@ private Collection identifyExternalJars(Collection packageNames) { if (packageInfo != null) { for (ClassInfo classInfo : packageInfo.getClassInfo()) { URI uri = classInfo.getClasspathElementURI(); - Optional filePath = ModuleUtils.toPath(uri); - filePath.ifPresent(classpath::add); + classpath.add(Path.of(uri)); } } } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java index e317f56b49e..851942083ed 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java @@ -41,7 +41,7 @@ public interface GeneralCreatorRequest extends GeneralCodeGenNames { * * @return the code paths to use for reading and writing artifacts */ - CodeGenPaths codeGenPaths(); + Optional codeGenPaths(); /** * Optionally, any compiler options to pass explicitly to the java compiler. Not applicable during annotation processing. diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java index 655c5925e1b..c1d40e5f496 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java @@ -123,14 +123,6 @@ public Set allowListedAnnotationTypes() { return (allowListedAnnoTypeNames != null) ? allowListedAnnoTypeNames : Collections.emptySet(); } - @Override - public Optional createInterceptorPlan(ServiceInfoBasics interceptedService, - ProcessingEnvironment processEnv, - Set annotationTypeTriggers) { - return createInterceptorProcessor(interceptedService, this, Optional.of(processEnv)) - .createInterceptorPlan(annotationTypeTriggers); - } - /** * Abstract base for handling the resolution of annotation types by name. */ diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java index adbbed14052..20dff362081 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java @@ -64,7 +64,7 @@ public class ModuleUtils { * This application package name is what we fall back to for the application name and the module name if not otherwise * specified directly. */ - public static final String APPLICATION_PACKAGE_FILE_NAME = PicoServicesConfig.NAME + "-app-package-name.txt"; + public static final String APPLICATION_PACKAGE_FILE_NAME = "app-package-name.txt"; static final String SERVICE_PROVIDER_MODULE_INFO_HBS = "module-info.hbs"; static final String SRC_MAIN_JAVA_DIR = "/src/main/java"; @@ -324,19 +324,6 @@ public static Path toBasePath(String sourcePath) { return Objects.requireNonNull(path); } - /** - * Will return non-empty File if the uri represents a local file on the fs. - * - * @param uri the uri of the artifact - * @return the file instance, or empty if not local - */ - static Optional toPath(URI uri) { - if (uri.getHost() != null) { - return Optional.empty(); - } - return Optional.of(Paths.get(uri)); - } - /** * Returns true if the given module name is unnamed. * diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java b/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java index 34d03805132..bc0f4bb56f9 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java @@ -33,7 +33,6 @@ import java.util.stream.Collectors; import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.element.TypeElement; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; @@ -59,6 +58,7 @@ public class ServicesToProcess implements Resettable { private static final ServicesToProcess SERVICES = new ServicesToProcess(); private static final AtomicInteger RUNNING_PROCESSORS = new AtomicInteger(); + private static final List RUNNABLES_TO_CALL_WHEN_DONE = new ArrayList<>(); private final Set servicesTypeNames = new LinkedHashSet<>(); private final Set requiredModules = new TreeSet<>(); @@ -104,6 +104,16 @@ public static ServicesToProcess servicesInstance() { private ServicesToProcess() { } + /** + * Creates a new instance, apart from the current global singleton instance exposed from {@link #servicesInstance()}. + * + * @return the new instance + * @see #servicesInstance() + */ + public static ServicesToProcess create() { + return new ServicesToProcess(); + } + @Override public boolean reset(boolean ignoredDeep) { servicesTypeNames.clear(); @@ -223,12 +233,12 @@ Map activatorGenericDecls() { */ public void addAccessLevel(TypeName serviceTypeName, InjectionPointInfo.Access access) { + Objects.requireNonNull(serviceTypeName); + Objects.requireNonNull(access); addServiceTypeName(serviceTypeName); - if (access != null) { - Object prev = servicesToAccess.put(serviceTypeName, access); - if (prev != null && !access.equals(prev)) { - throw new IllegalStateException("Can only support one access level for " + serviceTypeName); - } + Object prev = servicesToAccess.put(serviceTypeName, access); + if (prev != null && !access.equals(prev)) { + throw new IllegalStateException("Can only support one access level for " + serviceTypeName); } } @@ -825,7 +835,7 @@ String determineGeneratedPackageName() { * @param roundEnv the round env */ public static void onBeginProcessing(Messager processor, - Set annotations, + Set annotations, RoundEnvironment roundEnv) { boolean reallyStarted = !annotations.isEmpty(); if (reallyStarted && !roundEnv.processingOver()) { @@ -835,6 +845,15 @@ public static void onBeginProcessing(Messager processor, .getSimpleName() + " processing " + annotations + "; really-started=" + reallyStarted); } + /** + * Called to add a runnable to call when done with all annotation processing. + * + * @param runnable the runnable to call + */ + public static void addOnDoneRunnable(Runnable runnable) { + RUNNABLES_TO_CALL_WHEN_DONE.add(runnable); + } + /** * Called to signal the end of an annotation processing phase. * @@ -843,7 +862,7 @@ public static void onBeginProcessing(Messager processor, * @param roundEnv the round env */ public static void onEndProcessing(Messager processor, - Set annotations, + Set annotations, RoundEnvironment roundEnv) { boolean done = annotations.isEmpty(); if (done && roundEnv.processingOver()) { @@ -855,6 +874,11 @@ public static void onEndProcessing(Messager processor, // perform module analysis to ensure the proper definitions are specified for modules and applications ServicesToProcess.servicesInstance().performModuleUsageValidation(processor); } + + if (done) { + RUNNABLES_TO_CALL_WHEN_DONE.forEach(Runnable::run); + RUNNABLES_TO_CALL_WHEN_DONE.clear(); + } } /** diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java b/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java index d623dc9af19..7a76110d28a 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java @@ -58,6 +58,17 @@ public final class TypeNames { */ public static final String PICO_CONFIGURED_BY = "io.helidon.pico.configdriven.api.ConfiguredBy"; + /** + * Pico class name {@value} for {@code InjectionPointProvider}. + */ + public static final String PICO_INJECTION_POINT_PROVIDER = "io.helidon.pico.api.InjectionPointProvider"; + + /** + * Pico class name {@value} for {@code AbstractConfiguredServiceProvider}. + */ + public static final String PICO_ABSTRACT_CONFIGURED_SERVICE_PROVIDER = + "io.helidon.pico.configdriven.runtime.AbstractConfiguredServiceProvider"; + /** * Jakarta {@value} annotation. */ diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java b/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java index 347b17b4b58..e1869374543 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java @@ -54,6 +54,7 @@ import io.helidon.common.LazyValue; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.AnnotationAndValueDefault; +import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; import io.helidon.pico.api.ElementInfo; @@ -82,6 +83,7 @@ import io.github.classgraph.TypeArgument; import io.github.classgraph.TypeSignature; import jakarta.inject.Provider; +import jakarta.inject.Singleton; import static io.helidon.pico.tools.CommonUtils.first; import static io.helidon.pico.tools.CommonUtils.hasValue; @@ -404,6 +406,41 @@ public static Set createQualifierAndValueSet(List annotationsWithAnnotationOf(TypeElement type, + String metaAnnoType) { + Set annotations = createAnnotationAndValueSet(type); + if (annotations.isEmpty()) { + return List.of(); + } + + List list = annotationsWithAnnotationsOfNoOpposite(type, metaAnnoType); + if (list.isEmpty()) { + list.addAll(annotationsWithAnnotationsOfNoOpposite(type, oppositeOf(metaAnnoType))); + } + + return annotations.stream() + .filter(it -> list.contains(it.typeName().name())) + .collect(Collectors.toList()); + } + + private static List annotationsWithAnnotationsOfNoOpposite(TypeElement type, + String annotation) { + List list = new ArrayList<>(); + type.getAnnotationMirrors() + .forEach(am -> findAnnotationMirror(annotation, + am.getAnnotationType().asElement() + .getAnnotationMirrors()) + .ifPresent(it -> list.add(am.getAnnotationType().asElement().toString()))); + return list; + } + /** * Creates a set of annotations based upon class info introspection. * @@ -539,6 +576,31 @@ static Set createAnnotationAndValueSetFromMetaAnnotation(Ann return result; } + /** + * Extracts all of the scope type names from the provided type. + * + * @param type the type to analyze + * @return the list of all scope type annotation and values + */ + public static List toScopeAnnotations(TypeElement type) { + List scopeAnnotations = annotationsWithAnnotationOf(type, TypeNames.JAKARTA_SCOPE); + if (scopeAnnotations.isEmpty()) { + scopeAnnotations = annotationsWithAnnotationOf(type, TypeNames.JAKARTA_CDI_NORMAL_SCOPE); + } + + if (Options.isOptionEnabled(Options.TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE)) { + boolean hasApplicationScope = scopeAnnotations.stream() + .map(it -> it.typeName().name()) + .anyMatch(it -> it.equals(TypeNames.JAKARTA_APPLICATION_SCOPED) + || it.equals(TypeNames.JAVAX_APPLICATION_SCOPED)); + if (hasApplicationScope) { + scopeAnnotations.add(AnnotationAndValueDefault.create(Singleton.class)); + } + } + + return scopeAnnotations; + } + /** * Extract the scope name from the given introspected class. * @@ -1388,7 +1450,7 @@ public static boolean isAbstract(Element element) { /** * Converts the modifiers to an {@link io.helidon.pico.api.ElementInfo.Access} type. * - * @param modifiers the moifiers + * @param modifiers the modifiers * @return the access */ static InjectionPointInfo.Access toAccess(int modifiers) { @@ -1403,6 +1465,23 @@ static InjectionPointInfo.Access toAccess(int modifiers) { } } + /** + * Converts the modifiers to an {@link io.helidon.pico.api.ElementInfo.Access} type. + * + * @param modifiers the modifiers + * @return the access + */ + public static ElementInfo.Access toAccess(Set modifiers) { + if (modifiers.contains(TypeInfo.MODIFIER_PROTECTED)) { + return ElementInfo.Access.PROTECTED; + } else if (modifiers.contains(TypeInfo.MODIFIER_PRIVATE)) { + return ElementInfo.Access.PRIVATE; + } else if (modifiers.contains(TypeInfo.MODIFIER_PUBLIC)) { + return ElementInfo.Access.PUBLIC; + } + return ElementInfo.Access.PACKAGE_PRIVATE; + } + /** * Determines the access from an {@link javax.lang.model.element.Element} (from anno processing). * diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java index 57f8e739bc6..7525362ba52 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java @@ -16,14 +16,11 @@ package io.helidon.pico.tools.spi; -import java.util.Map; - import io.helidon.common.types.TypeName; import io.helidon.pico.api.Contract; import io.helidon.pico.tools.ActivatorCreatorRequest; import io.helidon.pico.tools.ActivatorCreatorResponse; -import io.helidon.pico.tools.GeneralCreatorRequest; -import io.helidon.pico.tools.InterceptionPlan; +import io.helidon.pico.tools.CodeGenInterceptorRequest; import io.helidon.pico.tools.InterceptorCreatorResponse; /** @@ -57,11 +54,9 @@ public interface ActivatorCreator { * Generates just the interceptors. * * @param request the request for what to generate - * @param interceptionPlans the interceptor plans * @return the response result for the create operation */ - InterceptorCreatorResponse codegenInterceptors(GeneralCreatorRequest request, - Map interceptionPlans); + InterceptorCreatorResponse codegenInterceptors(CodeGenInterceptorRequest request); /** * Generates the would-be implementation type name that will be generated if diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java index da453f6b5fc..ae32c6bf091 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java @@ -107,19 +107,6 @@ default boolean isAllowListed(String annotationType) { return allowListedAnnotationTypes().contains(annotationType); } - /** - * After an annotation qualifies the enclosing service for interception, this method will be used to provide - * the injection plan that applies to that service type. - * - * @param interceptedService the service being intercepted - * @param processingEnvironment optionally, the processing environment (if being called by annotation processing) - * @param annotationTypeTriggers the set of annotation names that are associated with interception. - * @return the injection plan, or empty for the implementation to use the default strategy for creating a plan - */ - Optional createInterceptorPlan(ServiceInfoBasics interceptedService, - ProcessingEnvironment processingEnvironment, - Set annotationTypeTriggers); - /** * Returns the processor appropriate for the context revealed in the calling arguments, favoring reflection if * the serviceTypeElement is provided. diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/ApplicationCreatorDefaultTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/ApplicationCreatorDefaultTest.java index 538cf7c851f..7378f438ccf 100644 --- a/pico/tools/src/test/java/io/helidon/pico/tools/ApplicationCreatorDefaultTest.java +++ b/pico/tools/src/test/java/io/helidon/pico/tools/ApplicationCreatorDefaultTest.java @@ -59,11 +59,13 @@ void codegenHelloWorldApplication() { PicoServices picoServices = PicoServices.picoServices().orElseThrow(); Services services = picoServices.services(); List> serviceProviders = services.lookupAll(allServices); + assertThat(serviceProviders.size(), is(0)); List serviceTypeNames = serviceProviders.stream() .map(sp -> TypeNameDefault.createFromTypeName(sp.serviceInfo().serviceTypeName())) .collect(Collectors.toList()); + // note: this test needs to align with target/pico/... for this to work/test properly CodeGenPaths codeGenPaths = CodeGenPathsDefault.builder() .generatedSourcesPath("target/pico/generated-sources") .outputPath("target/pico/generated-classes")