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