View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.dependency.fromDependencies;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.StringWriter;
26  import java.io.Writer;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.nio.file.Paths;
32  import java.util.Collections;
33  import java.util.Comparator;
34  import java.util.List;
35  import java.util.Objects;
36  import java.util.Properties;
37  import java.util.function.Function;
38  import java.util.stream.Collectors;
39  
40  import org.apache.maven.artifact.Artifact;
41  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
42  import org.apache.maven.execution.MavenSession;
43  import org.apache.maven.plugin.MojoExecutionException;
44  import org.apache.maven.plugins.annotations.LifecyclePhase;
45  import org.apache.maven.plugins.annotations.Mojo;
46  import org.apache.maven.plugins.annotations.Parameter;
47  import org.apache.maven.plugins.annotations.ResolutionScope;
48  import org.apache.maven.plugins.dependency.utils.ResolverUtil;
49  import org.apache.maven.project.MavenProject;
50  import org.apache.maven.project.MavenProjectHelper;
51  import org.apache.maven.project.ProjectBuilder;
52  import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
53  import org.apache.velocity.Template;
54  import org.apache.velocity.VelocityContext;
55  import org.apache.velocity.app.VelocityEngine;
56  import org.apache.velocity.tools.generic.CollectionTool;
57  import org.sonatype.plexus.build.incremental.BuildContext;
58  
59  import static java.util.Optional.ofNullable;
60  
61  /**
62   * This goal renders dependencies based on a velocity template.
63   *
64   * @since 3.9.0
65   */
66  @Mojo(
67          name = "render-dependencies",
68          requiresDependencyResolution = ResolutionScope.TEST,
69          defaultPhase = LifecyclePhase.GENERATE_SOURCES,
70          threadSafe = true)
71  public class RenderDependenciesMojo extends AbstractDependencyFilterMojo {
72      /**
73       * Encoding to write the rendered template.
74       * @since 3.9.0
75       */
76      @Parameter(property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}")
77      private String outputEncoding;
78  
79      /**
80       * The file to write the rendered template string. If undefined, it just prints the classpath as [INFO].
81       * @since 3.9.0
82       */
83      @Parameter(property = "mdep.outputFile")
84      private File outputFile;
85  
86      /**
87       * If not null or empty it will attach the artifact with this classifier.
88       * @since 3.9.0
89       */
90      @Parameter(property = "mdep.classifier", defaultValue = "template")
91      private String classifier;
92  
93      /**
94       * Extension to use for the attached file if classifier is not null/empty.
95       * @since 3.9.0
96       */
97      @Parameter(property = "mdep.extension", defaultValue = "txt")
98      private String extension;
99  
100     /**
101      * Velocity template to use to render the output file.
102      * It can be inline or a file path.
103      * @since 3.9.0
104      */
105     @Parameter(property = "mdep.template", required = true)
106     private String template;
107 
108     private final MavenProjectHelper projectHelper;
109 
110     @Inject
111     protected RenderDependenciesMojo(
112             MavenSession session,
113             BuildContext buildContext,
114             MavenProject project,
115             ResolverUtil resolverUtil,
116             ProjectBuilder projectBuilder,
117             ArtifactHandlerManager artifactHandlerManager,
118             MavenProjectHelper projectHelper) {
119         super(session, buildContext, project, resolverUtil, projectBuilder, artifactHandlerManager);
120         this.projectHelper = projectHelper;
121     }
122 
123     /**
124      * Main entry into mojo.
125      *
126      * @throws MojoExecutionException with a message if an error occurs
127      */
128     @Override
129     protected void doExecute() throws MojoExecutionException {
130         // sort them to ease template work and ensure it is deterministic
131         final List<Artifact> artifacts =
132                 ofNullable(getResolvedDependencies(true)).orElseGet(Collections::emptySet).stream()
133                         .sorted(Comparator.comparing(Artifact::getGroupId)
134                                 .thenComparing(Artifact::getArtifactId)
135                                 .thenComparing(Artifact::getBaseVersion)
136                                 .thenComparing(orEmpty(Artifact::getClassifier))
137                                 .thenComparing(orEmpty(Artifact::getType)))
138                         .collect(Collectors.toList());
139 
140         if (artifacts.isEmpty()) {
141             getLog().warn("No dependencies found.");
142         }
143 
144         final String rendered = render(artifacts);
145 
146         if (outputFile == null) {
147             getLog().info(rendered);
148         } else {
149             store(rendered, outputFile);
150         }
151         if (classifier != null && !classifier.isEmpty()) {
152             attachFile(rendered);
153         }
154     }
155 
156     /**
157      * Do render the template.
158      * @param artifacts input.
159      * @return the template rendered.
160      */
161     private String render(final List<Artifact> artifacts) {
162         final Path templatePath = getTemplatePath();
163         final boolean fromFile = templatePath != null && Files.exists(templatePath);
164 
165         final Properties props = new Properties();
166         props.setProperty("runtime.strict_mode.enable", "true");
167         if (fromFile) {
168             props.setProperty(
169                     "resource.loader.file.path",
170                     templatePath.toAbsolutePath().getParent().toString());
171         }
172 
173         final VelocityEngine ve = new VelocityEngine(props);
174         ve.init();
175 
176         final VelocityContext context = new VelocityContext();
177         context.put("artifacts", artifacts);
178         context.put("sorter", new CollectionTool());
179 
180         // Merge template + context
181         final StringWriter writer = new StringWriter();
182         try (StringWriter ignored = writer) {
183             if (fromFile) {
184                 final Template template =
185                         ve.getTemplate(templatePath.getFileName().toString());
186                 template.merge(context, writer);
187             } else {
188                 ve.evaluate(context, writer, "tpl-" + Math.abs(hashCode()), template);
189             }
190         } catch (final IOException e) {
191             // no-op, not possible
192         }
193 
194         return writer.toString();
195     }
196 
197     private Path getTemplatePath() {
198         try {
199             return Paths.get(template);
200         } catch (final RuntimeException re) {
201             return null;
202         }
203     }
204 
205     /**
206      * Trivial null protection impl for comparing callback.
207      * @param getter nominal getter.
208      * @return a comparer of getter defaulting on empty if getter value is null.
209      */
210     private Comparator<Artifact> orEmpty(final Function<Artifact, String> getter) {
211         return Comparator.comparing(a -> ofNullable(getter.apply(a)).orElse(""));
212     }
213 
214     /**
215      * @param content the rendered template
216      * @throws MojoExecutionException in case of an error
217      */
218     protected void attachFile(final String content) throws MojoExecutionException {
219         final File attachedFile;
220         if (outputFile == null) {
221             attachedFile = new File(getProject().getBuild().getDirectory(), classifier);
222             store(content, attachedFile);
223         } else { // already written
224             attachedFile = outputFile;
225         }
226         projectHelper.attachArtifact(getProject(), extension, classifier, attachedFile);
227     }
228 
229     /**
230      * Stores the specified string into that file.
231      *
232      * @param content the string to write into the file
233      */
234     private void store(final String content, final File out) throws MojoExecutionException {
235         // make sure the parent path exists.
236         final Path parent = out.toPath().getParent();
237         if (parent != null) {
238             try {
239                 Files.createDirectories(parent);
240             } catch (final IOException e) {
241                 throw new MojoExecutionException(e);
242             }
243         }
244 
245         final String encoding = Objects.toString(outputEncoding, StandardCharsets.UTF_8.name());
246         try (Writer w = Files.newBufferedWriter(out.toPath(), Charset.forName(encoding))) {
247             w.write(content);
248             getLog().info("Wrote file '" + out + "'.");
249         } catch (final IOException ex) {
250             throw new MojoExecutionException("Error while writing to file '" + out, ex);
251         }
252     }
253 
254     @Override
255     protected ArtifactsFilter getMarkedArtifactFilter() {
256         return null;
257     }
258 
259     public void setExtension(final String extension) {
260         this.extension = extension;
261     }
262 
263     public void setOutputEncoding(final String outputEncoding) {
264         this.outputEncoding = outputEncoding;
265     }
266 
267     public void setOutputFile(final File outputFile) {
268         this.outputFile = outputFile;
269     }
270 
271     public void setClassifier(final String classifier) {
272         this.classifier = classifier;
273     }
274 
275     public void setTemplate(final String template) {
276         this.template = template;
277     }
278 }