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 java.io.BufferedReader;
23  import java.io.BufferedWriter;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.io.OutputStreamWriter;
30  import java.io.Writer;
31  import java.util.ArrayList;
32  import java.util.Comparator;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Objects;
36  import java.util.Set;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
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  import org.codehaus.plexus.util.StringUtils;
53  
54  /**
55   * This goal outputs a classpath string of dependencies from the local repository to a file or log.
56   *
57   * @author ankostis
58   * @since 2.0-alpha-2
59   */
60  // CHECKSTYLE_OFF: LineLength
61  @Mojo( name = "build-classpath", requiresDependencyResolution = ResolutionScope.TEST, defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true )
62  // CHECKSTYLE_ON: LineLength
63  public class BuildClasspathMojo
64      extends AbstractDependencyFilterMojo
65      implements Comparator<Artifact>
66  {
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()
180         throws MojoExecutionException
181     {
182         // initialize the separators.
183         boolean isFileSepSet = StringUtils.isNotEmpty( fileSeparator );
184         boolean isPathSepSet = StringUtils.isNotEmpty( pathSeparator );
185 
186         // don't allow them to have absolute paths when they attach.
187         if ( attach && StringUtils.isEmpty( localRepoProperty ) )
188         {
189             localRepoProperty = "${M2_REPO}";
190         }
191 
192         Set<Artifact> artifacts = getResolvedDependencies( true );
193 
194         if ( artifacts == null || artifacts.isEmpty() )
195         {
196             getLog().info( "No dependencies found." );
197         }
198 
199         List<Artifact> artList = new ArrayList<>( artifacts );
200 
201         StringBuilder sb = new StringBuilder();
202         Iterator<Artifact> i = artList.iterator();
203 
204         if ( i.hasNext() )
205         {
206             appendArtifactPath( i.next(), sb );
207 
208             while ( i.hasNext() )
209             {
210                 sb.append( isPathSepSet ? this.pathSeparator : File.pathSeparator );
211                 appendArtifactPath( i.next(), sb );
212             }
213         }
214 
215         String cpString = sb.toString();
216 
217         // if file separator is set, I need to replace the default one from all
218         // the file paths that were pulled from the artifacts
219         if ( isFileSepSet )
220         {
221             // Escape file separators to be used as literal strings
222             final String pattern = Pattern.quote( File.separator );
223             final String replacement = Matcher.quoteReplacement( fileSeparator );
224             cpString = cpString.replaceAll( pattern, replacement );
225         }
226 
227         // make the string valid for filtering
228         if ( outputFilterFile )
229         {
230             cpString = "classpath=" + cpString;
231         }
232 
233         if ( outputProperty != null )
234         {
235             getProject().getProperties().setProperty( outputProperty, cpString );
236             if ( getLog().isDebugEnabled() )
237             {
238                 getLog().debug( outputProperty + " = " + cpString );
239             }
240         }
241 
242         if ( outputFile == null )
243         {
244             getLog().info( "Dependencies classpath:" + System.lineSeparator() + cpString );
245         }
246         else
247         {
248             if ( regenerateFile || !isUpToDate( cpString ) )
249             {
250                 storeClasspathFile( cpString, outputFile );
251             }
252             else
253             {
254                 this.getLog().info( "Skipped writing classpath file '" + outputFile + "'.  No changes found." );
255             }
256         }
257         if ( attach )
258         {
259             attachFile( cpString );
260         }
261     }
262 
263     /**
264      * @param cpString The classpath.
265      * @throws MojoExecutionException in case of an error.
266      */
267     protected void attachFile( String cpString )
268         throws MojoExecutionException
269     {
270         File attachedFile = new File( getProject().getBuild().getDirectory(), "classpath" );
271         storeClasspathFile( cpString, attachedFile );
272 
273         projectHelper.attachArtifact( getProject(), attachedFile, "classpath" );
274     }
275 
276     /**
277      * Appends the artifact path into the specified StringBuilder.
278      *
279      * @param art {@link Artifact}
280      * @param sb {@link StringBuilder}
281      */
282     protected void appendArtifactPath( Artifact art, StringBuilder sb )
283     {
284         if ( prefix == null )
285         {
286             String file = art.getFile().getPath();
287             // substitute the property for the local repo path to make the classpath file portable.
288             if ( StringUtils.isNotEmpty( localRepoProperty ) )
289             {
290                 ProjectBuildingRequest projectBuildingRequest = session.getProjectBuildingRequest();
291                 File localBasedir = repositoryManager.getLocalRepositoryBasedir( projectBuildingRequest );
292 
293                 file = StringUtils.replace( file, localBasedir.getAbsolutePath(), localRepoProperty );
294             }
295             sb.append( file );
296         }
297         else
298         {
299             // TODO: add param for prepending groupId and version.
300             sb.append( prefix );
301             sb.append( File.separator );
302             sb.append( DependencyUtil.getFormattedFileName( art, this.stripVersion, this.prependGroupId,
303                                                             this.useBaseVersion, this.stripClassifier ) );
304         }
305     }
306 
307     /**
308      * Checks that new classpath differs from that found inside the old classpathFile.
309      *
310      * @return true if the specified classpath equals the one found inside the file, false otherwise (including when
311      *         file does not exist but new classpath does).
312      */
313     private boolean isUpToDate( String cpString )
314     {
315         try
316         {
317             String oldCp = readClasspathFile();
318             return ( cpString == null ? oldCp == null : cpString.equals( oldCp ) );
319         }
320         catch ( IOException ex )
321         {
322             this.getLog().warn( "Error while reading old classpath file '" + outputFile + "' for up-to-date check: "
323                 + ex );
324 
325             return false;
326         }
327     }
328 
329     /**
330      * Stores the specified string into that file.
331      *
332      * @param cpString the string to write into the file
333      */
334     private void storeClasspathFile( String cpString, File out )
335         throws MojoExecutionException
336     {
337         // make sure the parent path exists.
338         out.getParentFile().mkdirs();
339         
340         String encoding = Objects.toString( outputEncoding, "UTF-8" );
341 
342         try ( Writer w =
343             new BufferedWriter( new OutputStreamWriter( new FileOutputStream( out ), encoding ) ) )
344         {
345             w.write( cpString );
346             getLog().info( "Wrote classpath file '" + out + "'." );
347         }
348         catch ( IOException ex )
349         {
350             throw new MojoExecutionException( "Error while writing to classpath file '" + out + "': " + ex.toString(),
351                                               ex );
352         }
353     }
354 
355     /**
356      * Reads the file specified by the mojo param 'outputFile' into a string. Assumes the field
357      * 'outputFile' is not null.
358      * 
359      * @return the string contained in the classpathFile, if it 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         String encoding = Objects.toString( outputEncoding, "UTF-8" );
377 
378         try ( BufferedReader r =
379             new BufferedReader( new InputStreamReader( new FileInputStream( outputFile ), encoding ) ) )
380         {
381             for ( String line = r.readLine(); line != null; line = r.readLine() )
382             {
383                 sb.append( line );
384             }
385 
386             return sb.toString();
387         }
388     }
389 
390     /**
391      * Compares artifacts lexicographically, using pattern [group_id][artifact_id][version].
392      *
393      * @param art1 first object
394      * @param art2 second object
395      * @return the value <code>0</code> if the argument string is equal to this string; a value less than <code>0</code>
396      *         if this string is lexicographically less than the string argument; and a value greater than
397      *         <code>0</code> if this string is lexicographically greater than the string argument.
398      */
399     @Override
400     public int compare( Artifact art1, Artifact art2 )
401     {
402         if ( art1 == art2 )
403         {
404             return 0;
405         }
406         else if ( art1 == null )
407         {
408             return -1;
409         }
410         else if ( art2 == null )
411         {
412             return +1;
413         }
414 
415         String s1 = art1.getGroupId() + art1.getArtifactId() + art1.getVersion();
416         String s2 = art2.getGroupId() + art2.getArtifactId() + art2.getVersion();
417 
418         return s1.compareTo( s2 );
419     }
420 
421     @Override
422     protected ArtifactsFilter getMarkedArtifactFilter()
423     {
424         return null;
425     }
426 
427     /**
428      * @param outputFile the outputFile to set
429      */
430     public void setOutputFile( File outputFile )
431     {
432         this.outputFile = outputFile;
433     }
434 
435     /**
436      * @param theOutputProperty the outputProperty to set
437      */
438     public void setOutputProperty( String theOutputProperty )
439     {
440         this.outputProperty = theOutputProperty;
441     }
442 
443     /**
444      * @param theFileSeparator the fileSeparator to set
445      */
446     public void setFileSeparator( String theFileSeparator )
447     {
448         this.fileSeparator = theFileSeparator;
449     }
450 
451     /**
452      * @param thePathSeparator the pathSeparator to set
453      */
454     public void setPathSeparator( String thePathSeparator )
455     {
456         this.pathSeparator = thePathSeparator;
457     }
458 
459     /**
460      * @param thePrefix the prefix to set
461      */
462     public void setPrefix( String thePrefix )
463     {
464         this.prefix = thePrefix;
465     }
466 
467     /**
468      * @param theRegenerateFile the regenerateFile to set
469      */
470     public void setRegenerateFile( boolean theRegenerateFile )
471     {
472         this.regenerateFile = theRegenerateFile;
473     }
474 
475     /**
476      * @return the stripVersion
477      */
478     public boolean isStripVersion()
479     {
480         return this.stripVersion;
481     }
482 
483     /**
484      * @param theStripVersion the stripVersion to set
485      */
486     public void setStripVersion( boolean theStripVersion )
487     {
488         this.stripVersion = theStripVersion;
489     }
490 
491     /**
492      * @param localRepoProperty {@link #localRepoProperty}
493      */
494     public void setLocalRepoProperty( String localRepoProperty )
495     {
496         this.localRepoProperty = localRepoProperty;
497     }
498 }