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