result
});
}
+ private Path trackerStatePath() {
+ return scratchClassOutputPath(targetClassOutputPath(processingEnv.getFiler())).resolve(DEFAULT_SCRATCH_FILE_NAME);
+ }
+
}
diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ProcessingTracker.java b/pico/processor/src/main/java/io/helidon/pico/processor/ProcessingTracker.java
new file mode 100644
index 00000000000..3bad1593ac2
--- /dev/null
+++ b/pico/processor/src/main/java/io/helidon/pico/processor/ProcessingTracker.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.pico.processor;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.TypeElement;
+
+import io.helidon.pico.tools.ToolsException;
+
+/**
+ * This class adds persistent tracking (typically under ./target/XXX) to allow seamless full and/or incremental processing of
+ * types to be tracked over repeated compilation cycles over time. It is expected to be integrated into a host annotation
+ * processor implementation.
+ *
+ * For example, when incremental processing occurs, the elements passed to process in all rounds will just be a subset of
+ * all of the annotated services since the compiler (from the IDE) only recompiles the files that have been changed. This is
+ * typically different from how maven invokes compilation (doing a full compile where all types will be seen in the round). The
+ * {@link PicoAnnotationProcessor}, for example, would see this reduced subset of types in the round and would otherwise have
+ * created a {@link io.helidon.pico.api.ModuleComponent} only representative of the reduced subset of classes. This would be
+ * incorrect and lead to an invalid module component source file to have been generated.
+ *
+ * We use this tracker to persist the list of generated activators much in the same way that
+ * {@code META-INF/services} are tracked. A target scratch directory (i.e., target/pico in this case) is used instead - in order
+ * to keep it out of the build jar.
+ *
+ * Usage:
+ *
+ * - {@link #initializeFrom} - during the APT initialization phase
+ * - {@link #processing(String)} - during each processed type that the annotation processor visits in the round
+ * - {@link #removedTypeNames()} or {@link #remainingTypeNames()} as needed - to see the changes over time
+ * - {@link #close()} - during final lifecycle of the APT in order to persist state to be (re)written out to disk
+ *
+ *
+ * @see PicoAnnotationProcessor
+ */
+class ProcessingTracker implements AutoCloseable {
+ static final String DEFAULT_SCRATCH_FILE_NAME = "activators.lst";
+
+ private final Path path;
+ private final Set allTypeNames;
+ private final TypeElementFinder typeElementFinder;
+ private final Set foundOrProcessed = new LinkedHashSet<>();
+
+ /**
+ * Creates an instance using the given path to keep persistent state.
+ *
+ * @param persistentScratchPath the fully qualified path to carry the state
+ * @param allLines all lines read at initialization
+ * @param typeElementFinder the type element finder (e.g., {@link ProcessingEnvironment#getElementUtils})
+ */
+ ProcessingTracker(Path persistentScratchPath,
+ List allLines,
+ TypeElementFinder typeElementFinder) {
+ this.path = persistentScratchPath;
+ this.allTypeNames = new LinkedHashSet<>(allLines);
+ this.typeElementFinder = typeElementFinder;
+ }
+
+ public static ProcessingTracker initializeFrom(Path persistentScratchPath,
+ ProcessingEnvironment processingEnv) {
+ List allLines = List.of();
+ File file = persistentScratchPath.toFile();
+ if (file.exists() && file.canRead()) {
+ try {
+ allLines = Files.readAllLines(persistentScratchPath, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new ToolsException(e.getMessage(), e);
+ }
+ }
+ return new ProcessingTracker(persistentScratchPath, allLines, toTypeElementFinder(processingEnv));
+ }
+
+ public ProcessingTracker processing(String typeName) {
+ foundOrProcessed.add(Objects.requireNonNull(typeName));
+ return this;
+ }
+
+ public Set allTypeNamesFromInitialization() {
+ return allTypeNames;
+ }
+
+ public Set removedTypeNames() {
+ Set typeNames = new LinkedHashSet<>(allTypeNamesFromInitialization());
+ typeNames.removeAll(remainingTypeNames());
+ return typeNames;
+ }
+
+ public Set remainingTypeNames() {
+ Set typeNames = new LinkedHashSet<>(allTypeNamesFromInitialization());
+ typeNames.addAll(foundOrProcessed);
+ typeNames.removeIf(typeName -> !found(typeName));
+ return typeNames;
+ }
+
+ @Override
+ public void close() throws IOException {
+ Path parent = path.getParent();
+ if (parent == null) {
+ throw new ToolsException("bad path: " + path);
+ }
+ Files.createDirectories(parent);
+ Files.write(path, remainingTypeNames(), StandardCharsets.UTF_8);
+ }
+
+ private boolean found(String typeName) {
+ return (typeElementFinder.apply(typeName) != null);
+ }
+
+ private static TypeElementFinder toTypeElementFinder(ProcessingEnvironment processingEnv) {
+ return typeName -> processingEnv.getElementUtils().getTypeElement(typeName);
+ }
+
+ @FunctionalInterface
+ interface TypeElementFinder extends Function {
+ }
+
+}
diff --git a/pico/processor/src/test/java/io/helidon/pico/processor/ProcessingTrackerTest.java b/pico/processor/src/test/java/io/helidon/pico/processor/ProcessingTrackerTest.java
new file mode 100644
index 00000000000..c06928094eb
--- /dev/null
+++ b/pico/processor/src/test/java/io/helidon/pico/processor/ProcessingTrackerTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.pico.processor;
+
+import java.util.List;
+
+import javax.lang.model.element.TypeElement;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.mockito.Mockito.mock;
+
+class ProcessingTrackerTest {
+
+ @Test
+ void noDelta() {
+ List typeNames = List.of("a", "b", "c");
+ ProcessingTracker tracker = new ProcessingTracker(null, typeNames,
+ typeName -> mock(TypeElement.class));
+ assertThat(tracker.removedTypeNames().size(),
+ is(0));
+ assertThat(tracker.remainingTypeNames(),
+ containsInAnyOrder("a", "b", "c"));
+ }
+
+ @Test
+ void incrementalCompilation() {
+ List typeNames = List.of("a", "b", "c");
+ ProcessingTracker tracker = new ProcessingTracker(null, typeNames,
+ typeName -> mock(TypeElement.class));
+ tracker.processing("b");
+
+ assertThat(tracker.removedTypeNames().size(),
+ is(0));
+ assertThat(tracker.remainingTypeNames(),
+ containsInAnyOrder("a", "b", "c"));
+ }
+
+ @Test
+ void incrementalCompilationWithFilesRemoved() {
+ List typeNames = List.of("a", "b", "c");
+ ProcessingTracker tracker = new ProcessingTracker(null, typeNames,
+ typeName -> (typeName.equals("b") ? null : mock(TypeElement.class)));
+
+ assertThat(tracker.removedTypeNames().size(),
+ is(1));
+ assertThat(tracker.remainingTypeNames(),
+ containsInAnyOrder("a", "c"));
+ }
+
+ @Test
+ void incrementalCompilationWithFilesAddedAndRemoved() {
+ List typeNames = List.of("a");
+ ProcessingTracker tracker = new ProcessingTracker(null, typeNames,
+ typeName -> mock(TypeElement.class));
+ tracker.processing("b");
+ tracker.processing("a");
+
+ assertThat(tracker.removedTypeNames().size(),
+ is(0));
+ assertThat(tracker.remainingTypeNames(),
+ containsInAnyOrder("a", "b"));
+ }
+
+ @Test
+ void cleanCompilation() {
+ List typeNames = List.of();
+ ProcessingTracker tracker = new ProcessingTracker(null, typeNames,
+ typeName -> mock(TypeElement.class));
+ tracker.processing("a");
+ tracker.processing("b");
+ tracker.processing("c");
+
+ assertThat(tracker.removedTypeNames().size(),
+ is(0));
+ assertThat(tracker.remainingTypeNames(),
+ containsInAnyOrder("a", "b", "c"));
+ }
+
+ @Test
+ void fullCompilationWithFilesAdded() {
+ List typeNames = List.of("a");
+ ProcessingTracker tracker = new ProcessingTracker(null, typeNames,
+ typeName -> mock(TypeElement.class));
+ tracker.processing("a");
+ tracker.processing("b");
+ tracker.processing("c");
+
+ assertThat(tracker.removedTypeNames().size(),
+ is(0));
+ assertThat(tracker.remainingTypeNames(),
+ containsInAnyOrder("a", "b", "c"));
+ }
+
+}
diff --git a/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java b/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java
index 3690106f14c..f9863ab8f33 100644
--- a/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java
+++ b/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java
@@ -19,7 +19,7 @@
import jakarta.inject.Singleton;
@Singleton
-class TheOtherService implements OtherContract{
+class TheOtherService implements OtherContract {
private boolean throwException;
@Modify
diff --git a/pico/tests/resources-pico/pom.xml b/pico/tests/resources-pico/pom.xml
index f2234d1b4a6..7f7d185cf5a 100644
--- a/pico/tests/resources-pico/pom.xml
+++ b/pico/tests/resources-pico/pom.xml
@@ -94,7 +94,7 @@
-Apico.autoAddNonContractInterfaces=true
-Apico.allowListedInterceptorAnnotations=io.helidon.pico.tests.pico.interceptor.TestNamed
- -Apico.application.pre.create=true
+ -Apico.application.pre.create=false
-Apico.mapApplicationToSingletonScope=true
-Apico.debug=${pico.debug}
@@ -145,10 +145,7 @@
-Apico.debug=${pico.debug}
-Apico.autoAddNonContractInterfaces=true
- -Apico.application.pre.create=true
-
-
-
+ -Apico.application.pre.create=false
NAMED
diff --git a/pico/tests/resources-pico/src/main/java/module-info.java b/pico/tests/resources-pico/src/main/java/module-info.java
index 96a9a0ac8e8..714aeae2c47 100644
--- a/pico/tests/resources-pico/src/main/java/module-info.java
+++ b/pico/tests/resources-pico/src/main/java/module-info.java
@@ -33,5 +33,4 @@
exports io.helidon.pico.tests.pico.tbox;
provides io.helidon.pico.api.ModuleComponent with io.helidon.pico.tests.pico.Pico$$Module;
- provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$Application;
}
diff --git a/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_
index 32608ecea9b..b156cd28f92 100644
--- a/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_
+++ b/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_
@@ -33,7 +33,6 @@ module io.helidon.pico.tests.pico {
exports io.helidon.pico.tests.pico.tbox;
provides io.helidon.pico.api.ModuleComponent with io.helidon.pico.tests.pico.Pico$$Module;
- provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$Application;
// pico external contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1")
requires test1;
requires test2;
@@ -44,4 +43,6 @@ module io.helidon.pico.tests.pico {
uses io.helidon.pico.api.OptionallyNamed;
// pico contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1")
exports io.helidon.pico.tests.pico.provider;
+ // pico application - Generated(value = "io.helidon.pico.tools.ApplicationCreatorDefault", comments = "version=1")
+ provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$Application;
}
diff --git a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_
index 6aef3a52f72..52b16aea6cc 100644
--- a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_
+++ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_
@@ -3,12 +3,12 @@ module io.helidon.pico.tests.pico/test {
exports io.helidon.pico.tests.pico;
// pico module - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1")
provides io.helidon.pico.api.ModuleComponent with io.helidon.pico.tests.pico.Pico$$TestModule;
- // pico application - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1")
- provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$TestApplication;
// pico external contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1")
uses io.helidon.pico.api.Resettable;
uses io.helidon.pico.tests.pico.stacking.Intercepted;
uses io.helidon.pico.tests.pico.stacking.InterceptedImpl;
// pico services - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1")
requires transitive io.helidon.pico.runtime;
+ // pico application - Generated(value = "io.helidon.pico.tools.ApplicationCreatorDefault", comments = "version=1")
+ provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$TestApplication;
}
diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java
index 01c2d200d62..580dc95c986 100644
--- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java
+++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java
@@ -188,4 +188,13 @@ public interface ActivatorCreatorCodeGen {
@ConfiguredOption(DEFAULT_CLASS_PREFIX_NAME)
String classPrefixName();
+ /**
+ * Used in conjunction with {@link ActivatorCreatorConfigOptions#isModuleCreated()}. If a module is created and this set is
+ * populated then this set will be used to represent all {@link io.helidon.pico.api.Activator} type names that should be code
+ * generated for this {@link io.helidon.pico.api.ModuleComponent}.
+ *
+ * @return all module activator type names known for this given module being processed
+ */
+ Set allModuleActivatorTypeNames();
+
}
diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java
index f2fc6069626..7bf8bbff38e 100644
--- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java
+++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java
@@ -27,6 +27,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -139,7 +140,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req,
CodeGenPaths codeGenPaths = req.codeGenPaths().orElse(null);
Map serviceTypeToIsAbstractType = req.codeGen().serviceTypeIsAbstractTypes();
List activatorTypeNames = new ArrayList<>();
- List activatorTypeNamesPutInModule = new ArrayList<>();
+ Set activatorTypeNamesPutInModule = new TreeSet<>(req.codeGen().allModuleActivatorTypeNames());
Map activatorDetails = new LinkedHashMap<>();
for (TypeName serviceTypeName : req.serviceTypeNames()) {
try {
@@ -164,6 +165,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req,
}
}
builder.serviceTypeNames(activatorTypeNames)
+ .activatorTypeNamesPutInComponentModule(activatorTypeNamesPutInModule)
.serviceTypeDetails(activatorDetails);
ModuleDetail moduleDetail;
@@ -207,7 +209,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req,
}
private ModuleDetail toModuleDetail(ActivatorCreatorRequest req,
- List activatorTypeNamesPutInModule,
+ Set activatorTypeNamesPutInModule,
TypeName moduleTypeName,
TypeName applicationTypeName,
boolean isApplicationCreated,
@@ -628,7 +630,7 @@ String toModuleBody(ActivatorCreatorRequest req,
String packageName,
String className,
String moduleName,
- List activatorTypeNames) {
+ Set activatorTypeNames) {
String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_MODULE_HBS);
Map subst = new HashMap<>();
diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java
index 5bc1c06b027..319ec8a87b9 100644
--- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java
+++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java
@@ -18,6 +18,7 @@
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import io.helidon.builder.Builder;
import io.helidon.builder.Singular;
@@ -37,13 +38,20 @@ public interface ActivatorCreatorResponse extends GeneralCreatorResponse {
ActivatorCreatorConfigOptions getConfigOptions();
/**
- * return The interceptors that were generated.
+ * Return the interceptors that were generated.
*
* @return interceptors generated
*/
@Singular
Map serviceTypeInterceptorPlans();
+ /**
+ * The activator types placed in the generated {@link io.helidon.pico.api.ModuleComponent}.
+ *
+ * @return the activator type names placed in the module component
+ */
+ Set activatorTypeNamesPutInComponentModule();
+
/**
* The module-info detail, if a module was created.
*
diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java
index 950e1fdbad8..76000e05c78 100644
--- a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java
+++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java
@@ -191,7 +191,13 @@ public void codegenMetaInfServices(CodeGenPaths paths,
}
}
- private static Path targetClassOutputPath(Filer filer) {
+ /**
+ * Returns the target class output directory.
+ *
+ * @param filer the filer
+ * @return the path to the target class output directory
+ */
+ public static Path targetClassOutputPath(Filer filer) {
if (filer instanceof AbstractFilerMessager.DirectFilerMessager) {
CodeGenPaths paths = ((AbstractFilerMessager.DirectFilerMessager) filer).codeGenPaths();
return Path.of(paths.outputPath().orElseThrow());
@@ -209,7 +215,13 @@ private static Path targetClassOutputPath(Filer filer) {
}
}
- private static Path scratchClassOutputPath(Path targetOutputPath) {
+ /**
+ * Returns the path to the target scratch directory for Pico.
+ *
+ * @param targetOutputPath the target class output path
+ * @return the pico target scratch path
+ */
+ public static Path scratchClassOutputPath(Path targetOutputPath) {
Path fileName = targetOutputPath.getFileName();
Path parent = targetOutputPath.getParent();
if (fileName == null || parent == null) {