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

Reply via email to