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