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