diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2c8568..4fa5e56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -78,7 +78,7 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f6a34d0..c88894e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: 17 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e328fdb..b783a14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: script: | core.setFailed('Not running on main branch, github.ref is ${{ github.ref }}. Please start this workflow only on main') - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: distribution: "temurin" java-version: 17 diff --git a/CHANGELOG.md b/CHANGELOG.md index 99729c9..d3690c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [#77](https://github.com/itsallcode/openfasttrace-gradle/issues/77) + - Add support for OpenFastTrace plugin dependencies - [PR #80](https://github.com/itsallcode/openfasttrace-gradle/pull/80) - Fix JavaDoc warnings and let build fail on warnings - [PR #82](https://github.com/itsallcode/openfasttrace-gradle/pull/82) diff --git a/README.md b/README.md index 720744e..4a33346 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,22 @@ You can configure the following properties: * `filteredArtifactTypes`: Use only the listed artifact types during tracing * `filterWantedStatuses`: Import only specification items that have a status contained in the list of statuses. Possible values: `draft`, `proposed`, `approved`, `rejected`. See the [OFT user guide](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide/user_guide.md#filtering-by-status) for details. +### Using OpenFastTrace Plugins + +OpenFastTrace extension plugins can be added with the `pluginDependencies` property. The dependencies are added to the classpath used by requirement collection and tracing: + +```groovy +repositories { + mavenCentral() +} + +requirementTracing { + pluginDependencies = ['org.itsallcode:openfasttrace-asciidoc-plugin:0.3.0'] +} +``` + +These are OpenFastTrace extension plugins, not Gradle build plugins. Plugin JARs must provide the appropriate OpenFastTrace service descriptors and should not include a duplicate incompatible `openfasttrace-api` dependency. Plugin discovery remains additive to OpenFastTrace's built-in plugin directory. + ### Configuring the Short Tag Importer The short tag importer allows omitting artifact type and the covered artifact type. Optionally you can add a prefix to the item name, e.g. a common module name. diff --git a/example-projects/plugin-config/build.gradle b/example-projects/plugin-config/build.gradle new file mode 100644 index 0000000..8b4748c --- /dev/null +++ b/example-projects/plugin-config/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "base" + id 'org.itsallcode.openfasttrace' +} + +repositories { + mavenCentral() +} + +requirementTracing { + failBuild = true + inputDirectories = files('doc', 'src') + reportFormat = 'plain' + // Once we upgrade this, we can remove RegexMatchingImporterFactory + pluginDependencies = ['org.itsallcode:openfasttrace-asciidoc-plugin:0.3.0'] +} diff --git a/example-projects/plugin-config/doc/spec.adoc b/example-projects/plugin-config/doc/spec.adoc new file mode 100644 index 0000000..56e18b7 --- /dev/null +++ b/example-projects/plugin-config/doc/spec.adoc @@ -0,0 +1,6 @@ +== AsciiDoc Spec + +[.specitem, oft-sid="dsn~asciidoc-exampleB~1", oft-needs="impl,test"] +=== Example AsciiDoc Requirement + +Example AsciiDoc requirement diff --git a/example-projects/plugin-config/doc/spec.md b/example-projects/plugin-config/doc/spec.md new file mode 100644 index 0000000..f7393a1 --- /dev/null +++ b/example-projects/plugin-config/doc/spec.md @@ -0,0 +1,6 @@ +# MarkDown Tracing Example +`dsn~md-exampleA~1` + +Example MarkDown requirement + +Needs: impl, test diff --git a/example-projects/plugin-config/settings.gradle b/example-projects/plugin-config/settings.gradle new file mode 100644 index 0000000..782f259 --- /dev/null +++ b/example-projects/plugin-config/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'plugin-config' diff --git a/example-projects/plugin-config/src/Source.java b/example-projects/plugin-config/src/Source.java new file mode 100644 index 0000000..64c6abd --- /dev/null +++ b/example-projects/plugin-config/src/Source.java @@ -0,0 +1,5 @@ +// [impl->dsn~md-exampleA~1] +// [impl->dsn~asciidoc-exampleB~1] +class Source +{ +} diff --git a/example-projects/plugin-config/src/Test.java b/example-projects/plugin-config/src/Test.java new file mode 100644 index 0000000..f234e52 --- /dev/null +++ b/example-projects/plugin-config/src/Test.java @@ -0,0 +1,5 @@ +// [test->dsn~md-exampleA~1] +// [test->dsn~asciidoc-exampleB~1] +class Test +{ +} diff --git a/src/main/java/org/itsallcode/openfasttrace/api/importer/RegexMatchingImporterFactory.java b/src/main/java/org/itsallcode/openfasttrace/api/importer/RegexMatchingImporterFactory.java new file mode 100644 index 0000000..55ab514 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/api/importer/RegexMatchingImporterFactory.java @@ -0,0 +1,50 @@ +package org.itsallcode.openfasttrace.api.importer; + +import java.util.Collection; + +/** + * Compatibility shim for RegexMatchingImporterFactory which was renamed to + * AbstractRegexMatchingImporterFactory in OpenFastTrace 4.5.0. + *

+ * Shim can be removed when the following issue is fixed: + * + * itsallcode/openffasttrace-asciidoc-plugin # 27 + * + *

+ * Copied from + * OpenFastTrace + * Maven Plugin + * + * @deprecated use {@link AbstractRegexMatchingImporterFactory} instead. + */ +@Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("java:S118") // Shim class. Ignore name convention. +public abstract class RegexMatchingImporterFactory extends AbstractRegexMatchingImporterFactory +{ + /** + * Constructs a new RegexMatchingImporterFactory with the specified file extensions. + * + * @param extensions + * the file extensions to be associated with this importer factory + * @deprecated use {@link AbstractRegexMatchingImporterFactory} instead. + */ + @Deprecated(since = "3.2.0", forRemoval = true) + protected RegexMatchingImporterFactory(final String... extensions) + { + super(extensions); + } + + /** + * Constructs a new RegexMatchingImporterFactory with the specified file extensions. + * + * @param extensions + * the file extensions to be associated with this importer factory + * @deprecated use {@link AbstractRegexMatchingImporterFactory} instead. + */ + @Deprecated(since = "3.2.0", forRemoval = true) + protected RegexMatchingImporterFactory(final Collection extensions) + { + super(extensions); + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java index f3656aa..458a948 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java @@ -70,6 +70,8 @@ private static TaskProvider createCollectTask(final Project rootPro task.setGroup(TASK_GROUP_NAME); task.setDescription("Collect requirements and generate specobject file"); task.getInputDirectories().set(getAllInputDirectories(rootProject.getAllprojects())); + task.getPluginFiles().from( + getPluginDependencies(rootProject, rootProject.getAllprojects())); task.getOutputFile().set( rootProject.getLayout().getBuildDirectory().file("reports/requirements.xml")); task.getPathConfig().set(getPathConfig(rootProject.getAllprojects())); @@ -108,6 +110,8 @@ private static void configureTask(final Project rootProject, task.getReportFormat().set(config.getReportFormat()); task.getImportedRequirements() .from(getImportedRequirements(rootProject, rootProject.getAllprojects())); + task.getPluginFiles().from( + getPluginDependencies(rootProject, rootProject.getAllprojects())); task.getFilteredArtifactTypes().set(config.getFilteredArtifactTypes()); task.getFilteredTags().set(config.getFilteredTags()); task.getFilterAcceptsItemsWithoutTag().set(config.getFilterAcceptsItemsWithoutTag()); @@ -156,11 +160,38 @@ private static ConfigurableFileCollection getImportedRequirements(final Project private static Configuration getImportedRequirements(final Project project) { final String CONFIG_NAME = "oftRequirementConfig"; - final Configuration configuration = project.getConfigurations().create(CONFIG_NAME); - getConfig(project).getImportedRequirements().get().forEach(dependency -> { + return getOrCreateConfiguration(project, CONFIG_NAME, + getConfig(project).getImportedRequirements().get()); + } + + private static ConfigurableFileCollection getPluginDependencies(final Project rootProject, + final Set allProjects) + { + return rootProject.files(allProjects.stream() + .map(OpenFastTracePlugin::getPluginDependencies) + .toList()); + } + + private static Configuration getPluginDependencies(final Project project) + { + final String CONFIG_NAME = "oftPluginConfig"; + return getOrCreateConfiguration(project, CONFIG_NAME, getConfig(project).getPluginDependencies().get()); + } + + private static Configuration getOrCreateConfiguration(final Project project, final String configurationName, + final List dependencies) + { + final Configuration existingConfiguration = project.getConfigurations().findByName(configurationName); + if (existingConfiguration != null) + { + return existingConfiguration; + } + + final Configuration configuration = project.getConfigurations().create(configurationName); + dependencies.forEach(dependency -> { LOG.info("Adding dependency {} with configuration {} to project {}", dependency, - CONFIG_NAME, project); - project.getDependencies().add(CONFIG_NAME, dependency); + configurationName, project); + project.getDependencies().add(configurationName, dependency); }); return configuration; } diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java b/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java index 859ef34..ae70a88 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java @@ -26,6 +26,7 @@ public class TracingConfig private final ConfigurableFileCollection inputDirectories; private final RegularFileProperty reportFile; private final ListProperty importedRequirements; + private final ListProperty pluginDependencies; private final SetProperty filteredTags; private final SetProperty filteredArtifactTypes; private final SetProperty filterWantedStatuses; @@ -35,7 +36,7 @@ public class TracingConfig /** * Creates a tracing configuration with the plugin defaults. - * + * * @param project * the Gradle project owning the configuration */ @@ -49,6 +50,7 @@ public TracingConfig(final Project project) this.reportFormat = project.getObjects().property(String.class); this.reportFormat.set(DEFAULT_REPORT_FORMAT); this.importedRequirements = project.getObjects().listProperty(Object.class); + this.pluginDependencies = project.getObjects().listProperty(Object.class); this.filteredTags = project.getObjects().setProperty(String.class); this.filteredArtifactTypes = project.getObjects().setProperty(String.class); this.filterAcceptsItemsWithoutTag = project.getObjects().property(Boolean.class); @@ -62,7 +64,7 @@ public TracingConfig(final Project project) /** * Returns the report verbosity property. - * + * * @return the verbosity property */ public Property getReportVerbosity() @@ -120,6 +122,16 @@ public ListProperty getImportedRequirements() return importedRequirements; } + /** + * Returns the OpenFastTrace plugin dependencies. + * + * @return the plugin dependencies + */ + public ListProperty getPluginDependencies() + { + return pluginDependencies; + } + /** * Returns the tags to include in tracing. * @@ -300,6 +312,17 @@ public void setImportedRequirements(final List importedRequirements) this.importedRequirements.set(importedRequirements); } + /** + * Sets the OpenFastTrace plugin dependencies. + * + * @param pluginDependencies + * dependencies to add to the OpenFastTrace plugin classpath + */ + public void setPluginDependencies(final List pluginDependencies) + { + this.pluginDependencies.set(pluginDependencies); + } + /** * Sets the tags to include in tracing. * diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/task/CollectTask.java b/src/main/java/org/itsallcode/openfasttrace/gradle/task/CollectTask.java index 963ab94..9057097 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/task/CollectTask.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/CollectTask.java @@ -8,6 +8,7 @@ import java.util.stream.Stream; import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.SetProperty; @@ -17,6 +18,7 @@ import org.itsallcode.openfasttrace.api.importer.ImportSettings; import org.itsallcode.openfasttrace.api.importer.tag.config.PathConfig; import org.itsallcode.openfasttrace.core.*; +import org.itsallcode.openfasttrace.gradle.task.classloader.OftPluginClassLoader; import org.itsallcode.openfasttrace.gradle.task.config.SerializableTagPathConfig; /** Gradle task that collects specification items into a specobject file. */ @@ -35,6 +37,8 @@ public class CollectTask extends DefaultTask @SuppressWarnings({ "this-escape" }) public final ListProperty pathConfig = getProject().getObjects() .listProperty(SerializableTagPathConfig.class); + @SuppressWarnings("this-escape") + private final ConfigurableFileCollection pluginFiles = getProject().files(); /** Creates the task. */ public CollectTask() @@ -76,12 +80,28 @@ public ListProperty getPathConfig() return pathConfig; } + /** + * Returns the OpenFastTrace plugin files. + * + * @return the plugin files + */ + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + public ConfigurableFileCollection getPluginFiles() + { + return pluginFiles; + } + /** Collects specification items and writes the specobject file. */ @TaskAction public void collectRequirements() { createReportOutputDir(); + OftPluginClassLoader.runWithPlugins(pluginFiles, this::collectWithPlugins); + } + private void collectWithPlugins() + { final Oft oft = new OftRunner(); final ImportSettings settings = getImportSettings(); getLogger().info("Importing from {} locations {} and {} path configurations: {}", diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java b/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java index 4821745..c9cd267 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java @@ -20,6 +20,7 @@ import org.itsallcode.openfasttrace.api.report.ReportVerbosity; import org.itsallcode.openfasttrace.core.Oft; import org.itsallcode.openfasttrace.core.OftRunner; +import org.itsallcode.openfasttrace.gradle.task.classloader.OftPluginClassLoader; /** Gradle task that traces requirements and writes a report. */ @SuppressWarnings("this-escape") @@ -38,6 +39,7 @@ public class TraceTask extends DefaultTask private final Property detailsSectionDisplay = getProject().getObjects() .property(DetailsSectionDisplay.class); private final ConfigurableFileCollection importedRequirements = getProject().files(); + private final ConfigurableFileCollection pluginFiles = getProject().files(); private final SetProperty filteredArtifactTypes = getProject().getObjects() .setProperty(String.class); private final SetProperty filteredTags = getProject().getObjects() @@ -123,6 +125,18 @@ public ConfigurableFileCollection getImportedRequirements() return importedRequirements; } + /** + * Returns the OpenFastTrace plugin files. + * + * @return the plugin files + */ + @InputFiles + @PathSensitive(PathSensitivity.ABSOLUTE) + public ConfigurableFileCollection getPluginFiles() + { + return pluginFiles; + } + /** * Returns the artifact type filter. * @@ -201,6 +215,11 @@ private boolean shouldFailBuild() public void trace() { createReportOutputDir(); + OftPluginClassLoader.runWithPlugins(pluginFiles, this::traceWithPlugins); + } + + private void traceWithPlugins() + { final Oft oft = new OftRunner(); final ImportSettings importSettings = getImportSettings(); final List importedItems = oft.importItems(importSettings); diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/ChildFirstClassLoader.java b/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/ChildFirstClassLoader.java new file mode 100644 index 0000000..68b71e6 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/ChildFirstClassLoader.java @@ -0,0 +1,81 @@ +package org.itsallcode.openfasttrace.gradle.task.classloader; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class loader will first try to load the class from the given URLs and + * then from the parent class loader, unlike {@link URLClassLoader} which does + * it the other way around. + *

+ * This allows us to prefer external plugins over plugins on the classpath + * included with OFT. + *

+ * This is based on + * + * "Java: A Child First Class Loader" by Isuru Weerarathna + * + *

+ */ +class ChildFirstClassLoader extends URLClassLoader +{ + private static final Logger LOGGER = Logger.getLogger(ChildFirstClassLoader.class.getName()); + + ChildFirstClassLoader(final String name, final URL[] urls, final ClassLoader parent) + { + super(name, urls, parent); + } + + @Override + protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException + { + if (isSharedOpenFastTraceClass(name)) + { + return super.loadClass(name, resolve); + } + final Class loadedClass = findClass(name, resolve); + if (resolve) + { + resolveClass(loadedClass); + } + return loadedClass; + } + + private static boolean isSharedOpenFastTraceClass(final String name) + { + return name.startsWith("org.itsallcode.openfasttrace.api.") + || name.startsWith("org.itsallcode.openfasttrace.core."); + } + + private Class findClass(final String name, final boolean resolve) throws ClassNotFoundException + { + // Has the class loaded already? + final Class loadedClass = findLoadedClass(name); + if (loadedClass != null) + { + return loadedClass; + } + return loadClassInternally(name, resolve); + } + + private Class loadClassInternally(final String name, final boolean resolve) throws ClassNotFoundException + { + try + { + // Find the class from given jar urls + return findClass(name); + } + catch (final ClassNotFoundException ignore) + { + LOGGER.log(Level.FINEST, () -> "Unable to find class " + name + " with child classloader '" + + this.getClass().getClassLoader().getName() + "'. " + + "Falling back to parent classloader '" + super.getClass().getClassLoader().getName() + "'."); + // Class does not exist in the given URLs. + // Let's try finding it in our parent class's classloader. + // This will throw ClassNotFoundException on failure. + return super.loadClass(name, resolve); + } + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/OftPluginClassLoader.java b/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/OftPluginClassLoader.java new file mode 100644 index 0000000..a941fca --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/OftPluginClassLoader.java @@ -0,0 +1,108 @@ +package org.itsallcode.openfasttrace.gradle.task.classloader; + +import java.io.IOException; +import java.net.*; +import java.util.*; + +import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.itsallcode.openfasttrace.core.OftRunner; + +/** Runs OpenFastTrace operations with additional plugin artifacts on the context classpath. */ +public final class OftPluginClassLoader +{ + private static final Logger LOG = Logging.getLogger(OftPluginClassLoader.class); + + private OftPluginClassLoader() + { + } + + /** + * Runs an operation with the given plugin files available to service loading. + * + * @param pluginFiles + * plugin artifacts to expose + * @param action + * operation to run + */ + public static void runWithPlugins(final FileCollection pluginFiles, final Runnable action) + { + if (pluginFiles.isEmpty()) + { + action.run(); + return; + } + + final Thread thread = Thread.currentThread(); + final ClassLoader originalClassLoader = thread.getContextClassLoader(); + final ClassLoader parent = new ParentClassLoader(OftRunner.class.getClassLoader(), originalClassLoader); + final URLClassLoader pluginClassLoader = createClassLoader(pluginFiles, parent); + thread.setContextClassLoader(pluginClassLoader); + try + { + action.run(); + } + finally + { + thread.setContextClassLoader(originalClassLoader); + try + { + pluginClassLoader.close(); + } + catch (final IOException e) + { + LOG.warn("Could not close OpenFastTrace plugin classloader", e); + } + } + } + + private static URLClassLoader createClassLoader(final FileCollection pluginFiles, final ClassLoader parent) + { + final Set pluginUrls = new HashSet<>(pluginFiles.getFiles().stream() + .map(file -> file.toPath().toUri()) + .toList()); + // OFT only accepts service providers loaded by the classloader that discovered them. + // Add OFT's built-in provider JARs to this loader so they are not filtered out. + addServiceProviderJars(parent, pluginUrls, + "org.itsallcode.openfasttrace.api.exporter.ExporterFactory"); + addServiceProviderJars(parent, pluginUrls, + "org.itsallcode.openfasttrace.api.importer.ImporterFactory"); + final URL[] urls = pluginUrls.toArray(URL[]::new); + final String pluginUrlsString = Arrays.toString(urls); + return new ChildFirstClassLoader("ChildFirst ClassLoader for " + pluginUrlsString, urls, parent); + } + + private static void addServiceProviderJars(final ClassLoader parent, final Set urls, + final String serviceName) + { + try + { + final String resourceName = "META-INF/services/" + serviceName; + for (final URL resource : java.util.Collections.list(parent.getResources(resourceName))) + { + final URLConnection connection = resource.openConnection(); + if (connection instanceof final JarURLConnection jarConnection) + { + urls.add(toUri(jarConnection.getJarFileURL())); + } + } + } + catch (final IOException e) + { + throw new IllegalStateException("Could not locate service provider jars for " + serviceName, e); + } + } + + private static URI toUri(final URL url) + { + try + { + return url.toURI(); + } + catch (final URISyntaxException e) + { + throw new IllegalStateException("Invalid JAR file URL: " + url, e); + } + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/ParentClassLoader.java b/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/ParentClassLoader.java new file mode 100644 index 0000000..b7b50d4 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/ParentClassLoader.java @@ -0,0 +1,73 @@ +package org.itsallcode.openfasttrace.gradle.task.classloader; + +import java.io.IOException; +import java.net.URL; +import java.util.*; + +final class ParentClassLoader extends ClassLoader +{ + private final ClassLoader[] parents; + + ParentClassLoader(final ClassLoader... parents) + { + super(null); + this.parents = Arrays.stream(parents) + .filter(Objects::nonNull) + .distinct() + .toArray(ClassLoader[]::new); + } + + @Override + @SuppressWarnings("java:S3032") // Explicit loading is required to delegate to multiple independent parents. + protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException + { + for (final ClassLoader parent : parents) + { + try + { + return Class.forName(name, resolve, parent); + } + catch (final ClassNotFoundException e) + { + // Ignore and try the next parent class loader‚ + } + } + throw new ClassNotFoundException(name); + } + + @Override + public URL getResource(final String name) + { + for (final ClassLoader parent : parents) + { + final URL resource = parent.getResource(name); + if (resource != null) + { + return resource; + } + } + return null; + } + + @Override + public Enumeration getResources(final String name) throws IOException + { + final List resources = Arrays.stream(parents) + .map(parent -> getResources(parent, name)) + .flatMap(List::stream) + .toList(); + return Collections.enumeration(resources); + } + + private static List getResources(final ClassLoader parent, final String name) + { + try + { + return Collections.list(parent.getResources(name)); + } + catch (final IOException e) + { + throw new IllegalStateException("Could not get resources for " + name, e); + } + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java index 27380c4..9482f0d 100644 --- a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java @@ -30,6 +30,7 @@ class OpenFastTracePluginTest private static final Path DEPENDENCY_CONFIG_DIR = EXAMPLES_DIR.resolve("dependency-config"); private static final Path PUBLISH_CONFIG_DIR = EXAMPLES_DIR.resolve("publish-config"); private static final Path HTML_REPORT_CONFIG_DIR = EXAMPLES_DIR.resolve("html-report"); + private static final Path PLUGIN_CONFIG_DIR = EXAMPLES_DIR.resolve("plugin-config"); @Parameter private GradleTestConfig config; @@ -318,6 +319,13 @@ void testTraceMultiProject() .assertReportFileLines("ok - 6 total"); } + @Test + void testTraceExampleProjectWithPluginDependency() + { + testConfigurationCache(PLUGIN_CONFIG_DIR, Path.of("build/reports/tracing.txt"), + "ok - 6 total"); + } + @Test void traceDependencyProject() { diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java b/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java index 8951308..7ca8225 100644 --- a/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.*; +import java.util.function.Function; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; @@ -53,15 +54,18 @@ public PluginTestFixture withReportFile(final Path relativeReportPath) Result run() { - final GradleRunner runner = createGradleRunner(); - final BuildResult buildResult = runner.build(); - return new Result(buildResult); + return run(GradleRunner::build); } Result runExpectingFailure() + { + return run(GradleRunner::buildAndFail); + } + + private Result run(final Function runnerFunction) { final GradleRunner runner = createGradleRunner(); - final BuildResult buildResult = runner.buildAndFail(); + final BuildResult buildResult = runnerFunction.apply(runner); return new Result(buildResult); }