diff --git a/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java b/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java index fca1af3b8f4..440265555ba 100644 --- a/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java @@ -103,6 +103,16 @@ public interface ServiceInfoCriteria { */ Optional moduleName(); + /** + * Determines whether the non-proxied, {@link Intercepted} services should be returned in any lookup operation. If this + * option is disabled then only the {@link Interceptor}-generated service will be eligible to be returned and not the service + * being intercepted. + * The default value is {@code false}. + * + * @return true if the non-proxied type intercepted services should be eligible + */ + boolean includeIntercepted(); + /** * Determines whether this service info matches the criteria for injection. * Matches is a looser form of equality check than {@code equals()}. If a service matches criteria diff --git a/pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java b/pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java index a8759166bd6..492be957cfd 100644 --- a/pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java @@ -43,6 +43,15 @@ public interface ServiceProviderBindable extends ServiceProvider { */ void moduleName(String moduleName); + /** + * Returns true if this service provider instance is an {@link Interceptor}. + * + * @return true if this service provider is an interceptor + */ + default boolean isInterceptor() { + return false; + } + /** * Returns {@code true} if this service provider is intercepted. * diff --git a/pico/configdriven/tests/configuredby-application/pom.xml b/pico/configdriven/tests/configuredby-application/pom.xml index 0765f49631d..ab088df9822 100644 --- a/pico/configdriven/tests/configuredby-application/pom.xml +++ b/pico/configdriven/tests/configuredby-application/pom.xml @@ -99,7 +99,6 @@ -Apico.debug=${pico.debug} -Apico.autoAddNonContractInterfaces=true - -Apico.allowListedInterceptorAnnotations=jakarta.inject.Named -Apico.application.pre.create=true -Apico.mapApplicationToSingletonScope=true diff --git a/pico/configdriven/tests/configuredby/pom.xml b/pico/configdriven/tests/configuredby/pom.xml index 2fa9883c975..52489467d70 100644 --- a/pico/configdriven/tests/configuredby/pom.xml +++ b/pico/configdriven/tests/configuredby/pom.xml @@ -112,9 +112,6 @@ -Apico.autoAddNonContractInterfaces=true - - - true diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/IZ.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/IZ.java new file mode 100644 index 00000000000..eb223b38872 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/IZ.java @@ -0,0 +1,26 @@ +/* + * 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.interceptor.test; + +import io.helidon.pico.Contract; + +@Contract +public interface IZ { + + String methodIZ1(String arg1); + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/TestInterceptorTrigger.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/TestInterceptorTrigger.java new file mode 100644 index 00000000000..012086bbd98 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/TestInterceptorTrigger.java @@ -0,0 +1,27 @@ +/* + * 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.interceptor.test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import io.helidon.pico.InterceptedTrigger; + +@InterceptedTrigger +@Retention(RetentionPolicy.CLASS) +public @interface TestInterceptorTrigger { +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/ZImpl.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/ZImpl.java new file mode 100644 index 00000000000..729e39d015c --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/ZImpl.java @@ -0,0 +1,64 @@ +/* + * 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.interceptor.test; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.pico.configdriven.configuredby.test.FakeWebServer; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +/** + * This test case is applying {@link InterceptorBasedAnno} (an {@link io.helidon.pico.InterceptedTrigger}) on this class directly. + * Since it is a config-driven service it is forced to used the interface based approach to interceptors. + */ +// TODO: https://github.com/helidon-io/helidon/issues/6542 +@TestInterceptorTrigger +//@ConfiguredBy(ZImplConfig.class) +@SuppressWarnings("ALL") +public class ZImpl implements IZ { + private final AtomicInteger postConstructCallCount = new AtomicInteger(); + +// @Inject +// ZImpl(ZImplConfig config/*, +// List> singletons*/) { +// assert (config != null && !config.name().isEmpty()) : Objects.toString(config); +//// assert (singletons.size() == 1) : singletons.toString(); +// } + + @Inject + ZImpl(Optional fakeWebServer) { + assert (fakeWebServer.isPresent()); + } + + @Override + public String methodIZ1(String val) { + return "methodIZ1:" + val; + } + + @PostConstruct + void postConstruct() { + postConstructCallCount.incrementAndGet(); + } + + int postConstructCallCount() { + return postConstructCallCount.get(); + } + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/ZImplConfig.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/ZImplConfig.java new file mode 100644 index 00000000000..e432944695f --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/interceptor/test/ZImplConfig.java @@ -0,0 +1,34 @@ +/* + * 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.interceptor.test; + +import io.helidon.builder.config.ConfigBean; + +/** + * Drives {@link ZImpl} activation. + */ +@ConfigBean(drivesActivation = true) +public interface ZImplConfig { + + /** + * For testing purposes. + * + * @return for testing purposes + */ + String name(); + +} 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 49fe3144f90..6622f940a6f 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 @@ -340,6 +340,8 @@ protected void innerExecute() { } else { getLog().error("failed to process", res.error().orElse(null)); } + } catch (Exception e) { + throw new ToolsException("An error occurred creating the " + PicoServicesConfig.NAME + " Application", e); } finally { Thread.currentThread().setContextClassLoader(prev); } 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 f1b516f9047..1ff59926e13 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 @@ -27,7 +27,6 @@ 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; @@ -170,11 +169,10 @@ public boolean process(Set annotations, if (!roundEnv.processingOver()) { for (String annoType : annoTypes()) { // annotation may not be on the classpath, in such a case just ignore it - DefaultTypeName annoName = DefaultTypeName.createFromTypeName(annoType); - - if (available(annoName)) { - Set typesToProcess = roundEnv.getElementsAnnotatedWith(toTypeElement(annoName)); - + TypeName annoName = DefaultTypeName.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... @@ -362,12 +360,16 @@ protected void processServiceType(TypeName serviceTypeName, 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( - toInterceptedServiceInfoFor(serviceTypeName), interceptorCreator, Optional.of(processingEnv)); + interceptedServiceInfo, + interceptorCreator, + Optional.of(processingEnv)); Set annotationTypeTriggers = processor.allAnnotationTypeTriggers(); if (annotationTypeTriggers.isEmpty()) { services.addInterceptorPlanFor(serviceTypeName, Optional.empty()); @@ -376,7 +378,7 @@ void maybeSetInterceptorPlanForServiceType(TypeName serviceTypeName, Optional plan = processor.createInterceptorPlan(annotationTypeTriggers); if (plan.isEmpty()) { - warn("expected to see an interception plan for: " + serviceTypeName); + warn("unable to produce an interception plan for: " + serviceTypeName); } services.addInterceptorPlanFor(serviceTypeName, plan); } @@ -517,14 +519,18 @@ void adjustContractsForExternals(Set contracts, Set externalModuleNamesRequired) { AtomicReference externalModuleName = new AtomicReference<>(); for (TypeName contract : contracts) { - if (!isInThisModule(toTypeElement(contract), externalModuleName)) { + Optional typeElement = toTypeElement(contract); + if (typeElement.isPresent() + && !isInThisModule(typeElement.get(), externalModuleName)) { maybeAddExternalModule(externalModuleName.get(), externalModuleNamesRequired); externalContracts.add(contract); } } for (TypeName externalContract : externalContracts) { - if (isInThisModule(toTypeElement(externalContract), externalModuleName)) { + 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); } @@ -723,7 +729,7 @@ void gatherContractsToBeProcessed(Set processed, }); toServiceTypeHierarchy(typeElement, false).stream() - .map(te -> toTypeElement(te).asType()) + .map(te -> toTypeElement(te).orElseThrow().asType()) .forEach(tm -> { processed.add(tm); gatherContractsToBeProcessed(processed, TypeTools.toTypeElement(tm).orElse(null)); @@ -793,18 +799,8 @@ Set toExternalContracts(TypeElement type, return result; } - TypeElement toTypeElement(TypeName typeName) { - return Objects.requireNonNull(processingEnv.getElementUtils().getTypeElement(typeName.name())); - } - - /** - * Check if a type is available on application classpath. - * - * @param typeName type name - * @return {@code true} if the type is available - */ - boolean available(TypeName typeName) { - return processingEnv.getElementUtils().getTypeElement(typeName.name()) != null; + Optional toTypeElement(TypeName typeName) { + return Optional.ofNullable(processingEnv.getElementUtils().getTypeElement(typeName.name())); } System.Logger logger() { 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 45fbb6f26eb..794af092e23 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 @@ -116,8 +116,11 @@ public boolean process(Set annotations, if (!roundEnv.processingOver()) { for (String annoType : annoTypes()) { TypeName annoName = DefaultTypeName.createFromTypeName(annoType); - TypeElement annoElement = toTypeElement(annoName); - Set typesToProcess = roundEnv.getElementsAnnotatedWith(annoElement); + Optional annoElement = toTypeElement(annoName); + if (annoElement.isEmpty()) { + continue; + } + Set typesToProcess = roundEnv.getElementsAnnotatedWith(annoElement.get()); doInner(annoName, typesToProcess, roundEnv); } } @@ -184,9 +187,7 @@ void doInner(TypeName annoTypeName, void doFiler(CustomAnnotationTemplateResponse response) { AbstractFilerMessager filer = AbstractFilerMessager.createAnnotationBasedFiler(processingEnv, this); CodeGenFiler codegen = CodeGenFiler.create(filer); - response.generatedSourceCode().forEach((typeName, codeBody) -> { - codegen.codegenJavaFilerOut(typeName, codeBody); - }); + response.generatedSourceCode().forEach(codegen::codegenJavaFilerOut); response.generatedResources().forEach((typedElementName, resourceBody) -> { String fileType = typedElementName.elementName(); if (!hasValue(fileType)) { diff --git a/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java index 4b79dd8f618..c5afc690acc 100644 --- a/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java +++ b/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java @@ -96,6 +96,7 @@ public abstract class AbstractServiceProvider private DependenciesInfo dependencies; private Map injectionPlan; private ServiceProvider interceptor; + private boolean thisIsAnInterceptor; /** * The default constructor. @@ -245,6 +246,11 @@ public void moduleName(String moduleName) { } } + @Override + public boolean isInterceptor() { + return thisIsAnInterceptor; + } + @Override public Optional> interceptor() { return Optional.ofNullable(interceptor); @@ -257,6 +263,24 @@ public void interceptor(ServiceProvider interceptor) { throw alreadyInitialized(); } this.interceptor = interceptor; + if (interceptor instanceof AbstractServiceProvider) { + ((AbstractServiceProvider) interceptor).intercepted(this); + } + } + + /** + * Incorporate the intercepted qualifiers into our own qualifiers. + * + * @param intercepted the service being intercepted + */ + void intercepted(AbstractServiceProvider intercepted) { + if (activationSemaphore.availablePermits() == 0 || phase != Phase.INIT) { + throw alreadyInitialized(); + } + this.thisIsAnInterceptor = true; + this.serviceInfo = DefaultServiceInfo.toBuilder(this.serviceInfo) + .addQualifiers(intercepted.serviceInfo().qualifiers()) + .build(); } @Override diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java index f7d1e14e387..6ff09427c08 100644 --- a/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java @@ -34,6 +34,7 @@ import io.helidon.pico.InjectionException; import io.helidon.pico.InjectionPointInfo; import io.helidon.pico.Interceptor; +import io.helidon.pico.PicoServiceProviderException; import io.helidon.pico.PicoServices; import io.helidon.pico.PicoServicesConfig; import io.helidon.pico.ServiceInfo; @@ -70,7 +71,13 @@ static Map createInjectionPlans(PicoServices picoServ } dependencies.allDependencies() - .forEach(dep -> accumulate(dep, result, picoServices, self, resolveIps, logger)); + .forEach(dep -> { + try { + accumulate(dep, result, picoServices, self, resolveIps, logger); + } catch (Exception e) { + throw new PicoServiceProviderException("An error occurred creating the injection plan", e, self); + } + }); return result; } @@ -82,17 +89,8 @@ private static void accumulate(DependencyInfo dep, ServiceProvider self, boolean resolveIps, System.Logger logger) { - ServiceInfoCriteria depTo = dep.dependencyTo(); ServiceInfo selfInfo = self.serviceInfo(); - if (selfInfo.declaredWeight().isPresent() - && selfInfo.contractsImplemented().containsAll(depTo.contractsImplemented())) { - // if we have a weight on ourselves, and we inject an interface that we actually offer, then - // be sure to use it to get lower weighted injection points ... - depTo = DefaultServiceInfoCriteria.toBuilder(depTo) - .weight(selfInfo.declaredWeight().get()) - .build(); - } - + ServiceInfoCriteria depTo = toCriteria(dep, self, selfInfo); Services services = picoServices.services(); PicoServicesConfig cfg = picoServices.config(); boolean isPrivateSupported = cfg.supportsJsr330Privates(); @@ -179,6 +177,37 @@ private static void accumulate(DependencyInfo dep, }); } + /** + * Creates and maybe adjusts the criteria to match the context of who is doing the lookup. + * + * @param dep the dependency info to lookup + * @param self the service doing the lookup + * @param selfInfo the service info for the service doing the lookup + * @return the criteria + */ + static ServiceInfoCriteria toCriteria(DependencyInfo dep, + ServiceProvider self, + ServiceInfo selfInfo) { + ServiceInfoCriteria criteria = dep.dependencyTo(); + DefaultServiceInfoCriteria.Builder builder = null; + if (selfInfo.declaredWeight().isPresent() + && selfInfo.contractsImplemented().containsAll(criteria.contractsImplemented())) { + // if we have a weight on ourselves, and we inject an interface that we actually offer, then + // be sure to use it to get lower weighted injection points + builder = DefaultServiceInfoCriteria.toBuilder(criteria) + .weight(selfInfo.declaredWeight().get()); + } + + if ((self instanceof ServiceProviderBindable) && ((ServiceProviderBindable) self).isInterceptor()) { + if (builder == null) { + builder = DefaultServiceInfoCriteria.toBuilder(criteria); + } + builder = builder.includeIntercepted(true); + } + + return (builder != null) ? builder.build() : criteria; + } + /** * Resolution comes after the plan was loaded or created. * diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java index fe055029a81..5935cc0d3f1 100644 --- a/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java @@ -82,40 +82,46 @@ class DefaultServices implements Services, ServiceBinder, Resettable { } @SuppressWarnings({"unchecked", "rawtypes"}) - static List explodeAndSort(Collection coll, - ServiceInfoCriteria criteria, - boolean expected) { - List result; - + static List explodeFilterAndSort(Collection coll, + ServiceInfoCriteria criteria, + boolean expected) { + List exploded; if ((coll.size() > 1) || coll.stream().anyMatch(sp -> sp instanceof ServiceProviderProvider)) { - result = new ArrayList<>(); + exploded = new ArrayList<>(); coll.forEach(s -> { if (s instanceof ServiceProviderProvider) { List> subList = ((ServiceProviderProvider) s) .serviceProviders(criteria, true, true); if (subList != null && !subList.isEmpty()) { - subList.stream().filter(Objects::nonNull).forEach(result::add); + subList.stream().filter(Objects::nonNull).forEach(exploded::add); } } else { - result.add(s); + exploded.add(s); } }); + } else { + exploded = (coll instanceof List) ? (List) coll : new ArrayList<>(coll); + } - if (result.size() > 1) { - result.sort(serviceProviderComparator()); - } - - return result; + List result; + if (criteria.includeIntercepted()) { + result = exploded; } else { - result = (coll instanceof List) ? (List) coll : new ArrayList<>(coll); + result = (List) exploded.stream() + .filter(sp -> !(sp instanceof AbstractServiceProvider) || !((AbstractServiceProvider) sp).isIntercepted()) + .collect(Collectors.toList()); } if (expected && result.isEmpty()) { throw resolutionBasedInjectionError(criteria); } + if (result.size() > 1) { + result.sort(serviceProviderComparator()); + } + return result; } @@ -361,7 +367,7 @@ List> lookup(ServiceInfoCriteria criteria, if (serviceTypeName != null) { ServiceProvider exact = servicesByTypeName.get(serviceTypeName); if (exact != null && !isIntercepted(exact)) { - return explodeAndSort(List.of(exact), criteria, expected); + return explodeFilterAndSort(List.of(exact), criteria, expected); } } if (hasOneContractInCriteria) { @@ -372,7 +378,7 @@ List> lookup(ServiceInfoCriteria criteria, .limit(limit) .collect(Collectors.toList()); if (!result.isEmpty()) { - return explodeAndSort(result, criteria, expected); + return explodeFilterAndSort(result, criteria, expected); } } } @@ -398,7 +404,7 @@ List> lookup(ServiceInfoCriteria criteria, } if (!result.isEmpty()) { - result = explodeAndSort(result, criteria, expected); + result = explodeFilterAndSort(result, criteria, expected); } if (cfg.serviceLookupCaching()) { @@ -418,7 +424,7 @@ ServiceProvider serviceProviderFor(String serviceTypeName) { List> allServiceProviders(boolean explode) { if (explode) { - return explodeAndSort(servicesByTypeName.values(), null, false); + return explodeFilterAndSort(servicesByTypeName.values(), PicoServices.EMPTY_CRITERIA, false); } return new ArrayList<>(servicesByTypeName.values()); diff --git a/pico/services/src/main/java/io/helidon/pico/services/Invocation.java b/pico/services/src/main/java/io/helidon/pico/services/Invocation.java index 960a719f624..3762c303ec2 100644 --- a/pico/services/src/main/java/io/helidon/pico/services/Invocation.java +++ b/pico/services/src/main/java/io/helidon/pico/services/Invocation.java @@ -47,6 +47,11 @@ private Invocation(InvocationContext ctx, this.interceptorIterator = ctx.interceptors().listIterator(); } + @Override + public String toString() { + return String.valueOf(ctx.elementInfo()); + } + /** * Creates an instance of {@link Invocation} and invokes it in this context. * diff --git a/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java b/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java index ea735e6df10..e13589fd305 100644 --- a/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java +++ b/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java @@ -24,7 +24,7 @@ import io.helidon.pico.DefaultBootstrap; import io.helidon.pico.PicoServices; import io.helidon.pico.PicoServicesConfig; -import io.helidon.pico.services.testsubjects.HelloPicoApplication; +import io.helidon.pico.services.testsubjects.HelloPico$$Application; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -50,7 +50,7 @@ void setUp() { @AfterEach void tearDown() { - HelloPicoApplication.ENABLED = true; + HelloPico$$Application.ENABLED = true; SimplePicoTestingSupport.resetAll(); } diff --git a/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java b/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java index 75b651198c2..2d407029924 100644 --- a/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java +++ b/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java @@ -38,7 +38,7 @@ import io.helidon.pico.ServiceInfo; import io.helidon.pico.ServiceProvider; import io.helidon.pico.Services; -import io.helidon.pico.services.testsubjects.HelloPicoApplication; +import io.helidon.pico.services.testsubjects.HelloPico$$Application; import io.helidon.pico.services.testsubjects.HelloPicoImpl$$picoActivator; import io.helidon.pico.services.testsubjects.HelloPicoWorld; import io.helidon.pico.services.testsubjects.HelloPicoWorldImpl; @@ -82,7 +82,7 @@ void setUp() { @AfterEach void tearDown() { - HelloPicoApplication.ENABLED = true; + HelloPico$$Application.ENABLED = true; SimplePicoTestingSupport.resetAll(); } @@ -95,18 +95,18 @@ void sanity() { equalTo(EXPECTED_MODULES)); List descriptions = ServiceUtils.toDescriptions(moduleProviders); assertThat(descriptions, - containsInAnyOrder("EmptyModule:ACTIVE", "HelloPicoModule:ACTIVE")); + containsInAnyOrder("EmptyModule:ACTIVE", "HelloPico$$Module:ACTIVE")); List> applications = services.lookupAll(Application.class); assertThat(applications.size(), equalTo(1)); assertThat(ServiceUtils.toDescriptions(applications), - containsInAnyOrder("HelloPicoApplication:ACTIVE")); + containsInAnyOrder("HelloPico$$Application:ACTIVE")); } @Test void standardActivationWithNoApplicationEnabled() { - HelloPicoApplication.ENABLED = false; + HelloPico$$Application.ENABLED = false; Optional picoServices = PicoServices.picoServices(); ((DefaultPicoServices) picoServices.orElseThrow()).reset(true); diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoApplication.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPico$$Application.java similarity index 94% rename from pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoApplication.java rename to pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPico$$Application.java index e17fd480232..9a497239ac4 100644 --- a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoApplication.java +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPico$$Application.java @@ -30,13 +30,13 @@ */ @Generated(value = "example", comments = "API Version: n") @Singleton -@Named(HelloPicoApplication.NAME) -public class HelloPicoApplication implements Application { +@Named(HelloPico$$Application.NAME) +public class HelloPico$$Application implements Application { public static boolean ENABLED = true; static final String NAME = "HelloPicoApplication"; - public HelloPicoApplication() { + public HelloPico$$Application() { assert(true); // for setting breakpoints in debug } diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoModule.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPico$$Module.java similarity index 91% rename from pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoModule.java rename to pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPico$$Module.java index bb7495f58c0..aef3ec48219 100644 --- a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoModule.java +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPico$$Module.java @@ -27,12 +27,12 @@ @Generated(value = "example", comments = "API Version: n") @Singleton -@Named(HelloPicoModule.NAME) -public final class HelloPicoModule implements Module { +@Named(HelloPico$$Module.NAME) +public final class HelloPico$$Module implements Module { public static final String NAME = "example"; - public HelloPicoModule() { + public HelloPico$$Module() { } @Override diff --git a/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application index 8980ce4399a..18e14235d0b 100644 --- a/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application +++ b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application @@ -14,4 +14,4 @@ # limitations under the License. # -io.helidon.pico.services.testsubjects.HelloPicoApplication +io.helidon.pico.services.testsubjects.HelloPico$$Application diff --git a/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module index e07a24ff52e..912a5bd39ac 100644 --- a/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module +++ b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module @@ -15,4 +15,4 @@ # io.helidon.pico.services.testsubjects.EmptyModule -io.helidon.pico.services.testsubjects.HelloPicoModule +io.helidon.pico.services.testsubjects.HelloPico$$Module diff --git a/pico/tests/resources-pico/pom.xml b/pico/tests/resources-pico/pom.xml index 79430604505..d8531585c84 100644 --- a/pico/tests/resources-pico/pom.xml +++ b/pico/tests/resources-pico/pom.xml @@ -93,7 +93,7 @@ -Apico.autoAddNonContractInterfaces=true - -Apico.allowListedInterceptorAnnotations=jakarta.inject.Named + -Apico.allowListedInterceptorAnnotations=io.helidon.pico.tests.pico.interceptor.TestNamed -Apico.application.pre.create=true -Apico.mapApplicationToSingletonScope=true -Apico.debug=${pico.debug} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/TestNamed.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/TestNamed.java new file mode 100644 index 00000000000..cb06dac58cc --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/TestNamed.java @@ -0,0 +1,37 @@ +/* + * 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.tests.pico.interceptor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used as an {@link io.helidon.pico.InterceptedTrigger} from the maven-plugin call (see pom.xml). + */ +//@InterceptedTrigger - intentional decision not to add this in order to avoid standard annotation processing. +// it will instead be handled by the maven-plugin +@Retention(RetentionPolicy.CLASS) +public @interface TestNamed { + + /** + * For testing only. + * + * @return for testing only + */ + String value(); + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java index 000f907c86c..a5cfd14c882 100644 --- a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java @@ -29,41 +29,67 @@ import jakarta.inject.Named; import jakarta.inject.Singleton; +/** + * This test case is applying {@link InterceptorBasedAnno} (an {@link io.helidon.pico.InterceptedTrigger}) using the no-arg + * constructor approach - all methods are intercepted. + *

+ * Also note that interception was triggered by the presence of the {@link TestNamed} and {@link InterceptorBasedAnno} triggers. + */ @Singleton @Named("ClassX") +@TestNamed("TestNamed-ClassX") @ExternalContracts(value = Closeable.class, moduleNames = {"test1", "test2"}) +@SuppressWarnings("unused") public class XImpl implements IA, IB, Closeable { XImpl() { - // this is the one that will be used by interception } @Inject - public XImpl(Optional optionalIA) { + // will be intercepted + XImpl(Optional optionalIA) { assert (optionalIA.isEmpty()); } + // a decoy constructor (and will not be intercepted) + @InterceptorBasedAnno("IA2") + XImpl(IB ib) { + throw new IllegalStateException("should not be here"); + } + @Override + // will be intercepted public void methodIA1() { } @InterceptorBasedAnno("IA2") @Override + // will be intercepted public void methodIA2() { } @Named("methodIB") @InterceptorBasedAnno("IBSubAnno") @Override + // will be intercepted public void methodIB(@Named("arg1") String val) { } + @Named("methodIB2") + @InterceptorBasedAnno("IBSubAnno") + @Override + // will be intercepted + public String methodIB2(@Named("arg1") String val) { + return val; + } + @InterceptorBasedAnno @Override public void close() throws IOException, RuntimeException { throw new IOException("forced"); } + // will be intercepted public long methodX(String arg1, int arg2, boolean arg3) throws IOException, RuntimeException, AssertionError { @@ -71,16 +97,19 @@ public long methodX(String arg1, } // test of package private + // will be intercepted String methodY() { return "methodY"; } // test of protected + // will be intercepted protected String methodZ() { return "methodZ"; } // test of protected + // will be intercepted protected void throwRuntimeException() { throw new RuntimeException("forced"); } diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/YImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/YImpl.java new file mode 100644 index 00000000000..9720f9d0f61 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/YImpl.java @@ -0,0 +1,104 @@ +/* + * 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.tests.pico.interceptor; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Optional; + +import io.helidon.pico.ExternalContracts; +import io.helidon.pico.tests.plain.interceptor.IA; +import io.helidon.pico.tests.plain.interceptor.IB; +import io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/** + * This test case is applying {@link InterceptorBasedAnno} (an {@link io.helidon.pico.InterceptedTrigger}) on only + * the {@link IB} interface. + *

+ * Also note that interception was triggered by the presence of the {@link InterceptorBasedAnno} trigger. + */ +@Singleton +@Named("ClassY") +@ExternalContracts(value = Closeable.class, moduleNames = {"test1", "test2"}) +@SuppressWarnings("unused") +public class YImpl implements IB, Closeable { + + // intentionally w/o a default constructor - do not uncomment +// YImpl() { +// } + + @Inject + // will be intercepted + YImpl(Optional optionalIA) { + assert (optionalIA.isPresent() && optionalIA.get().getClass().getName().contains("XImpl")); + } + + // a decoy constructor (and will not be intercepted) + @InterceptorBasedAnno("IA2") + YImpl(IB ib) { + throw new IllegalStateException("should not be here"); + } + + @Named("methodIB") + @InterceptorBasedAnno("IBSubAnno") + @Override + // will be intercepted + public void methodIB(@Named("arg1") String val) { + } + + @Named("methodIB2") + @InterceptorBasedAnno("IBSubAnno") + @Override + // will be intercepted + public String methodIB2(@Named("arg1") String val) { + return val; + } + + @InterceptorBasedAnno + @Override + // will be intercepted + public void close() throws IOException, RuntimeException { + throw new IOException("forced"); + } + + // will not be intercepted + public long methodX(String arg1, + int arg2, + boolean arg3) throws IOException, RuntimeException, AssertionError { + return 101; + } + + // will not be intercepted + String methodY() { + return "methodY"; + } + + // will not be intercepted + protected String methodZ() { + return "methodZ"; + } + + // will not be intercepted + protected void throwRuntimeException() { + throw new RuntimeException("forced"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java index 3d3cf385d2a..37d8a361be0 100644 --- a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java @@ -61,8 +61,7 @@ public Optional first(ContextualServiceQuery query) { assert (injected != null); assert (postConstructed); int num = counter++; - String id = getClass().getSimpleName() + ":instance_" + num + ", " - + query + ", " + injected; + String id = getClass().getSimpleName() + ":instance_" + num + ", " + injected; return Optional.of(new MyConcreteClassContract(id)); } diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/ComplexInterceptorTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/ComplexInterceptorTest.java deleted file mode 100644 index 6254811fd9b..00000000000 --- a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/ComplexInterceptorTest.java +++ /dev/null @@ -1,175 +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.tests.pico.interceptor; - -import java.io.File; -import java.nio.file.Files; -import java.util.List; -import java.util.Map; - -import io.helidon.common.types.DefaultTypeName; -import io.helidon.common.types.TypeName; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.pico.DefaultQualifierAndValue; -import io.helidon.pico.DefaultServiceInfo; -import io.helidon.pico.Interceptor; -import io.helidon.pico.PicoException; -import io.helidon.pico.PicoServices; -import io.helidon.pico.ServiceProvider; -import io.helidon.pico.Services; -import io.helidon.pico.testing.ReflectionBasedSingletonServiceProvider; -import io.helidon.pico.tests.plain.interceptor.IA; -import io.helidon.pico.tests.plain.interceptor.IB; -import io.helidon.pico.tests.plain.interceptor.NamedInterceptor; - -import jakarta.inject.Named; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static io.helidon.pico.PicoServicesConfig.KEY_PERMITS_DYNAMIC; -import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_APPLICATIONS; -import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_MODULES; -import static io.helidon.pico.PicoServicesConfig.NAME; -import static io.helidon.pico.testing.PicoTestingSupport.basicTestableConfig; -import static io.helidon.pico.testing.PicoTestingSupport.bind; -import static io.helidon.pico.testing.PicoTestingSupport.resetAll; -import static io.helidon.pico.testing.PicoTestingSupport.testableServices; -import static io.helidon.pico.testing.PicoTestingSupport.toDescriptions; -import static io.helidon.pico.tests.pico.TestUtils.loadStringFromResource; -import static io.helidon.pico.tools.TypeTools.toFilePath; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class ComplexInterceptorTest { - - Config config = basicTestableConfig(); - PicoServices picoServices; - Services services; - - @BeforeEach - void setUp() { - setUp(config); - } - - void setUp(Config config) { - this.picoServices = testableServices(config); - this.services = picoServices.services(); - } - - @AfterEach - void tearDown() { - resetAll(); - } - - @Test - void createInterceptorSource() throws Exception { - TypeName interceptorTypeName = DefaultTypeName.create(XImpl$$Pico$$Interceptor.class); - String path = toFilePath(interceptorTypeName); - File file = new File("./target/generated-sources/annotations", path); - assertThat(file.exists(), is(true)); - String java = Files.readString(file.toPath()); - assertEquals(loadStringFromResource("expected/ximpl-interceptor._java_"), - java); - } - - @Test - void runtimeWithNoInterception() throws Exception { - List> iaProviders = services.lookupAll(IA.class); - assertThat(toDescriptions(iaProviders), - contains("XImpl$$Pico$$Interceptor:INIT", "XImpl:INIT")); - - List> ibProviders = services.lookupAll(IB.class); - assertThat(iaProviders, equalTo(ibProviders)); - - ServiceProvider ximplProvider = services.lookupFirst(XImpl.class); - assertThat(iaProviders.get(0), is(ximplProvider)); - - XImpl x = ximplProvider.get(); - x.methodIA1(); - x.methodIA2(); - x.methodIB("test"); - long val = x.methodX("a", 2, true); - assertThat(val, equalTo(101L)); - assertThat(x.methodY(), equalTo("methodY")); - assertThat(x.methodZ(), equalTo("methodZ")); - PicoException pe = assertThrows(PicoException.class, x::close); - assertThat(pe.getMessage(), - equalTo("forced: service provider: XImpl:ACTIVE")); - RuntimeException re = assertThrows(RuntimeException.class, x::throwRuntimeException); - assertThat(re.getMessage(), equalTo("forced")); - } - - @Test - void runtimeWithInterception() throws Exception { - // disable application and modules to affectively start with an empty registry. - Config config = Config.builder( - ConfigSources.create( - Map.of(NAME + "." + KEY_PERMITS_DYNAMIC, "true", - NAME + "." + KEY_USES_COMPILE_TIME_APPLICATIONS, "false", - NAME + "." + KEY_USES_COMPILE_TIME_MODULES, "true"), - "config-1")) - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .build(); - tearDown(); - setUp(config); - bind(picoServices, ReflectionBasedSingletonServiceProvider - .create(NamedInterceptor.class, - DefaultServiceInfo.builder() - .serviceTypeName(NamedInterceptor.class.getName()) - .addQualifier(DefaultQualifierAndValue.createNamed(Named.class.getName())) - .addExternalContractsImplemented(Interceptor.class.getName()) - .build())); - assertThat(NamedInterceptor.ctorCount.get(), equalTo(0)); - - List> iaProviders = picoServices.services().lookupAll(IA.class); - assertThat(toDescriptions(iaProviders), - contains("XImpl$$Pico$$Interceptor:INIT", "XImpl:INIT")); - - List> ibProviders = services.lookupAll(IB.class); - assertThat(iaProviders, equalTo(ibProviders)); - - ServiceProvider ximplProvider = services.lookupFirst(XImpl.class); - assertThat(iaProviders.get(0), is(ximplProvider)); - - assertThat(NamedInterceptor.ctorCount.get(), equalTo(0)); - XImpl xIntercepted = ximplProvider.get(); - assertThat(NamedInterceptor.ctorCount.get(), equalTo(1)); - - xIntercepted.methodIA1(); - xIntercepted.methodIA2(); - xIntercepted.methodIB("test"); - long val = xIntercepted.methodX("a", 2, true); - assertThat(val, equalTo(202L)); - assertThat(xIntercepted.methodY(), equalTo("methodY")); - assertThat(xIntercepted.methodZ(), equalTo("methodZ")); - PicoException pe = assertThrows(PicoException.class, xIntercepted::close); - assertThat(pe.getMessage(), - equalTo("forced: service provider: XImpl:ACTIVE")); - RuntimeException re = assertThrows(RuntimeException.class, xIntercepted::throwRuntimeException); - assertThat(re.getMessage(), equalTo("forced")); - - assertThat(NamedInterceptor.ctorCount.get(), equalTo(1)); - } - -} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/InterceptorRuntimeTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/InterceptorRuntimeTest.java new file mode 100644 index 00000000000..52f92114846 --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/InterceptorRuntimeTest.java @@ -0,0 +1,259 @@ +/* + * 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.tests.pico.interceptor; + +import java.io.Closeable; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Interceptor; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.testing.ReflectionBasedSingletonServiceProvider; +import io.helidon.pico.tests.plain.interceptor.IB; +import io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno; +import io.helidon.pico.tests.plain.interceptor.TestNamedInterceptor; + +import jakarta.inject.Named; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.DefaultQualifierAndValue.create; +import static io.helidon.pico.DefaultQualifierAndValue.createNamed; +import static io.helidon.pico.PicoServicesConfig.KEY_PERMITS_DYNAMIC; +import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_APPLICATIONS; +import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_MODULES; +import static io.helidon.pico.PicoServicesConfig.NAME; +import static io.helidon.pico.testing.PicoTestingSupport.basicTestableConfig; +import static io.helidon.pico.testing.PicoTestingSupport.bind; +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static io.helidon.pico.testing.PicoTestingSupport.toDescription; +import static io.helidon.pico.testing.PicoTestingSupport.toDescriptions; +import static io.helidon.pico.tests.pico.TestUtils.loadStringFromResource; +import static io.helidon.pico.tools.TypeTools.toFilePath; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class InterceptorRuntimeTest { + + Config config = basicTestableConfig(); + PicoServices picoServices; + Services services; + + @BeforeEach + void setUp() { + setUp(config); + } + + void setUp(Config config) { + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @AfterEach + void tearDown() { + resetAll(); + } + + @Test + void createNoArgBasedInterceptorSource() throws Exception { + TypeName interceptorTypeName = DefaultTypeName.create(XImpl$$Pico$$Interceptor.class); + String path = toFilePath(interceptorTypeName); + File file = new File("./target/generated-sources/annotations", path); + assertThat(file.exists(), is(true)); + String java = Files.readString(file.toPath()); + assertEquals(loadStringFromResource("expected/ximpl-interceptor._java_"), + java); + } + + @Test + void createInterfaceBasedInterceptorSource() throws Exception { + TypeName interceptorTypeName = DefaultTypeName.create(YImpl$$Pico$$Interceptor.class); + String path = toFilePath(interceptorTypeName); + File file = new File("./target/generated-sources/annotations", path); + assertThat(file.exists(), is(true)); + String java = Files.readString(file.toPath()); + assertEquals(loadStringFromResource("expected/yimpl-interceptor._java_"), + java); + } + + @Test + void runtimeWithNoInterception() throws Exception { + ServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(Closeable.class.getName()) + .includeIntercepted(true) + .build(); + List> closeableProviders = services.lookupAll(criteria); + assertThat("the interceptors should always be weighted higher than the non-interceptors", + toDescriptions(closeableProviders), + contains("XImpl$$Pico$$Interceptor:INIT", "YImpl$$Pico$$Interceptor:INIT", + "XImpl:INIT", "YImpl:INIT")); + + criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(Closeable.class.getName()) + .includeIntercepted(false) + .build(); + closeableProviders = services.lookupAll(criteria); + assertThat("the interceptors should always be weighted higher than the non-interceptors", + toDescriptions(closeableProviders), + contains("XImpl$$Pico$$Interceptor:INIT", "YImpl$$Pico$$Interceptor:INIT")); + + List> ibProviders = services.lookupAll(IB.class); + assertThat(closeableProviders, + equalTo(ibProviders)); + + ServiceProvider ximplProvider = services.lookupFirst(XImpl.class); + assertThat(closeableProviders.get(0), + is(ximplProvider)); + + XImpl x = ximplProvider.get(); + x.methodIA1(); + x.methodIA2(); + x.methodIB("test"); + String sval = x.methodIB2("test"); + assertThat(sval, + equalTo("test")); + long val = x.methodX("a", 2, true); + assertThat(val, + equalTo(101L)); + assertThat(x.methodY(), + equalTo("methodY")); + assertThat(x.methodZ(), + equalTo("methodZ")); + PicoException pe = assertThrows(PicoException.class, x::close); + assertThat(pe.getMessage(), + equalTo("forced: service provider: XImpl:ACTIVE")); + RuntimeException re = assertThrows(RuntimeException.class, x::throwRuntimeException); + assertThat(re.getMessage(), + equalTo("forced")); + + // we cannot look up by service type here - we need to instead lookup by one of the interfaces + ServiceProvider yimplProvider = services + .lookupFirst( + DefaultServiceInfoCriteria.builder() + .addContractImplemented(Closeable.class.getName()) + .qualifiers(Set.of(create(Named.class, "ClassY"))) + .build()); + assertThat(toDescription(yimplProvider), + equalTo("YImpl$$Pico$$Interceptor:INIT")); + IB ibOnYInterceptor = (IB)yimplProvider.get(); + sval = ibOnYInterceptor.methodIB2("test"); + assertThat(sval, + equalTo("test")); + } + + @Test + void runtimeWithInterception() throws Exception { + // disable application and modules to effectively start with an empty registry + Config config = Config.builder( + ConfigSources.create( + Map.of(NAME + "." + KEY_PERMITS_DYNAMIC, "true", + NAME + "." + KEY_USES_COMPILE_TIME_APPLICATIONS, "false", + NAME + "." + KEY_USES_COMPILE_TIME_MODULES, "true"), + "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + tearDown(); + setUp(config); + bind(picoServices, ReflectionBasedSingletonServiceProvider + .create(TestNamedInterceptor.class, + DefaultServiceInfo.builder() + .serviceTypeName(TestNamedInterceptor.class.getName()) + .addQualifier(createNamed(TestNamed.class.getName())) + .addQualifier(createNamed(InterceptorBasedAnno.class.getName())) + .addExternalContractsImplemented(Interceptor.class.getName()) + .build())); + assertThat(TestNamedInterceptor.ctorCount.get(), + equalTo(0)); + + List> closeableProviders = picoServices.services().lookupAll(Closeable.class); + assertThat("the interceptors should always be weighted higher than the non-interceptors", + toDescriptions(closeableProviders), + contains("XImpl$$Pico$$Interceptor:INIT", "YImpl$$Pico$$Interceptor:INIT")); + + List> ibProviders = services.lookupAll(IB.class); + assertThat(closeableProviders, + equalTo(ibProviders)); + + ServiceProvider ximplProvider = services.lookupFirst(XImpl.class); + assertThat(closeableProviders.get(0), + is(ximplProvider)); + + assertThat(TestNamedInterceptor.ctorCount.get(), + equalTo(0)); + XImpl xIntercepted = ximplProvider.get(); + assertThat(TestNamedInterceptor.ctorCount.get(), + equalTo(1)); + + xIntercepted.methodIA1(); + xIntercepted.methodIA2(); + xIntercepted.methodIB("test"); + String sval = xIntercepted.methodIB2("test"); + assertThat(sval, + equalTo("intercepted:test")); + long val = xIntercepted.methodX("a", 2, true); + assertThat(val, + equalTo(202L)); + assertThat(xIntercepted.methodY(), + equalTo("intercepted:methodY")); + assertThat(xIntercepted.methodZ(), + equalTo("intercepted:methodZ")); + PicoException pe = assertThrows(PicoException.class, xIntercepted::close); + assertThat(pe.getMessage(), + equalTo("forced: service provider: XImpl:ACTIVE")); + RuntimeException re = assertThrows(RuntimeException.class, xIntercepted::throwRuntimeException); + assertThat(re.getMessage(), + equalTo("forced")); + + assertThat(TestNamedInterceptor.ctorCount.get(), + equalTo(1)); + + // we cannot look up by service type here - we need to instead lookup by one of the interfaces + ServiceProvider yimplProvider = services + .lookupFirst( + DefaultServiceInfoCriteria.builder() + .addContractImplemented(Closeable.class.getName()) + .qualifiers(Set.of(create(Named.class, "ClassY"))) + .build()); + assertThat(toDescription(yimplProvider), + equalTo("YImpl$$Pico$$Interceptor:INIT")); + IB ibOnYInterceptor = (IB) yimplProvider.get(); + sval = ibOnYInterceptor.methodIB2("test"); + assertThat(sval, + equalTo("intercepted:test")); + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java index 403fb36489b..aed40d4eac1 100644 --- a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java @@ -59,19 +59,11 @@ void myConcreteClassContractTest() { equalTo("MyServices$MyConcreteClassContractPerRequestIPProvider:INIT")); MyConcreteClassContract instance0 = sp.get(); assertThat(instance0.toString(), - equalTo("MyConcreteClassContractPerRequestIPProvider:instance_0, ContextualServiceQuery" - + "(serviceInfoCriteria=ServiceInfoCriteria(serviceTypeName=Optional.empty, " - + "scopeTypeNames=[], qualifiers=[], contractsImplemented=[], runLevel=Optional.empty, " - + "weight=Optional.empty, externalContractsImplemented=[], activatorTypeName=Optional.empty," - + " moduleName=Optional.empty), injectionPointInfo=Optional.empty, expected=true), " + equalTo("MyConcreteClassContractPerRequestIPProvider:instance_0, " + "MyConcreteClassContractPerRequestProvider:instance_0")); MyConcreteClassContract instance1 = sp.get(); assertThat(instance1.toString(), - equalTo("MyConcreteClassContractPerRequestIPProvider:instance_1, ContextualServiceQuery" - + "(serviceInfoCriteria=ServiceInfoCriteria(serviceTypeName=Optional.empty, " - + "scopeTypeNames=[], qualifiers=[], contractsImplemented=[], runLevel=Optional.empty, " - + "weight=Optional.empty, externalContractsImplemented=[], activatorTypeName=Optional.empty," - + " moduleName=Optional.empty), injectionPointInfo=Optional.empty, expected=true), " + equalTo("MyConcreteClassContractPerRequestIPProvider:instance_1, " + "MyConcreteClassContractPerRequestProvider:instance_0")); } 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 acd0c1ccd99..a59f5a50deb 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 @@ -236,18 +236,22 @@ void hierarchyOfInjections() { */ @Test void runlevel() { - // we start with 1 because we are looking for interceptors (which there is one here in this module) - assertThat(picoServices.metrics().orElseThrow().lookupCount().orElseThrow(), equalTo(1)); + assertThat("we start with 2 because we are looking for interceptors (which there is 2 here in this module)", + picoServices.metrics().orElseThrow().lookupCount().orElseThrow(), + equalTo(2)); List> runLevelServices = services .lookupAll(DefaultServiceInfoCriteria.builder().runLevel(RunLevel.STARTUP).build(), true); List desc = runLevelServices.stream().map(ServiceProvider::description).collect(Collectors.toList()); - assertThat(desc, contains("TestingSingleton:INIT")); + assertThat(desc, + contains("TestingSingleton:INIT")); runLevelServices.forEach(sp -> Objects.requireNonNull(sp.get(), sp + " failed on get()")); - assertThat("activation should not have triggered any new lookups other than the startup we just did", - picoServices.metrics().orElseThrow().lookupCount().orElseThrow(), equalTo(2)); + assertThat("activation should not triggered one new lookup from startup", + picoServices.metrics().orElseThrow().lookupCount().orElseThrow(), + equalTo(3)); desc = runLevelServices.stream().map(ServiceProvider::description).collect(Collectors.toList()); - assertThat(desc, contains("TestingSingleton:ACTIVE")); + assertThat(desc, + contains("TestingSingleton:ACTIVE")); } /** diff --git a/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ b/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ index c5534330498..0b5c816a26d 100644 --- a/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ +++ b/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ @@ -29,8 +29,9 @@ import static io.helidon.pico.services.Invocation.createInvokeAndSupply; import static io.helidon.pico.services.Invocation.mergeAndCollapse; /** - * Pico {@link Interceptor} manager for {@link io.helidon.pico.tests.pico.interceptor.XImpl }. + * Pico {@link Interceptor} for {@link io.helidon.pico.tests.pico.interceptor.XImpl }. */ +// using the no-arg constructor approach @io.helidon.common.Weight(100.001) @io.helidon.pico.Intercepted(io.helidon.pico.tests.pico.interceptor.XImpl.class) @Singleton @@ -40,61 +41,92 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce private static final List __serviceLevelAnnotations = List.of( DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class), DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX")), - DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))); + DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX")), + DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable")), + DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))); private static final TypedElementName __ctor = DefaultTypedElementName.builder() .typeName(create(void.class)) .elementName(io.helidon.pico.ElementInfo.CONSTRUCTOR) .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Inject.class)) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodIA1 = DefaultTypedElementName.builder() .typeName(create(void.class)) .elementName("methodIA1") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodIA2 = DefaultTypedElementName.builder() .typeName(create(void.class)) .elementName("methodIA2") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IA2"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodIB = DefaultTypedElementName.builder() .typeName(create(void.class)) .elementName("methodIB") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IBSubAnno"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "methodIB"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodIB__p1 = DefaultTypedElementName.builder() .typeName(create(java.lang.String.class)) .elementName("p1") .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) .build(); + private static final TypedElementName __methodIB2 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("methodIB2") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IBSubAnno"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "methodIB2"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) + .build(); + private static final TypedElementName __methodIB2__p1 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("p1") + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) + .build(); private static final TypedElementName __close = DefaultTypedElementName.builder() .typeName(create(void.class)) .elementName("close") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class)) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodX = DefaultTypedElementName.builder() .typeName(create(long.class)) .elementName("methodX") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodX__p1 = DefaultTypedElementName.builder() .typeName(create(java.lang.String.class)) @@ -112,31 +144,39 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .typeName(create(java.lang.String.class)) .elementName("methodY") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __methodZ = DefaultTypedElementName.builder() .typeName(create(java.lang.String.class)) .elementName("methodZ") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); private static final TypedElementName __throwRuntimeException = DefaultTypedElementName.builder() .typeName(create(void.class)) .elementName("throwRuntimeException") .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.pico.interceptor.TestNamed.class, Map.of("value", "TestNamed-ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); + private static final TypeName __serviceTypeName = DefaultTypeName.create(io.helidon.pico.tests.pico.interceptor.XImpl.class); + private final Provider __provider; private final ServiceProvider __sp; private final io.helidon.pico.tests.pico.interceptor.XImpl __impl; - private final TypeName __serviceTypeName; private final List> __methodIA1__interceptors; private final List> __methodIA2__interceptors; private final List> __methodIB__interceptors; + private final List> __methodIB2__interceptors; private final List> __close__interceptors; private final List> __methodX__interceptors; private final List> __methodY__interceptors; @@ -145,6 +185,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce private final InterceptedMethod __methodIA1__call; private final InterceptedMethod __methodIA2__call; private final InterceptedMethod __methodIB__call; + private final InterceptedMethod __methodIB2__call; private final InterceptedMethod __close__call; private final InterceptedMethod __methodX__call; private final InterceptedMethod __methodY__call; @@ -153,21 +194,21 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce @Inject XImpl$$Pico$$Interceptor( + @Named("io.helidon.pico.tests.pico.interceptor.TestNamed") List> io_helidon_pico_tests_pico_interceptor_TestNamed, @Named("io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno") List> io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, - @Named("jakarta.inject.Named") List> jakarta_inject_Named, Provider provider) { this.__provider = Objects.requireNonNull(provider); this.__sp = (provider instanceof ServiceProvider) ? (ServiceProvider) __provider : null; - this.__serviceTypeName = DefaultTypeName.create(io.helidon.pico.tests.pico.interceptor.XImpl.class); - List> __ctor__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); - this.__methodIA1__interceptors = mergeAndCollapse(jakarta_inject_Named); - this.__methodIA2__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); - this.__methodIB__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); - this.__close__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); - this.__methodX__interceptors = mergeAndCollapse(jakarta_inject_Named); - this.__methodY__interceptors = mergeAndCollapse(jakarta_inject_Named); - this.__methodZ__interceptors = mergeAndCollapse(jakarta_inject_Named); - this.__throwRuntimeException__interceptors = mergeAndCollapse(jakarta_inject_Named); + List> __ctor__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed, io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__methodIA1__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed); + this.__methodIA2__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed, io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__methodIB__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed, io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__methodIB2__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed, io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__close__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed, io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__methodX__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed); + this.__methodY__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed); + this.__methodZ__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed); + this.__throwRuntimeException__interceptors = mergeAndCollapse(io_helidon_pico_tests_pico_interceptor_TestNamed); Supplier call = __provider::get; io.helidon.pico.tests.pico.interceptor.XImpl result = createInvokeAndSupply( @@ -177,7 +218,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .classAnnotations(__serviceLevelAnnotations) .elementInfo(__ctor) .interceptors(__ctor__interceptors) - /*.build()*/, + .build(), call); this.__impl = Objects.requireNonNull(result); @@ -209,6 +250,15 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce } }; + this.__methodIB2__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB2__interceptors, __methodIB2, + new TypedElementName[] {__methodIB2__p1}) { + @Override + public java.lang.String invoke(Object... args) throws Throwable { + return impl().methodIB2((java.lang.String) args[0]); + } + }; + this.__close__call = new InterceptedMethod( __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __close__interceptors, __close) { @Override @@ -268,6 +318,11 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce createInvokeAndSupply(__methodIB__call.ctx(), () -> __methodIB__call.apply(p1)); } + @Override + public java.lang.String methodIB2(java.lang.String p1) { + return createInvokeAndSupply(__methodIB2__call.ctx(), () -> __methodIB2__call.apply(p1)); + } + @Override public void close() throws java.io.IOException, java.lang.RuntimeException { createInvokeAndSupply(__close__call.ctx(), () -> __close__call.apply()); diff --git a/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ b/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ new file mode 100644 index 00000000000..f8938d9ff80 --- /dev/null +++ b/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ @@ -0,0 +1,176 @@ +// This is a generated file (powered by Helidon). Do not edit or extend from this artifact as it is subject to change at any time! + +package io.helidon.pico.tests.pico.interceptor; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.DefaultTypedElementName; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.DefaultInvocationContext; +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationException; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.services.InterceptedMethod; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.pico.services.Invocation.createInvokeAndSupply; +import static io.helidon.pico.services.Invocation.mergeAndCollapse; + +/** + * Pico {@link Interceptor} for {@link io.helidon.pico.tests.pico.interceptor.YImpl }. + */ +// using the interfaces approach +@io.helidon.common.Weight(100.001) +@io.helidon.pico.Intercepted(io.helidon.pico.tests.pico.interceptor.YImpl.class) +@Singleton +@SuppressWarnings("ALL") +@jakarta.annotation.Generated(value = "io.helidon.pico.tools.DefaultInterceptorCreator", comments = "version=1") +public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.interceptor.YImpl */ implements io.helidon.pico.tests.plain.interceptor.IB, java.io.Closeable, java.lang.AutoCloseable { + private static final List __serviceLevelAnnotations = List.of( + DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class), + DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassY")), + DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable")), + DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))); + + private static final TypedElementName __ctor = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName(io.helidon.pico.ElementInfo.CONSTRUCTOR) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Inject.class)) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassY"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) + .build(); + private static final TypedElementName __methodIB = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("methodIB") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IBSubAnno"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "methodIB"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) + .build(); + private static final TypedElementName __methodIB__p1 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("p1") + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) + .build(); + private static final TypedElementName __methodIB2 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("methodIB2") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IBSubAnno"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "methodIB2"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) + .build(); + private static final TypedElementName __methodIB2__p1 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("p1") + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) + .build(); + private static final TypedElementName __close = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("close") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class)) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassY"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) + .build(); + + private static final TypeName __serviceTypeName = DefaultTypeName.create(io.helidon.pico.tests.pico.interceptor.YImpl.class); + + private final Provider __provider; + private final ServiceProvider __sp; + private final io.helidon.pico.tests.pico.interceptor.YImpl __impl; + private final List> __methodIB__interceptors; + private final List> __methodIB2__interceptors; + private final List> __close__interceptors; + private final InterceptedMethod __methodIB__call; + private final InterceptedMethod __methodIB2__call; + private final InterceptedMethod __close__call; + + @Inject + YImpl$$Pico$$Interceptor( + @Named("io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno") List> io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, + Provider provider) { + this.__provider = Objects.requireNonNull(provider); + this.__sp = (provider instanceof ServiceProvider) ? (ServiceProvider) __provider : null; + List> __ctor__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__methodIB__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__methodIB2__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + this.__close__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno); + + Supplier call = __provider::get; + io.helidon.pico.tests.pico.interceptor.YImpl result = createInvokeAndSupply( + DefaultInvocationContext.builder() + .serviceProvider(__sp) + .serviceTypeName(__serviceTypeName) + .classAnnotations(__serviceLevelAnnotations) + .elementInfo(__ctor) + .interceptors(__ctor__interceptors) + .build(), + call); + this.__impl = Objects.requireNonNull(result); + + this.__methodIB__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB__interceptors, __methodIB, + new TypedElementName[] {__methodIB__p1}) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().methodIB((java.lang.String) args[0]); + return null; + } + }; + + this.__methodIB2__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB2__interceptors, __methodIB2, + new TypedElementName[] {__methodIB2__p1}) { + @Override + public java.lang.String invoke(Object... args) throws Throwable { + return impl().methodIB2((java.lang.String) args[0]); + } + }; + + this.__close__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __close__interceptors, __close) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().close(); + return null; + } + }; + } + + @Override + public void methodIB(java.lang.String p1) { + createInvokeAndSupply(__methodIB__call.ctx(), () -> __methodIB__call.apply(p1)); + } + + @Override + public java.lang.String methodIB2(java.lang.String p1) { + return createInvokeAndSupply(__methodIB2__call.ctx(), () -> __methodIB2__call.apply(p1)); + } + + @Override + public void close() throws java.io.IOException, java.lang.RuntimeException { + createInvokeAndSupply(__close__call.ctx(), () -> __close__call.apply()); + } + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java index fe1f97684d5..2366078f49c 100644 --- a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java @@ -21,4 +21,6 @@ public interface IB { void methodIB(String val); + String methodIB2(String val); + } diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/NamedInterceptor.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java similarity index 84% rename from pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/NamedInterceptor.java rename to pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java index 942a972e891..52f6ab2bae5 100644 --- a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/NamedInterceptor.java +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java @@ -24,10 +24,10 @@ import io.helidon.pico.InvocationContext; @SuppressWarnings({"ALL", "unchecked"}) -public class NamedInterceptor implements Interceptor { +public class TestNamedInterceptor implements Interceptor { public static final AtomicInteger ctorCount = new AtomicInteger(); - public NamedInterceptor() { + public TestNamedInterceptor() { ctorCount.incrementAndGet(); } @@ -43,6 +43,9 @@ public V proceed(InvocationContext ctx, long longResult = (Long) result; Object interceptedResult = (longResult * 2); return (V) interceptedResult; + } else if (methodInfo != null && methodInfo.typeName().name().equals(String.class.getName())) { + V result = chain.proceed(args); + return (V) ("intercepted:" + result); } else { return chain.proceed(args); } diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java index 84fd4f23048..45931e33bd8 100644 --- a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java @@ -46,6 +46,13 @@ public void methodIA1() { public void methodIA2() { } + @Named("methodIB2") + @InterceptorBasedAnno("IBSubAnno") + @Override + public String methodIB2(@Named("arg1") String val) { + return val; + } + @Named("methodIB") @InterceptorBasedAnno("IBSubAnno") @Override diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java index e25a090cef1..2fc294401e8 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java @@ -387,6 +387,7 @@ public InterceptorCreatorResponse codegenInterceptors(GeneralCreatorRequest req, private Path codegenInterceptorFilerOut(GeneralCreatorRequest req, DefaultActivatorCreatorResponse.Builder builder, InterceptionPlan interceptionPlan) { + validate(interceptionPlan); TypeName interceptorTypeName = DefaultInterceptorCreator.createInterceptorSourceTypeName(interceptionPlan); DefaultInterceptorCreator interceptorCreator = new DefaultInterceptorCreator(); String body = interceptorCreator.createInterceptorSourceBody(interceptionPlan); @@ -396,6 +397,17 @@ private Path codegenInterceptorFilerOut(GeneralCreatorRequest req, return req.filer().codegenJavaFilerOut(interceptorTypeName, body).orElseThrow(); } + private void validate(InterceptionPlan plan) { + List ctorElements = plan.interceptedElements().stream() + .map(InterceptedElement::elementInfo) + .filter(it -> it.elementKind() == ElementInfo.ElementKind.CONSTRUCTOR) + .collect(Collectors.toList()); + if (ctorElements.size() > 1) { + throw new IllegalStateException("Can only have interceptor with a single (injectable) constructor for: " + + plan.interceptedService().serviceTypeName()); + } + } + private ActivatorCodeGenDetail createActivatorCodeGenDetail(ActivatorCreatorRequest req, TypeName serviceTypeName, LazyValue scan) { diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java index 54b7e7f5014..f54947474b8 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java @@ -58,6 +58,7 @@ import io.github.classgraph.ClassInfo; import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodTypeSignature; import io.github.classgraph.ScanResult; import jakarta.inject.Singleton; @@ -81,7 +82,8 @@ public class DefaultInterceptorCreator extends AbstractCreator implements Interc private static final String INTERCEPTOR_NAME_SUFFIX = "Interceptor"; private static final String INNER_INTERCEPTOR_CLASS_NAME = "$$" + NAME_PREFIX + INTERCEPTOR_NAME_SUFFIX; - private static final String COMPLEX_INTERCEPTOR_HBS = "complex-interceptor.hbs"; + private static final String NO_ARG_INTERCEPTOR_HBS = "no-arg-based-interceptor.hbs"; + private static final String INTERFACES_INTERCEPTOR_HBS = "interface-based-interceptor.hbs"; private static final double INTERCEPTOR_PRIORITY_DELTA = 0.001; private static final String CTOR_ALIAS = "ctor"; private static final Set ALLOW_LIST = new LinkedHashSet<>(); @@ -388,13 +390,6 @@ String serviceTypeName() { return interceptedService.serviceTypeName(); } - /** - * @return the annotation resolver in use - */ - AnnotationTypeNameResolver resolver() { - return resolver; - } - /** * @return the trigger filter in use */ @@ -403,7 +398,7 @@ TriggerFilter triggerFilter() { } /** - * The set of annotation types that are trigger interception. + * The set of annotation types that trigger interception. * * @return the set of annotation types that are trigger interception */ @@ -424,15 +419,30 @@ public Set allAnnotationTypeTriggers() { @Override public Optional createInterceptorPlan(Set interceptorAnnotationTriggers) { - List interceptedElements = getInterceptedElements(interceptorAnnotationTriggers); - if (interceptedElements == null || interceptedElements.isEmpty()) { - return Optional.empty(); + boolean hasNoArgConstructor = hasNoArgConstructor(); + Set interfaces = interfaces(); + + // the code generation will extend the class when there is a zero/no-arg constructor, but if not then we will use a + // different generated source altogether that will only allow the interception of the interfaces. + // note also that the service type, or the method has to have the interceptor trigger annotation to qualify. + if (!hasNoArgConstructor && interfaces.isEmpty()) { + String msg = "There must either be a no-arg constructor, or otherwise the target service must implement at least " + + "one interface type. Note that when a no-arg constructor is available then your entire type, including " + + "all of its public methods are interceptable. If, however, there is no applicable no-arg constructor " + + "available then only the interface-based methods of the target service type are interceptable for: " + + serviceTypeName(); + ToolsException te = new ToolsException(msg); + logger.log(System.Logger.Level.ERROR, "Unable to create an interceptor plan for: " + serviceTypeName(), te); + throw te; } - if (!hasNoArgConstructor()) { - ToolsException te = new ToolsException("There must be a no-arg constructor for: " + serviceTypeName()); - logger.log(System.Logger.Level.WARNING, "skipping interception for: " + serviceTypeName(), te); - return Optional.empty(); + List interceptedElements = (hasNoArgConstructor) + ? getInterceptedElements(interceptorAnnotationTriggers) + : getInterceptedElements(interceptorAnnotationTriggers, interfaces); + if (interceptedElements.isEmpty()) { + ToolsException te = new ToolsException("No methods available to intercept for: " + serviceTypeName()); + logger.log(System.Logger.Level.ERROR, "Unable to create an interceptor plan for: " + serviceTypeName(), te); + throw te; } Set serviceLevelAnnotations = getServiceLevelAnnotations(); @@ -441,6 +451,8 @@ public Optional createInterceptorPlan(Set interceptorA .serviceLevelAnnotations(serviceLevelAnnotations) .annotationTriggerTypeNames(interceptorAnnotationTriggers) .interceptedElements(interceptedElements) + .hasNoArgConstructor(hasNoArgConstructor) + .interfaces(interfaces) .build(); return Optional.of(plan); } @@ -456,17 +468,28 @@ public Optional createInterceptorPlan(Set interceptorA abstract Set getServiceLevelAnnotations(); /** - * Intercepted classes must have a no-arg constructor (current restriction). - * * @return true if there is a no-arg constructor present */ abstract boolean hasNoArgConstructor(); + /** + * @return the set of interfaces implemented + */ + abstract Set interfaces(); + + /** + * @return all public methods + */ abstract List getInterceptedElements(Set interceptorAnnotationTriggers); + /** + * @return all public methods for only the given interfaces + */ + abstract List getInterceptedElements(Set interceptorAnnotationTriggers, + Set interfaces); + boolean containsAny(Set annotations, Set annotationTypeNames) { - assert (!annotationTypeNames.isEmpty()); for (AnnotationAndValue annotation : annotations) { if (annotationTypeNames.contains(annotation.typeName().name())) { return true; @@ -476,7 +499,6 @@ boolean containsAny(Set annotations, } boolean isProcessed(InjectionPointInfo.ElementKind kind, - int methodArgCount, Set modifiers, Boolean isPrivate, Boolean isStatic) { @@ -501,14 +523,11 @@ boolean isProcessed(InjectionPointInfo.ElementKind kind, } } - if (ElementInfo.ElementKind.CONSTRUCTOR == kind && methodArgCount != 0) { - return false; - } - return true; } } + private static class ProcessorBased extends AbstractInterceptorProcessor { private final TypeElement serviceTypeElement; private final ProcessingEnvironment processEnv; @@ -524,18 +543,18 @@ private static class ProcessorBased extends AbstractInterceptorProcessor { } @Override - public Set getAllAnnotations() { + Set getAllAnnotations() { Set set = gatherAllAnnotationsUsedOnPublicNonStaticMethods(serviceTypeElement, processEnv); return set.stream().map(a -> a.typeName().name()).collect(Collectors.toCollection(LinkedHashSet::new)); } @Override - public Set getServiceLevelAnnotations() { + Set getServiceLevelAnnotations() { return createAnnotationAndValueSet(serviceTypeElement); } @Override - public boolean hasNoArgConstructor() { + boolean hasNoArgConstructor() { return serviceTypeElement.getEnclosedElements().stream() .filter(it -> it.getKind().equals(ElementKind.CONSTRUCTOR)) .map(ExecutableElement.class::cast) @@ -543,17 +562,120 @@ public boolean hasNoArgConstructor() { } @Override - protected List getInterceptedElements(Set interceptorAnnotationTriggers) { + Set interfaces() { + return gatherInterfaces(new LinkedHashSet<>(), serviceTypeElement); + } + + Set gatherInterfaces(Set result, + TypeElement typeElement) { + if (typeElement == null) { + return result; + } + + typeElement.getInterfaces().forEach(tm -> { + result.add(TypeTools.createTypeNameFromMirror(tm).orElseThrow()); + gatherInterfaces(result, TypeTools.toTypeElement(tm).orElse(null)); + }); + + return result; + } + + @Override + List getInterceptedElements(Set interceptorAnnotationTriggers) { List result = new ArrayList<>(); Set serviceLevelAnnos = getServiceLevelAnnotations(); + + // find the injectable constructor, falling back to the no-arg constructor + gatherInjectableConstructor(result, serviceLevelAnnos, interceptorAnnotationTriggers); + + // gather all of the public methods as well as the no-arg constructor + serviceTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.METHOD) + .map(ExecutableElement.class::cast) + .filter(e -> isProcessed(toKind(e), /*e.getParameters().size(),*/ e.getModifiers(), null, null)) + .forEach(ee -> result.add( + create(ee, serviceLevelAnnos, interceptorAnnotationTriggers))); + return result; + } + + @Override + List getInterceptedElements(Set interceptorAnnotationTriggers, + Set interfaces) { + assert (!interfaces.isEmpty()); + List result = new ArrayList<>(); + Set serviceLevelAnnos = getServiceLevelAnnotations(); + + // find the injectable constructor, falling back to the no-arg constructor + gatherInjectableConstructor(result, serviceLevelAnnos, interceptorAnnotationTriggers); + + // gather all of the methods that map to one of our interfaces serviceTypeElement.getEnclosedElements().stream() - .filter(e -> e.getKind() == ElementKind.METHOD || e.getKind() == ElementKind.CONSTRUCTOR) + .filter(e -> e.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) - .filter(e -> isProcessed(toKind(e), e.getParameters().size(), e.getModifiers(), null, null)) - .forEach(ee -> result.add(create(ee, serviceLevelAnnos, interceptorAnnotationTriggers))); + .filter(e -> isProcessed(toKind(e), e.getModifiers(), null, null)) + .filter(e -> mapsToAnInterface(e, interfaces)) + .forEach(ee -> result.add( + create(ee, serviceLevelAnnos, interceptorAnnotationTriggers))); return result; } + void gatherInjectableConstructor(List result, + Set serviceLevelAnnos, + Set interceptorAnnotationTriggers) { + serviceTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR) + .map(ExecutableElement.class::cast) + .filter(ee -> !ee.getModifiers().contains(Modifier.PRIVATE)) + .filter(ee -> { + boolean hasInject = ee.getAnnotationMirrors().stream() + .map(TypeTools::createAnnotationAndValue) + .map(AnnotationAndValue::typeName) + .map(TypeName::name) + .anyMatch(anno -> TypeNames.JAKARTA_INJECT.equals(anno) || TypeNames.JAVAX_INJECT.equals(anno)); + return hasInject; + }) + .forEach(ee -> result.add( + create(ee, serviceLevelAnnos, interceptorAnnotationTriggers))); + + if (result.size() > 1) { + throw new ToolsException("There can be at most one injectable constructor for: " + serviceTypeName()); + } + + if (result.size() == 1) { + return; + } + + // find the no-arg constructor as the fallback + serviceTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR) + .map(ExecutableElement.class::cast) + .filter(ee -> !ee.getModifiers().contains(Modifier.PRIVATE)) + .filter(ee -> ee.getParameters().isEmpty()) + .forEach(ee -> result.add( + create(ee, serviceLevelAnnos, interceptorAnnotationTriggers))); + if (result.isEmpty()) { + throw new ToolsException("There should either be a no-arg or injectable constructor for: " + serviceTypeName()); + } + } + + /** + * @return returns true if the given method is implemented in one of the provided interface type names + */ + boolean mapsToAnInterface(ExecutableElement ee, + Set interfaces) { + for (TypeName typeName : interfaces) { + TypeElement te = processEnv.getElementUtils().getTypeElement(typeName.name()); + Objects.requireNonNull(te, typeName.toString()); + boolean hasIt = te.getEnclosedElements().stream() + // _note to self_: there needs to be a better way than this! + .anyMatch(e -> e.toString().equals(ee.toString())); + if (hasIt) { + return true; + } + } + return false; + } + private InterceptedElement create(ExecutableElement ee, Set serviceLevelAnnos, Set interceptorAnnotationTriggers) { @@ -568,6 +690,7 @@ private InterceptedElement create(ExecutableElement ee, } } + private static class ReflectionBased extends AbstractInterceptorProcessor { private final ClassInfo classInfo; @@ -580,37 +703,103 @@ private static class ReflectionBased extends AbstractInterceptorProcessor { } @Override - public Set getAllAnnotations() { + Set getAllAnnotations() { Set set = gatherAllAnnotationsUsedOnPublicNonStaticMethods(classInfo); return set.stream().map(a -> a.typeName().name()).collect(Collectors.toCollection(LinkedHashSet::new)); } @Override - public Set getServiceLevelAnnotations() { + Set getServiceLevelAnnotations() { return createAnnotationAndValueSet(classInfo); } @Override - public boolean hasNoArgConstructor() { + boolean hasNoArgConstructor() { return classInfo.getConstructorInfo().stream() .filter(mi -> !mi.isPrivate()) .anyMatch(mi -> mi.getParameterInfo().length == 0); } @Override - protected List getInterceptedElements(Set interceptorAnnotationTriggers) { + Set interfaces() { + return gatherInterfaces(new LinkedHashSet<>(), classInfo); + } + + Set gatherInterfaces(Set result, + ClassInfo classInfo) { + if (classInfo == null) { + return result; + } + + classInfo.getInterfaces().forEach(tm -> { + result.add(TypeTools.createTypeNameFromClassInfo(tm)); + gatherInterfaces(result, tm); + }); + + return result; + } + + @Override + List getInterceptedElements(Set interceptorAnnotationTriggers) { List result = new ArrayList<>(); Set serviceLevelAnnos = getServiceLevelAnnotations(); classInfo.getMethodAndConstructorInfo() - .filter(m -> isProcessed(toKind(m), - m.getParameterInfo().length, null, m.isPrivate(), m.isStatic())) + .filter(m -> isProcessed(toKind(m), /*m.getParameterInfo().length,*/ null, m.isPrivate(), m.isStatic())) .filter(m -> containsAny(serviceLevelAnnos, interceptorAnnotationTriggers) - || containsAny(createAnnotationAndValueSet( - m.getAnnotationInfo()), interceptorAnnotationTriggers)) - .forEach(mi -> result.add(create(mi, serviceLevelAnnos, interceptorAnnotationTriggers))); + || containsAny(createAnnotationAndValueSet(m.getAnnotationInfo()), interceptorAnnotationTriggers)) + .forEach(mi -> result.add( + create(mi, serviceLevelAnnos, interceptorAnnotationTriggers))); return result; } + @Override + List getInterceptedElements(Set interceptorAnnotationTriggers, + Set interfaces) { + List result = new ArrayList<>(); + Set serviceLevelAnnos = getServiceLevelAnnotations(); + classInfo.getMethodAndConstructorInfo() + .filter(m -> isProcessed(toKind(m), null, m.isPrivate(), m.isStatic())) + .filter(m -> containsAny(serviceLevelAnnos, interceptorAnnotationTriggers) + || containsAny(createAnnotationAndValueSet(m.getAnnotationInfo()), interceptorAnnotationTriggers)) + .filter(m -> mapsToAnInterface(m, interfaces)) + .forEach(mi -> result.add( + create(mi, serviceLevelAnnos, interceptorAnnotationTriggers))); + return result; + } + + boolean mapsToAnInterface(MethodInfo targetMethodInfo, + Set interfaces) { + MethodTypeSignature sig = targetMethodInfo.getTypeSignatureOrTypeDescriptor(); + for (TypeName typeName : interfaces) { + ClassInfo ci = toClassInfo(typeName, classInfo); + Objects.requireNonNull(ci, typeName.toString()); + for (MethodInfo mi : ci.getDeclaredMethodInfo(targetMethodInfo.getName())) { + if (mi.equals(targetMethodInfo) || sig.equals(mi.getTypeSignatureOrTypeDescriptor())) { + return true; + } + } + } + return false; + } + + ClassInfo toClassInfo(TypeName typeName, + ClassInfo child) { + for (ClassInfo ci : child.getInterfaces()) { + if (TypeTools.createTypeNameFromClassInfo(ci).equals(typeName)) { + return ci; + } + } + + for (ClassInfo ci : child.getSuperclasses()) { + ClassInfo foundIt = toClassInfo(typeName, ci); + if (foundIt != null) { + return foundIt; + } + } + + return null; + } + private InterceptedElement create(MethodInfo mi, Set serviceLevelAnnos, Set interceptorAnnotationTriggers) { @@ -648,8 +837,10 @@ static AnnotationTypeNameResolver createResolverFromReflection() { public AbstractInterceptorProcessor createInterceptorProcessor(ServiceInfoBasics interceptedService, InterceptorCreator delegateCreator, Optional processEnv) { + Objects.requireNonNull(interceptedService); + Objects.requireNonNull(delegateCreator); if (processEnv.isPresent()) { - return createInterceptorProcessorFromProcessor(interceptedService, delegateCreator, processEnv.get()); + return createInterceptorProcessorFromProcessor(interceptedService, delegateCreator, processEnv); } return createInterceptorProcessorFromReflection(interceptedService, delegateCreator); } @@ -658,27 +849,30 @@ public AbstractInterceptorProcessor createInterceptorProcessor(ServiceInfoBasics /** * Create an interceptor processor based on annotation processing. * - * @param interceptedService the service being processed - * @param realCreator the real/delegate creator - * @param processEnv the processing env + * @param interceptedService the service being processed + * @param delegateCreator the real/delegate creator + * @param processEnv the processing env, if available * @return the {@link io.helidon.pico.tools.DefaultInterceptorCreator.AbstractInterceptorProcessor} to use */ AbstractInterceptorProcessor createInterceptorProcessorFromProcessor(ServiceInfoBasics interceptedService, - InterceptorCreator realCreator, - ProcessingEnvironment processEnv) { - Options.init(processEnv); + InterceptorCreator delegateCreator, + Optional processEnv) { + processEnv.ifPresent(Options::init); ALLOW_LIST.addAll(Options.getOptionStringList(Options.TAG_ALLOW_LISTED_INTERCEPTOR_ANNOTATIONS)); - return new ProcessorBased(Objects.requireNonNull(interceptedService), - Objects.requireNonNull(realCreator), - Objects.requireNonNull(processEnv), - logger()); + if (processEnv.isPresent()) { + return new ProcessorBased(interceptedService, + delegateCreator, + processEnv.get(), + logger()); + } + return createInterceptorProcessorFromReflection(interceptedService, delegateCreator); } /** * Create an interceptor processor based on reflection processing. * * @param interceptedService the service being processed - * @param realCreator the real/delegate creator + * @param realCreator the real/delegate creator * @return the {@link io.helidon.pico.tools.DefaultInterceptorCreator.AbstractInterceptorProcessor} to use */ AbstractInterceptorProcessor createInterceptorProcessorFromReflection(ServiceInfoBasics interceptedService, @@ -717,6 +911,7 @@ String createInterceptorSourceBody(InterceptionPlan plan) { subst.put("generatedanno", toGeneratedSticker(null)); subst.put("weight", interceptorWeight(plan.interceptedService().declaredWeight())); subst.put("interceptedmethoddecls", toInterceptedMethodDecls(plan)); + subst.put("interfaces", toInterfacesDecl(plan)); subst.put("interceptedelements", IdAndToString .toList(plan.interceptedElements(), DefaultInterceptorCreator::toBody).stream() .filter(it -> !it.getId().equals(CTOR_ALIAS)) @@ -726,7 +921,8 @@ String createInterceptorSourceBody(InterceptionPlan plan) { str -> new IdAndToString(str.replace(".", "_"), str))); subst.put("servicelevelannotations", IdAndToString .toList(plan.serviceLevelAnnotations(), DefaultInterceptorCreator::toDecl)); - String template = templateHelper().safeLoadTemplate(COMPLEX_INTERCEPTOR_HBS); + String template = templateHelper().safeLoadTemplate( + plan.hasNoArgConstructor() ? NO_ARG_INTERCEPTOR_HBS : INTERFACES_INTERCEPTOR_HBS); return templateHelper().applySubstitutions(template, subst, true).trim(); } @@ -750,6 +946,12 @@ private static List toInterceptedMethodDecls(InterceptionPlan pla return result; } + private static String toInterfacesDecl(InterceptionPlan plan) { + return plan.interfaces().stream() + .map(TypeName::name) + .collect(Collectors.joining(", ")); + } + private static IdAndToString toDecl(InterceptedElement method) { MethodElementInfo mi = method.elementInfo(); String name = (mi.elementKind() == ElementInfo.ElementKind.CONSTRUCTOR) ? CTOR_ALIAS : mi.elementName(); @@ -806,7 +1008,9 @@ private static InterceptedMethodCodeGen toBody(InterceptedElement method) { String name = (mi.elementKind() == ElementInfo.ElementKind.CONSTRUCTOR) ? CTOR_ALIAS : mi.elementName(); StringBuilder builder = new StringBuilder(); builder.append("public ").append(mi.elementTypeName()).append(" ").append(mi.elementName()).append("("); - String args = mi.parameterInfo().stream().map(ElementInfo::elementName).collect(Collectors.joining(", ")); + String args = mi.parameterInfo().stream() + .map(ElementInfo::elementName) + .collect(Collectors.joining(", ")); String argDecls = ""; String objArrayArgs = ""; String typedElementArgs = ""; @@ -848,7 +1052,7 @@ private static InterceptedMethodCodeGen toBody(InterceptedElement method) { if (hasArgs) { elementArgInfo = ",\n\t\t\t\tnew TypedElementName[] {" + typedElementArgs + "}"; } - return new InterceptedMethodCodeGen(name, methodDecl, hasReturn, supplierType, elementArgInfo, args, + return new InterceptedMethodCodeGen(name, methodDecl, true, hasReturn, supplierType, elementArgInfo, args, objArrayArgs, untypedElementArgs, method.interceptedTriggerTypeNames(), builder); } @@ -873,6 +1077,7 @@ static double interceptorWeight(Optional serviceWeight) { static class InterceptedMethodCodeGen extends IdAndToString { private final String methodDecl; + private final boolean isOverride; private final boolean hasReturn; private final TypeName elementTypeName; private final String elementArgInfo; @@ -883,6 +1088,7 @@ static class InterceptedMethodCodeGen extends IdAndToString { InterceptedMethodCodeGen(String id, String methodDecl, + boolean isOverride, boolean hasReturn, TypeName elementTypeName, String elementArgInfo, @@ -893,6 +1099,7 @@ static class InterceptedMethodCodeGen extends IdAndToString { Object toString) { super(id, toString); this.methodDecl = methodDecl; + this.isOverride = isOverride; this.hasReturn = hasReturn; this.elementTypeName = elementTypeName; this.elementArgInfo = elementArgInfo; @@ -908,6 +1115,11 @@ public String getMethodDecl() { return methodDecl; } + // note: this needs to stay as a public getXXX() method to support Mustache + public boolean isOverride() { + return isOverride; + } + // note: this needs to stay as a public getXXX() method to support Mustache public TypeName getElementTypeName() { return elementTypeName; diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java index 986c1bce904..ef49ca391a9 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java @@ -21,6 +21,7 @@ import io.helidon.builder.Builder; import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.TypeName; import io.helidon.pico.ServiceInfoBasics; /** @@ -44,6 +45,20 @@ public interface InterceptionPlan { */ Set serviceLevelAnnotations(); + /** + * Returns true if the implementation has a zero/no-argument constructor. + * + * @return true if the service type being intercepted has a zero/no-argument constructor + */ + boolean hasNoArgConstructor(); + + /** + * The interfaces that this service implements (usually a superset of {@link ServiceInfoBasics#contractsImplemented()}). + * + * @return the interfaces implemented + */ + Set interfaces(); + /** * All the annotation names that contributed to triggering this interceptor plan. * 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 0e804dab249..7786ccddec6 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 @@ -500,6 +500,7 @@ static Set gatherAllAnnotationsUsedOnPublicNonStaticMethods( Elements elementUtils = processEnv.getElementUtils(); Set result = new LinkedHashSet<>(); createAnnotationAndValueSet(serviceTypeElement).forEach(anno -> { + result.add(anno); TypeElement typeElement = elementUtils.getTypeElement(anno.typeName().name()); if (typeElement != null) { typeElement.getAnnotationMirrors() @@ -971,14 +972,14 @@ static DefaultElementInfo createParameterInfo(TypeName serviceTypeName, ExecutableElement elemInfo, int elemOffset) { VariableElement paramInfo = elemInfo.getParameters().get(elemOffset - 1); - String elemType = paramInfo.asType().toString(); + TypeName elemTypeName = TypeTools.createTypeNameFromElement(paramInfo).orElseThrow(); Set annotations = createAnnotationAndValueSet(paramInfo.getAnnotationMirrors()); return DefaultElementInfo.builder() .serviceTypeName(serviceTypeName.name()) .elementName("p" + elemOffset) .elementKind(elemInfo.getKind() == ElementKind.CONSTRUCTOR ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD) - .elementTypeName(elemType) + .elementTypeName(elemTypeName.name()) .elementArgs(elemInfo.getParameters().size()) .elementOffset(elemOffset) .access(toAccess(elemInfo)) diff --git a/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs b/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs new file mode 100644 index 00000000000..99c8d051fe9 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs @@ -0,0 +1,106 @@ +{{! +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. +}}{{#header}}{{.}} +{{/header}} +package {{packageName}}; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.DefaultTypedElementName; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.DefaultInvocationContext; +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationException; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.services.InterceptedMethod; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.pico.services.Invocation.createInvokeAndSupply; +import static io.helidon.pico.services.Invocation.mergeAndCollapse; + +/** + * Pico {@link Interceptor} for {@link {{parent}} }. + */ +// using the interfaces approach +@io.helidon.common.Weight({{weight}}) +@io.helidon.pico.Intercepted({{parent}}.class) +@Singleton +@SuppressWarnings("ALL") +@jakarta.annotation.Generated({{{generatedanno}}}) +public class {{className}} /* extends {{parent}} */ implements {{interfaces}} { + private static final List __serviceLevelAnnotations = List.of({{#servicelevelannotations}} + {{{.}}}{{#unless @last}},{{/unless}}{{/servicelevelannotations}}); +{{#interceptedmethoddecls}} + private static final TypedElementName __{{id}} = DefaultTypedElementName.builder() + {{{.}}} + .build();{{/interceptedmethoddecls}} + + private static final TypeName __serviceTypeName = DefaultTypeName.create({{parent}}.class); + + private final Provider<{{parent}}> __provider; + private final ServiceProvider<{{parent}}> __sp; + private final {{parent}} __impl;{{#interceptedelements}} + private final List> __{{id}}__interceptors;{{/interceptedelements}}{{#interceptedelements}} + private final InterceptedMethod<{{parent}}, {{elementTypeName}}> __{{id}}__call;{{/interceptedelements}} + + @Inject + {{this.className}}({{#annotationtriggertypenames}} + @Named("{{{.}}}") List> {{id}},{{/annotationtriggertypenames}} + Provider<{{parent}}> provider) { + this.__provider = Objects.requireNonNull(provider); + this.__sp = (provider instanceof ServiceProvider) ? (ServiceProvider<{{parent}}>) __provider : null; + List> __ctor__interceptors = mergeAndCollapse({{#annotationtriggertypenames}}{{id}}{{#unless @last}}, {{/unless}}{{/annotationtriggertypenames}});{{#interceptedelements}} + this.__{{{id}}}__interceptors = mergeAndCollapse({{interceptedTriggerTypeNames}});{{/interceptedelements}} + + Supplier<{{parent}}> call = __provider::get; + {{parent}} result = createInvokeAndSupply( + DefaultInvocationContext.builder() + .serviceProvider(__sp) + .serviceTypeName(__serviceTypeName) + .classAnnotations(__serviceLevelAnnotations) + .elementInfo(__ctor) + .interceptors(__ctor__interceptors) + .build(), + call); + this.__impl = Objects.requireNonNull(result);{{#interceptedelements}} + + this.__{{id}}__call = new InterceptedMethod<{{parent}}, {{elementTypeName}}>( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __{{id}}__interceptors, __{{id}}{{elementArgInfo}}) { + @Override + public {{elementTypeName}} invoke(Object... args) throws Throwable { + {{#if hasReturn}}return impl().{{id}}({{objArrayArgs}});{{else}}impl().{{id}}({{objArrayArgs}}); + return null;{{/if}} + } + };{{/interceptedelements}} + } +{{#interceptedelements}} + @Override + {{{methodDecl}}} { + {{#if hasReturn}}return {{/if}}createInvokeAndSupply(__{{id}}__call.ctx(), () -> __{{id}}__call.apply({{args}})); + } +{{/interceptedelements}} +} diff --git a/pico/tools/src/main/resources/templates/pico/default/complex-interceptor.hbs b/pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs similarity index 94% rename from pico/tools/src/main/resources/templates/pico/default/complex-interceptor.hbs rename to pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs index 96312ae8b46..1dbc2caf64c 100644 --- a/pico/tools/src/main/resources/templates/pico/default/complex-interceptor.hbs +++ b/pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs @@ -43,8 +43,9 @@ import static io.helidon.pico.services.Invocation.createInvokeAndSupply; import static io.helidon.pico.services.Invocation.mergeAndCollapse; /** - * Pico {@link Interceptor} manager for {@link {{parent}} }. + * Pico {@link Interceptor} for {@link {{parent}} }. */ +// using the no-arg constructor approach @io.helidon.common.Weight({{weight}}) @io.helidon.pico.Intercepted({{parent}}.class) @Singleton @@ -58,10 +59,11 @@ public class {{className}} extends {{parent}} { {{{.}}} .build();{{/interceptedmethoddecls}} + private static final TypeName __serviceTypeName = DefaultTypeName.create({{parent}}.class); + private final Provider<{{parent}}> __provider; private final ServiceProvider<{{parent}}> __sp; - private final {{parent}} __impl; - private final TypeName __serviceTypeName;{{#interceptedelements}} + private final {{parent}} __impl;{{#interceptedelements}} private final List> __{{id}}__interceptors;{{/interceptedelements}}{{#interceptedelements}} private final InterceptedMethod<{{parent}}, {{elementTypeName}}> __{{id}}__call;{{/interceptedelements}} @@ -71,7 +73,6 @@ public class {{className}} extends {{parent}} { Provider<{{parent}}> provider) { this.__provider = Objects.requireNonNull(provider); this.__sp = (provider instanceof ServiceProvider) ? (ServiceProvider<{{parent}}>) __provider : null; - this.__serviceTypeName = DefaultTypeName.create({{parent}}.class); List> __ctor__interceptors = mergeAndCollapse({{#annotationtriggertypenames}}{{id}}{{#unless @last}}, {{/unless}}{{/annotationtriggertypenames}});{{#interceptedelements}} this.__{{{id}}}__interceptors = mergeAndCollapse({{interceptedTriggerTypeNames}});{{/interceptedelements}} @@ -83,7 +84,7 @@ public class {{className}} extends {{parent}} { .classAnnotations(__serviceLevelAnnotations) .elementInfo(__ctor) .interceptors(__ctor__interceptors) - /*.build()*/, + .build(), call); this.__impl = Objects.requireNonNull(result);{{#interceptedelements}} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java index 5a0e2dd652a..3b770d285c1 100644 --- a/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java +++ b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java @@ -19,12 +19,19 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.Optional; +import java.util.Set; import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.pico.DefaultServiceInfoBasics; import io.helidon.pico.InterceptedTrigger; import io.helidon.pico.tools.spi.InterceptorCreator; +import io.helidon.pico.tools.testsubjects.HelloPicoWorld; +import io.helidon.pico.tools.testsubjects.HelloPicoWorldImpl; import jakarta.inject.Named; +import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; import static io.helidon.pico.tools.DefaultInterceptorCreator.AnnotationTypeNameResolver; @@ -32,6 +39,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; class DefaultInterceptorCreatorTest extends AbstractBaseCreator { @@ -57,4 +65,18 @@ void resolverByReflection() { )); } + @Test + void interceptorPlanByReflection() { + DefaultServiceInfoBasics serviceInfoBasics = DefaultServiceInfoBasics.builder() + .serviceTypeName(HelloPicoWorldImpl.class.getName()) + .build(); + DefaultInterceptorCreator.AbstractInterceptorProcessor processor = + ((DefaultInterceptorCreator) interceptorCreator).createInterceptorProcessor(serviceInfoBasics, + interceptorCreator, + Optional.empty()); + InterceptionPlan plan = processor.createInterceptorPlan(Set.of(Singleton.class.getName())).orElseThrow(); + assertThat(plan.hasNoArgConstructor(), is(false)); + assertThat(plan.interfaces(), contains(DefaultTypeName.create(HelloPicoWorld.class))); + } + } diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java index 58ea85b0926..7fbab2ef604 100644 --- a/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java +++ b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java @@ -17,6 +17,7 @@ package io.helidon.pico.tools.testsubjects; import java.util.List; +import java.util.Objects; import java.util.Optional; import io.helidon.pico.RunLevel; @@ -30,6 +31,7 @@ @Singleton @RunLevel(0) +@SuppressWarnings("unused") public class HelloPicoWorldImpl implements HelloPicoWorld { @Inject @@ -52,6 +54,11 @@ public class HelloPicoWorldImpl implements HelloPicoWorld { int postConstructCallCount; int preDestroyCallCount; + @Inject + HelloPicoWorldImpl(PicoWorld picoWorld) { + Objects.requireNonNull(picoWorld); + } + @Override public String sayHello() { assert(postConstructCallCount == 1);