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