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.Writer;
26  import java.nio.charset.Charset;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.util.ArrayList;
30  import java.util.Comparator;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Objects;
34  import java.util.Set;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  import java.util.stream.Collectors;
38  import java.util.stream.Stream;
39  
40  import org.apache.commons.lang3.StringUtils;
41  import org.apache.maven.artifact.Artifact;
42  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
43  import org.apache.maven.execution.MavenSession;
44  import org.apache.maven.plugin.MojoExecutionException;
45  import org.apache.maven.plugins.annotations.LifecyclePhase;
46  import org.apache.maven.plugins.annotations.Mojo;
47  import org.apache.maven.plugins.annotations.Parameter;
48  import org.apache.maven.plugins.annotations.ResolutionScope;
49  import org.apache.maven.plugins.dependency.utils.DependencyUtil;
50  import org.apache.maven.plugins.dependency.utils.ResolverUtil;
51  import org.apache.maven.project.MavenProject;
52  import org.apache.maven.project.MavenProjectHelper;
53  import org.apache.maven.project.ProjectBuilder;
54  import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
55  import org.sonatype.plexus.build.incremental.BuildContext;
56  
57  /**
58   * This goal outputs a classpath string of dependencies from the local repository to a file or log.
59   *
60   * @author ankostis
61   * @since 2.0-alpha-2
62   */
63  @Mojo(
64          name = "build-classpath",
65          requiresDependencyResolution = ResolutionScope.TEST,
66          defaultPhase = LifecyclePhase.GENERATE_SOURCES,
67          threadSafe = true)
68  public class BuildClasspathMojo extends AbstractDependencyFilterMojo implements Comparator<Artifact> {
69  
70      @Parameter(property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}")
71      private String outputEncoding;
72  
73      /**
74       * Strip artifact version during copy (only works if prefix is set)
75       */
76      @Parameter(property = "mdep.stripVersion", defaultValue = "false")
77      private boolean stripVersion = false;
78  
79      /**
80       * Strip artifact classifier during copy (only works if prefix is set)
81       */
82      @Parameter(property = "mdep.stripClassifier", defaultValue = "false")
83      private boolean stripClassifier = false;
84  
85      /**
86       * The prefix to prepend on each dependent artifact. If undefined, the paths refer to the actual files store in the
87       * local repository (the stripVersion parameter does nothing then).
88       */
89      @Parameter(property = "mdep.prefix")
90      private String prefix;
91  
92      /**
93       * If defined, the name of a property to which the classpath string will be written.
94       * If neither this nor the outputFile parameter is set, the classpath will be logged at INFO level.
95       */
96      @Parameter(property = "mdep.outputProperty")
97      private String outputProperty;
98  
99      /**
100      * If defined, the file to which the classpath string will be written.
101      * If neither this nor the outputProperty parameter is set, the classpath will be logged at INFO level.
102      */
103     @Parameter(property = "mdep.outputFile")
104     private File outputFile;
105 
106     /**
107      * If 'true', it skips the up-to-date-check, and always regenerates the classpath file.
108      */
109     @Parameter(property = "mdep.regenerateFile", defaultValue = "false")
110     private boolean regenerateFile;
111 
112     /**
113      * Override the char used between the paths. This field is initialized to contain the first character of the value
114      * of the system property file.separator. On UNIX systems the value of this field is '/'; on Microsoft Windows
115      * systems it is '\'. The default is File.separator.
116      *
117      * @since 2.0
118      */
119     @Parameter(property = "mdep.fileSeparator", defaultValue = "")
120     private String fileSeparator;
121 
122     /**
123      * Override the char used between path folders. The system-dependent path-separator character. This field is
124      * initialized to contain the first character of the value of the system property path.separator. This character is
125      * used to separate filenames in a sequence of files given as a path list. On UNIX systems, this character is ':';
126      * on Microsoft Windows systems it is ';'.
127      *
128      * @since 2.0
129      */
130     @Parameter(property = "mdep.pathSeparator", defaultValue = "")
131     private String pathSeparator;
132 
133     /**
134      * Replace the absolute path to the local repo with this property. This field is ignored it prefix is declared. The
135      * value will be forced to "${M2_REPO}" if no value is provided AND the attach flag is true.
136      *
137      * @since 2.0
138      */
139     @Parameter(property = "mdep.localRepoProperty", defaultValue = "")
140     private String localRepoProperty;
141 
142     /**
143      * Attach the classpath file to the main artifact so it can be installed and deployed.
144      *
145      * @since 2.0
146      */
147     @Parameter(defaultValue = "false")
148     private boolean attach;
149 
150     /**
151      * Write out the classpath in a format compatible with filtering (classpath=xxxxx)
152      *
153      * @since 2.0
154      */
155     @Parameter(property = "mdep.outputFilterFile", defaultValue = "false")
156     private boolean outputFilterFile;
157 
158     /**
159      * Either append the artifact's baseVersion or uniqueVersion to the filename. Will only be used if
160      * {@link #isStripVersion()} is {@code false}.
161      *
162      * @since 2.6
163      */
164     @Parameter(property = "mdep.useBaseVersion", defaultValue = "true")
165     private boolean useBaseVersion = true;
166 
167     private final MavenProjectHelper projectHelper;
168 
169     @Inject
170     protected BuildClasspathMojo(
171             MavenSession session,
172             BuildContext buildContext,
173             MavenProject project,
174             ResolverUtil resolverUtil,
175             ProjectBuilder projectBuilder,
176             ArtifactHandlerManager artifactHandlerManager,
177             MavenProjectHelper projectHelper) {
178         super(session, buildContext, project, resolverUtil, projectBuilder, artifactHandlerManager);
179         this.projectHelper = projectHelper;
180     }
181 
182     /**
183      * Main entry into mojo. Gets the list of dependencies and iterates to create a classpath.
184      *
185      * @throws MojoExecutionException with a message if an error occurs
186      * @see #getResolvedDependencies(boolean)
187      */
188     @Override
189     protected void doExecute() throws MojoExecutionException {
190         // initialize the separators.
191         boolean isFileSepSet = fileSeparator != null && !fileSeparator.isEmpty();
192         boolean isPathSepSet = pathSeparator != null && !pathSeparator.isEmpty();
193 
194         // don't allow them to have absolute paths when they attach.
195         if (attach && (localRepoProperty == null || localRepoProperty.isEmpty())) {
196             localRepoProperty = "${M2_REPO}";
197         }
198 
199         Set<Artifact> artifacts = getResolvedDependencies(true);
200 
201         if (artifacts == null || artifacts.isEmpty()) {
202             getLog().info("No dependencies found.");
203         }
204 
205         List<Artifact> artList = new ArrayList<>(artifacts);
206 
207         StringBuilder sb = new StringBuilder();
208         Iterator<Artifact> i = artList.iterator();
209 
210         if (i.hasNext()) {
211             appendArtifactPath(i.next(), sb);
212 
213             while (i.hasNext()) {
214                 sb.append(isPathSepSet ? this.pathSeparator : File.pathSeparator);
215                 appendArtifactPath(i.next(), sb);
216             }
217         }
218 
219         String cpString = sb.toString();
220 
221         // if file separator is set, I need to replace the default one from all
222         // the file paths that were pulled from the artifacts
223         if (isFileSepSet) {
224             // Escape file separators to be used as literal strings
225             final String pattern = Pattern.quote(File.separator);
226             final String replacement = Matcher.quoteReplacement(fileSeparator);
227             cpString = cpString.replaceAll(pattern, replacement);
228         }
229 
230         // make the string valid for filtering
231         if (outputFilterFile) {
232             cpString = "classpath=" + cpString;
233         }
234 
235         if (outputProperty != null) {
236             getProject().getProperties().setProperty(outputProperty, cpString);
237             if (getLog().isDebugEnabled()) {
238                 getLog().debug(outputProperty + " = " + cpString);
239             }
240         }
241 
242         if (outputFile != null) {
243             if (regenerateFile || !isUpToDate(cpString)) {
244                 storeClasspathFile(cpString, outputFile);
245             } else {
246                 this.getLog().info("Skipped writing classpath file '" + outputFile + "'.  No changes found.");
247             }
248         }
249 
250         if (outputProperty == null && outputFile == null) {
251             getLog().info("Dependencies classpath:" + System.lineSeparator() + cpString);
252         }
253 
254         if (attach) {
255             attachFile(cpString);
256         }
257     }
258 
259     /**
260      * @param cpString the classpath
261      * @throws MojoExecutionException in case of an error
262      */
263     protected void attachFile(String cpString) throws MojoExecutionException {
264         File attachedFile = new File(getProject().getBuild().getDirectory(), "classpath");
265         storeClasspathFile(cpString, attachedFile);
266 
267         projectHelper.attachArtifact(getProject(), attachedFile, "classpath");
268     }
269 
270     /**
271      * Appends the artifact path to the specified StringBuilder.
272      *
273      * @param art {@link Artifact}
274      * @param sb {@link StringBuilder}
275      */
276     protected void appendArtifactPath(Artifact art, StringBuilder sb) {
277         if (prefix == null) {
278             String file = art.getFile().getPath();
279             // substitute the property for the local repo path to make the classpath file portable.
280             if (localRepoProperty != null && !localRepoProperty.isEmpty()) {
281                 File localBasedir =
282                         session.getRepositorySession().getLocalRepository().getBasedir();
283                 file = StringUtils.replace(file, localBasedir.getAbsolutePath(), localRepoProperty);
284             }
285             sb.append(file);
286         } else {
287             sb.append(prefix);
288             sb.append(File.separator);
289             sb.append(DependencyUtil.getFormattedFileName(
290                     art, this.stripVersion, this.prependGroupId, this.useBaseVersion, this.stripClassifier));
291         }
292     }
293 
294     /**
295      * Checks that new classpath differs from that found inside the old classpathFile.
296      *
297      * @return true if the specified classpath equals the one found inside the file, false otherwise (including when
298      *         file does not exist but new classpath does)
299      */
300     private boolean isUpToDate(String cpString) {
301         try {
302             String oldCp = readClasspathFile();
303             return Objects.equals(cpString, oldCp);
304         } catch (IOException ex) {
305             this.getLog()
306                     .warn("Error while reading old classpath file '" + outputFile + "' for up-to-date check: " + ex);
307 
308             return false;
309         }
310     }
311 
312     /**
313      * Stores the specified string into that file.
314      *
315      * @param cpString the string to write into the file
316      */
317     private void storeClasspathFile(String cpString, File out) throws MojoExecutionException {
318         // make sure the parent path exists.
319         out.getParentFile().mkdirs();
320 
321         String encoding = Objects.toString(outputEncoding, StandardCharsets.UTF_8.name());
322         try (Writer w = Files.newBufferedWriter(out.toPath(), Charset.forName(encoding))) {
323             w.write(cpString);
324             getLog().info("Wrote classpath file '" + out + "'.");
325         } catch (IOException ex) {
326             throw new MojoExecutionException("Error while writing to classpath file '" + out, ex);
327         }
328     }
329 
330     /**
331      * Reads the file specified by the mojo param 'outputFile' into a string. Assumes the field
332      * 'outputFile' is not null.
333      *
334      * @return the string contained in the classpathFile, if it exists, or null otherwise
335      * @throws IOException in case of an error
336      */
337     protected String readClasspathFile() throws IOException {
338         if (outputFile == null) {
339             throw new IllegalArgumentException(
340                     "The outputFile parameter " + "cannot be null if the file is intended to be read.");
341         }
342 
343         if (!outputFile.isFile()) {
344             return null;
345         }
346 
347         String encoding = Objects.toString(outputEncoding, StandardCharsets.UTF_8.name());
348 
349         try (Stream<String> lines = Files.lines(outputFile.toPath(), Charset.forName(encoding))) {
350             return lines.collect(Collectors.joining());
351         }
352     }
353 
354     /**
355      * Compares artifacts lexicographically, using pattern [group_id][artifact_id][version].
356      *
357      * @param art1 first object
358      * @param art2 second object
359      * @return the value <code>0</code> if the argument string is equal to this string; a value less than <code>0</code>
360      *         if this string is lexicographically less than the string argument; and a value greater than
361      *         <code>0</code> if this string is lexicographically greater than the string argument
362      */
363     @Override
364     public int compare(Artifact art1, Artifact art2) {
365         if (art1 == art2) {
366             return 0;
367         } else if (art1 == null) {
368             return -1;
369         } else if (art2 == null) {
370             return +1;
371         }
372 
373         String s1 = art1.getGroupId() + art1.getArtifactId() + art1.getVersion();
374         String s2 = art2.getGroupId() + art2.getArtifactId() + art2.getVersion();
375 
376         return s1.compareTo(s2);
377     }
378 
379     @Override
380     protected ArtifactsFilter getMarkedArtifactFilter() {
381         return null;
382     }
383 
384     /**
385      * @param outputFile the outputFile to set
386      */
387     public void setOutputFile(File outputFile) {
388         this.outputFile = outputFile;
389     }
390 
391     /**
392      * @param theOutputProperty the outputProperty to set
393      */
394     public void setOutputProperty(String theOutputProperty) {
395         this.outputProperty = theOutputProperty;
396     }
397 
398     /**
399      * @param theFileSeparator the fileSeparator to set
400      */
401     public void setFileSeparator(String theFileSeparator) {
402         this.fileSeparator = theFileSeparator;
403     }
404 
405     /**
406      * @param thePathSeparator the pathSeparator to set
407      */
408     public void setPathSeparator(String thePathSeparator) {
409         this.pathSeparator = thePathSeparator;
410     }
411 
412     /**
413      * @param thePrefix the prefix to set
414      */
415     public void setPrefix(String thePrefix) {
416         this.prefix = thePrefix;
417     }
418 
419     /**
420      * @param theRegenerateFile the regenerateFile to set
421      */
422     public void setRegenerateFile(boolean theRegenerateFile) {
423         this.regenerateFile = theRegenerateFile;
424     }
425 
426     /**
427      * @return the stripVersion
428      */
429     public boolean isStripVersion() {
430         return this.stripVersion;
431     }
432 
433     /**
434      * @param theStripVersion the stripVersion to set
435      */
436     public void setStripVersion(boolean theStripVersion) {
437         this.stripVersion = theStripVersion;
438     }
439 
440     /**
441      * @param localRepoProperty {@link #localRepoProperty}
442      */
443     public void setLocalRepoProperty(String localRepoProperty) {
444         this.localRepoProperty = localRepoProperty;
445     }
446 }