This is an automated email from the ASF dual-hosted git repository. rmannibucau pushed a commit to branch dev/1522 in repository https://gitbox.apache.org/repos/asf/maven-dependency-plugin.git
commit bd96d0a933bfcf7c4528d514b8989d40c374d2eb Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Wed Sep 17 18:13:49 2025 +0200 Fixes #1522, add render-dependencies mojo --- .../fromDependencies/RenderDependenciesMojo.java | 240 +++++++++++++++++++++ .../TestRenderDependenciesMojo.java | 104 +++++++++ .../render-dependencies-test/plugin-config.xml | 37 ++++ 3 files changed, 381 insertions(+) diff --git a/src/main/java/org/apache/maven/plugins/dependency/fromDependencies/RenderDependenciesMojo.java b/src/main/java/org/apache/maven/plugins/dependency/fromDependencies/RenderDependenciesMojo.java new file mode 100644 index 00000000..63ff132a --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/dependency/fromDependencies/RenderDependenciesMojo.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.dependency.fromDependencies; + +import javax.inject.Inject; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.plugins.dependency.utils.ResolverUtil; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.project.ProjectBuilder; +import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.tools.generic.CollectionTool; +import org.sonatype.plexus.build.incremental.BuildContext; + +import static java.util.Optional.ofNullable; + +/** + * This goal renders dependencies based on a velocity template. + * + * @since 3.8.2 + */ +@Mojo( + name = "render-dependencies", + requiresDependencyResolution = ResolutionScope.TEST, + defaultPhase = LifecyclePhase.GENERATE_SOURCES, + threadSafe = true) +public class RenderDependenciesMojo extends AbstractDependencyFilterMojo { + @Parameter(property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}") + private String outputEncoding; + + /** + * The file to write the rendered template string. If undefined, it just prints the classpath as [INFO]. + */ + @Parameter(property = "mdep.outputFile") + private File outputFile; + + /** + * If not null or empty it will attach the artifact with this classifier. + */ + @Parameter(property = "mdep.classifier", defaultValue = "template") + private String classifier; + + /** + * velocity template to use to render the output file. + */ + @Parameter(property = "mdep.template", defaultValue = "<set the template>") + private String template; + + private final MavenProjectHelper projectHelper; + + @Inject + protected RenderDependenciesMojo( + MavenSession session, + BuildContext buildContext, + MavenProject project, + ResolverUtil resolverUtil, + ProjectBuilder projectBuilder, + ArtifactHandlerManager artifactHandlerManager, + MavenProjectHelper projectHelper) { + super(session, buildContext, project, resolverUtil, projectBuilder, artifactHandlerManager); + this.projectHelper = projectHelper; + } + + /** + * Main entry into mojo. + * + * @throws MojoExecutionException with a message if an error occurs + */ + @Override + protected void doExecute() throws MojoExecutionException { + // sort them to ease template work and ensure it is deterministic + final List<Artifact> artifacts = + ofNullable(getResolvedDependencies(true)).orElseGet(Collections::emptySet).stream() + .sorted(Comparator.comparing(Artifact::getGroupId) + .thenComparing(Artifact::getArtifactId) + .thenComparing(Artifact::getBaseVersion) + .thenComparing(orEmpty(Artifact::getClassifier)) + .thenComparing(orEmpty(Artifact::getType))) + .collect(Collectors.toList()); + + if (artifacts.isEmpty()) { + getLog().warn("No dependencies found."); + } + + final String rendered = render(artifacts); + + if (outputFile == null) { + getLog().info(rendered); + } else { + store(rendered, outputFile); + } + if (classifier != null && !classifier.isEmpty()) { + attachFile(rendered); + } + } + + /** + * Do render the template. + * @param artifacts input. + * @return the template rendered. + */ + private String render(final List<Artifact> artifacts) { + final Properties props = new Properties(); + props.setProperty("runtime.references.strict", "true"); + + final VelocityEngine ve = new VelocityEngine(props); + ve.init(); + + final VelocityContext context = new VelocityContext(); + context.put("artifacts", artifacts); + context.put("sorter", new CollectionTool()); + + // Merge template + context + final StringWriter writer = new StringWriter(); + try { + ve.evaluate(context, writer, "tpl-" + Math.abs(hashCode()), template); + } finally { + try { + writer.close(); + } catch (final IOException e) { + // no-op, not possible + } + } + + return writer.toString(); + } + + /** + * Trivial null protection impl for comparing callback. + * @param getter nominal getter. + * @return a comparer of getter defaulting on empty if getter value is null. + */ + private Comparator<Artifact> orEmpty(final Function<Artifact, String> getter) { + return Comparator.comparing(a -> ofNullable(getter.apply(a)).orElse("")); + } + + /** + * @param content the rendered template + * @throws MojoExecutionException in case of an error + */ + protected void attachFile(final String content) throws MojoExecutionException { + final File attachedFile; + if (outputFile == null) { + attachedFile = new File(getProject().getBuild().getDirectory(), classifier); + store(content, attachedFile); + } else { // already written + attachedFile = outputFile; + } + store(content, attachedFile); + projectHelper.attachArtifact(getProject(), attachedFile, classifier); + } + + /** + * Stores the specified string into that file. + * + * @param content the string to write into the file + */ + private void store(final String content, final File out) throws MojoExecutionException { + // make sure the parent path exists. + final Path parent = out.toPath().getParent(); + if (parent != null) { + try { + Files.createDirectories(parent); + } catch (final IOException e) { + throw new MojoExecutionException(e); + } + } + + final String encoding = Objects.toString(outputEncoding, StandardCharsets.UTF_8.name()); + try (Writer w = Files.newBufferedWriter(out.toPath(), Charset.forName(encoding))) { + w.write(content); + getLog().info("Wrote file '" + out + "'."); + } catch (final IOException ex) { + throw new MojoExecutionException("Error while writing to file '" + out, ex); + } + } + + @Override + protected ArtifactsFilter getMarkedArtifactFilter() { + return null; + } + + public void setOutputEncoding(final String outputEncoding) { + this.outputEncoding = outputEncoding; + } + + public void setOutputFile(final File outputFile) { + this.outputFile = outputFile; + } + + public void setClassifier(final String classifier) { + this.classifier = classifier; + } + + public void setTemplate(final String template) { + this.template = template; + } +} diff --git a/src/test/java/org/apache/maven/plugins/dependency/fromDependencies/TestRenderDependenciesMojo.java b/src/test/java/org/apache/maven/plugins/dependency/fromDependencies/TestRenderDependenciesMojo.java new file mode 100644 index 00000000..9db2d0fb --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/dependency/fromDependencies/TestRenderDependenciesMojo.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.dependency.fromDependencies; + +import java.io.File; +import java.util.Set; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugins.dependency.AbstractDependencyMojoTestCase; +import org.apache.maven.plugins.dependency.testUtils.stubs.DependencyProjectStub; +import org.apache.maven.project.MavenProject; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestRenderDependenciesMojo extends AbstractDependencyMojoTestCase { + private RenderDependenciesMojo mojo; + + @Override + protected String getTestDirectoryName() { + return "render-dependencies"; + } + + @Override + protected boolean shouldCreateFiles() { + return true; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + final MavenProject project = new DependencyProjectStub(); + getContainer().addComponent(project, MavenProject.class.getName()); + + final MavenSession session = newMavenSession(project); + getContainer().addComponent(session, MavenSession.class.getName()); + + final File testPom = new File( + getBasedir(), "target/test-classes/unit/" + getTestDirectoryName() + "-test/plugin-config.xml"); + mojo = (RenderDependenciesMojo) lookupMojo(getTestDirectoryName(), testPom); + } + + /** + * Tests the rendering. + * Note that this is a real life example of using the mojo to generate a CRD for a SparkApplication. + * It is useful when combined with JIB for example since several versions of the CRD do not support wildcard for + * the classpath(s). + */ + public void testRender() throws Exception { + final File rendered = new File(testDir, "render-dependencies.testRender.txt"); + + final MavenProject project = mojo.getProject(); + final Set<Artifact> artifacts = stubFactory.getScopedArtifacts(); + final Set<Artifact> directArtifacts = stubFactory.getReleaseAndSnapshotArtifacts(); + artifacts.addAll(directArtifacts); + project.setArtifacts(artifacts); + project.setDependencyArtifacts(directArtifacts); + + mojo.setTemplate("deps:\n" + + " jars:\n" + + "#foreach($dep in $sorter.sort($artifacts, [\"artifactId:asc\"]))\n" + + "#set($type = $dep.type)\n" + + "#if(!$type || $type.trim().isEmpty())\n" + + " #set($type = \"jar\")\n" + + "#end\n" + + "#set($classifierSuffix = \"\")\n" + + "#if($dep.classifier && !$dep.classifier.trim().isEmpty())\n" + + " #set($classifierSuffix = \"-$dep.classifier\")\n" + + "#end\n" + + " - local:///opt/test/libs/$dep.artifactId-$dep.baseVersion$classifierSuffix.$type\n" + + "#end"); + mojo.setOutputFile(rendered); + mojo.execute(); + + assertThat(rendered) + .content() + .isEqualTo("deps:\n" + + " jars:\n" + + " - local:///opt/test/libs/compile-1.0.jar\n" + + " - local:///opt/test/libs/provided-1.0.jar\n" + + " - local:///opt/test/libs/release-1.0.jar\n" + + " - local:///opt/test/libs/runtime-1.0.jar\n" + + " - local:///opt/test/libs/snapshot-2.0-SNAPSHOT.jar\n" + + " - local:///opt/test/libs/system-1.0.jar\n" + + " - local:///opt/test/libs/test-1.0.jar\n"); + } +} diff --git a/src/test/resources/unit/render-dependencies-test/plugin-config.xml b/src/test/resources/unit/render-dependencies-test/plugin-config.xml new file mode 100644 index 00000000..1a046cae --- /dev/null +++ b/src/test/resources/unit/render-dependencies-test/plugin-config.xml @@ -0,0 +1,37 @@ +<!-- + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +--> +<project> + <build> + <plugins> + <plugin> + <artifactId>maven-dependency-plugin</artifactId> + <configuration> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.apache.maven</groupId> + <artifactId>maven-artifact</artifactId> + <version>2.0.4</version> + </dependency> + </dependencies> +</project> \ No newline at end of file