View Javadoc

1   package org.apache.maven.plugin.dependency;
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.artifact.factory.ArtifactFactory;
24  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
25  import org.apache.maven.artifact.repository.ArtifactRepository;
26  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
27  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
28  import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
29  import org.apache.maven.artifact.resolver.ArtifactResolver;
30  import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
31  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
32  import org.apache.maven.plugin.AbstractMojo;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugin.MojoFailureException;
35  import org.apache.maven.plugins.annotations.Component;
36  import org.apache.maven.plugins.annotations.Mojo;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
40  import org.apache.maven.shared.artifact.filter.PatternExcludesArtifactFilter;
41  import org.apache.maven.shared.artifact.filter.PatternIncludesArtifactFilter;
42  import org.codehaus.plexus.util.FileUtils;
43  import org.codehaus.plexus.util.StringUtils;
44  
45  import java.io.File;
46  import java.io.IOException;
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.LinkedHashSet;
50  import java.util.List;
51  import java.util.Set;
52  
53  /**
54   * Remove the project dependencies from the local repository, and optionally re-resolve them.
55   * 
56   * @author jdcasey
57   * @version $Id: PurgeLocalRepositoryMojo.java 1401976 2012-10-25 05:04:29Z pgier $
58   * @since 2.0
59   */
60  @Mojo( name = "purge-local-repository", threadSafe = true )
61  public class PurgeLocalRepositoryMojo
62      extends AbstractMojo
63  {
64  
65      public static final String FILE_FUZZINESS = "file";
66  
67      public static final String VERSION_FUZZINESS = "version";
68  
69      public static final String ARTIFACT_ID_FUZZINESS = "artifactId";
70  
71      public static final String GROUP_ID_FUZZINESS = "groupId";
72  
73      /**
74       * The current Maven project.
75       */
76      @Component
77      private MavenProject project;
78  
79      /**
80       * The list of dependencies in the form of groupId:artifactId which should BE deleted/purged from the local
81       * repository. Note that using this parameter will deactivate the normal process for purging the current project
82       * dependency tree. If this parameter is used, only the included artifacts will be purged. The manualIncludes
83       * parameter should not be used in combination with the includes/excludes parameters.
84       * 
85       * @since 2.6
86       */
87      @Parameter
88      private List<String> manualIncludes;
89  
90      /**
91       * Comma-separated list of groupId:artifactId entries, which should be used to manually include artifacts for
92       * deletion. This is a command-line alternative to the <code>manualIncludes</code> parameter, since List parameters
93       * are not currently compatible with CLI specification.
94       * 
95       * @since 2.6
96       */
97      @Parameter( property = "manualInclude" )
98      private String manualInclude;
99  
100     /**
101      * The list of dependencies in the form of groupId:artifactId which should BE deleted/refreshed.
102      * 
103      * @since 2.6
104      */
105     @Parameter
106     private List<String> includes;
107 
108     /**
109      * Comma-separated list of groupId:artifactId entries, which should be used to include artifacts for
110      * deletion/refresh. This is a command-line alternative to the <code>includes</code> parameter, since List
111      * parameters are not currently compatible with CLI specification.
112      * 
113      * @since 2.6
114      */
115     @Parameter( property = "include" )
116     private String include;
117 
118     /**
119      * The list of dependencies in the form of groupId:artifactId which should NOT be deleted/refreshed.
120      */
121     @Parameter
122     private List<String> excludes;
123 
124     /**
125      * Comma-separated list of groupId:artifactId entries, which should be used to exclude artifacts from
126      * deletion/refresh. This is a command-line alternative to the <code>excludes</code> parameter, since List
127      * parameters are not currently compatible with CLI specification.
128      */
129     @Parameter( property = "exclude" )
130     private String exclude;
131 
132     /**
133      * Whether to re-resolve the artifacts once they have been deleted from the local repository. If you are running
134      * this mojo from the command-line, you may want to disable this. By default, artifacts will be re-resolved.
135      */
136     @Parameter( property = "reResolve", defaultValue = "true" )
137     private boolean reResolve;
138 
139     /**
140      * The local repository, from which to delete artifacts.
141      */
142     @Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
143     private ArtifactRepository localRepository;
144 
145     /**
146      * List of Remote Repositories used by the resolver
147      */
148     @Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true )
149     protected List<ArtifactRepository> remoteRepositories;
150 
151     /**
152      * The artifact resolver used to re-resolve dependencies, if that option is enabled.
153      */
154     @Component
155     private ArtifactResolver resolver;
156 
157     /**
158      * The artifact metadata source used to resolve dependencies
159      */
160     @Component
161     private ArtifactMetadataSource metadataSource;
162 
163     /**
164      * Determines how liberally the plugin will delete an artifact from the local repository. Values are: <br/>
165      * <ul>
166      * <li><b>file</b> - Eliminate only the artifact's file.</li>
167      * <li><b>version</b> <i>(default)</i> - Eliminate all files associated with the version of the artifact.</li>
168      * <li><b>artifactId</b> - Eliminate all files associated with the artifact's artifactId.</li>
169      * <li><b>groupId</b> - Eliminate all files associated with the artifact's groupId.</li>
170      * </ul>
171      */
172     @Parameter( property = "resolutionFuzziness", defaultValue = "version" )
173     private String resolutionFuzziness;
174 
175     /**
176      * Whether this mojo should act on all transitive dependencies. Default value is true.
177      */
178     @Parameter( property = "actTransitively", defaultValue = "true" )
179     private boolean actTransitively;
180 
181     /**
182      * Used to construct artifacts for deletion/resolution...
183      */
184     @Component
185     private ArtifactFactory factory;
186 
187     /**
188      * Whether this plugin should output verbose messages. Default is false.
189      */
190     @Parameter( property = "verbose", defaultValue = "false" )
191     private boolean verbose;
192 
193     /**
194      * Whether to purge only snapshot artifacts.
195      * 
196      * @since 2.4
197      */
198     @Parameter( property = "snapshotsOnly", defaultValue = "false" )
199     private boolean snapshotsOnly;
200 
201     /**
202      * Includes only direct project dependencies.
203      */
204     private class DirectDependencyFilter
205         implements ArtifactFilter
206     {
207 
208         private Artifact projectArtifact;
209 
210         private Set<Artifact> directDependencyArtifacts;
211 
212         /**
213          * Default constructor
214          * 
215          * @param directDependencyArtifacts Set of Artifact objects which represent the direct dependencies of the
216          *            project
217          */
218         public DirectDependencyFilter( Artifact projectArtifact, Set<Artifact> directDependencyArtifacts )
219         {
220             this.projectArtifact = projectArtifact;
221             this.directDependencyArtifacts = directDependencyArtifacts;
222         }
223 
224         public boolean include( Artifact artifact )
225         {
226             if ( artifactsGAMatch( artifact, projectArtifact ) )
227             {
228                 return true;
229             }
230             for ( Artifact depArtifact : directDependencyArtifacts )
231             {
232                 if ( this.artifactsGAMatch( artifact, depArtifact ) )
233                 {
234                     return true;
235                 }
236             }
237             return false;
238         }
239 
240         /*
241          * Compare the groupId:artifactId of two artifacts.
242          */
243         private boolean artifactsGAMatch( Artifact artifact1, Artifact artifact2 )
244         {
245             if ( artifact1 == artifact2 )
246             {
247                 return true;
248             }
249 
250             if ( !artifact1.getGroupId().equals( artifact2.getGroupId() ) )
251             {
252                 getLog().debug( "Different groupId: " + artifact1 + "  " + artifact2 );
253                 return false;
254             }
255             if ( !artifact1.getArtifactId().equals( artifact2.getArtifactId() ) )
256             {
257                 getLog().debug( "Different artifactId: " + artifact1 + "  " + artifact2 );
258                 return false;
259             }
260             return true;
261         }
262     }
263 
264     /**
265      * Includes only artifacts that do not use system scope
266      */
267     private class SystemScopeExcludeFilter
268         implements ArtifactFilter
269     {
270         public boolean include( Artifact artifact )
271         {
272             return !Artifact.SCOPE_SYSTEM.equals( artifact.getScope() );
273         }
274     }
275 
276     /**
277      * Includes only snapshot artifacts
278      */
279     private class SnapshotsFilter
280         implements ArtifactFilter
281     {
282         public boolean include( Artifact artifact )
283         {
284             return artifact.isSnapshot();
285         }
286     }
287 
288     public void execute()
289         throws MojoExecutionException, MojoFailureException
290     {
291         if ( !StringUtils.isEmpty( manualInclude ) )
292         {
293             manualIncludes = this.parseIncludes( manualInclude );
294         }
295         // If it's a manual purge, the only step is to delete from the local repo
296         if ( manualIncludes != null && manualIncludes.size() > 0 )
297         {
298             manualPurge( manualIncludes );
299             return;
300         }
301 
302         Set<Artifact> dependencyArtifacts = null;
303 
304         try
305         {
306             dependencyArtifacts = project.createArtifacts( factory, null, null );
307         }
308         catch ( InvalidDependencyVersionException e )
309         {
310             throw new MojoFailureException( "Unable to purge dependencies due to invalid dependency version ", e );
311         }
312 
313         ArtifactFilter artifactFilter = createPurgeArtifactsFilter( dependencyArtifacts );
314 
315         Set<Artifact> resolvedArtifactsToPurge =
316             getFilteredResolvedArtifacts( project, dependencyArtifacts, artifactFilter );
317 
318         if ( resolvedArtifactsToPurge.isEmpty() )
319         {
320             getLog().info( "No artifacts included for purge for project: " + project.getId() );
321             return;
322         }
323 
324         verbose( "Purging dependencies for project: " + project.getId() );
325         purgeArtifacts( resolvedArtifactsToPurge );
326 
327         if ( reResolve )
328         {
329             try
330             {
331                 reResolveArtifacts( project, resolvedArtifactsToPurge, artifactFilter );
332             }
333             catch ( ArtifactResolutionException e )
334             {
335                 String failureMessage = "Failed to refresh project dependencies for: " + project.getId();
336                 MojoFailureException failure = new MojoFailureException( failureMessage );
337                 failure.initCause( e );
338 
339                 throw failure;
340             }
341             catch ( ArtifactNotFoundException e )
342             {
343                 String failureMessage = "Failed to refresh project dependencies for: " + project.getId();
344                 MojoFailureException failure = new MojoFailureException( failureMessage );
345                 failure.initCause( e );
346 
347                 throw failure;
348             }
349         }
350     }
351 
352     /**
353      * Purge/Delete artifacts from the local repository according to the given patterns.
354      * 
355      * @param inclusionPatterns
356      * @throws MojoExecutionException
357      */
358     private void manualPurge( List<String> includes )
359         throws MojoExecutionException
360     {
361         for ( String gavPattern : includes )
362         {
363             if ( StringUtils.isEmpty( gavPattern ) )
364             {
365                 getLog().debug( "Skipping empty gav pattern: " + gavPattern );
366                 continue;
367             }
368 
369             String relativePath = gavToPath( gavPattern );
370             if ( StringUtils.isEmpty( relativePath ) )
371             {
372                 continue;
373             }
374 
375             File purgeDir = new File( localRepository.getBasedir(), relativePath );
376             if ( purgeDir.exists() )
377             {
378                 getLog().debug( "Deleting directory: " + purgeDir );
379                 try
380                 {
381                     FileUtils.deleteDirectory( purgeDir );
382                 }
383                 catch ( IOException e )
384                 {
385                     throw new MojoExecutionException( "Unable to purge directory: " + purgeDir );
386                 }
387             }
388         }
389     }
390 
391     /**
392      * Convert a groupId:artifactId:version to a file system path
393      * 
394      * @param gav, the groupId:artifactId:version string
395      * @return
396      */
397     private String gavToPath( String gav )
398     {
399         if ( StringUtils.isEmpty( gav ) )
400         {
401             return null;
402         }
403 
404         String[] pathComponents = gav.split( ":" );
405 
406         StringBuffer path = new StringBuffer( pathComponents[0].replace( '.', '/' ) );
407 
408         for ( int i = 1; i < pathComponents.length; ++i )
409         {
410             path.append( "/" + pathComponents[i] );
411         }
412 
413         return path.toString();
414     }
415 
416     /**
417      * Create the includes exclude filter to use when resolving and purging dependencies Also excludes any "system"
418      * scope dependencies
419      * 
420      * @param dependencyArtifacts The dependency artifacts to use as a reference if we're excluding transitive
421      *            dependencies
422      * @return
423      */
424     private ArtifactFilter createPurgeArtifactsFilter( Set<Artifact> dependencyArtifacts )
425     {
426         AndArtifactFilter andFilter = new AndArtifactFilter();
427 
428         // System dependencies should never be purged
429         andFilter.add( new SystemScopeExcludeFilter() );
430 
431         if ( this.snapshotsOnly )
432         {
433             andFilter.add( new SnapshotsFilter() );
434         }
435 
436         // The CLI includes/excludes overrides configuration in the pom
437         if ( !StringUtils.isEmpty( this.include ) )
438         {
439             this.includes = parseIncludes( this.include );
440         }
441         if ( this.includes != null )
442         {
443             andFilter.add( new PatternIncludesArtifactFilter( includes ) );
444         }
445 
446         if ( !StringUtils.isEmpty( this.exclude ) )
447         {
448             this.excludes = parseIncludes( this.exclude );
449         }
450         if ( this.excludes != null )
451         {
452             andFilter.add( new PatternExcludesArtifactFilter( excludes ) );
453         }
454 
455         if ( !actTransitively )
456         {
457             andFilter.add( new DirectDependencyFilter( project.getArtifact(), dependencyArtifacts ) );
458         }
459 
460         return andFilter;
461     }
462 
463     /**
464      * Convert comma separated list of includes to List object
465      * 
466      * @param include
467      * @return the includes list
468      */
469     private List<String> parseIncludes( String include )
470     {
471         List<String> includes = new ArrayList<String>();
472 
473         if ( include != null )
474         {
475             String[] elements = include.split( "," );
476             includes.addAll( Arrays.asList( elements ) );
477         }
478 
479         return includes;
480     }
481 
482     private Set<Artifact> getFilteredResolvedArtifacts( MavenProject project, Set<Artifact> artifacts,
483                                                         ArtifactFilter filter )
484     {
485         try
486         {
487             ArtifactResolutionResult result =
488                 resolver.resolveTransitively( artifacts, project.getArtifact(), localRepository, remoteRepositories,
489                                               metadataSource, filter );
490 
491             @SuppressWarnings( "unchecked" )
492             Set<Artifact> resolvedArtifacts = result.getArtifacts();
493 
494             return resolvedArtifacts;
495         }
496         catch ( ArtifactResolutionException e )
497         {
498             getLog().info( "Unable to resolve all dependencies for : " + e.getGroupId() + ":" + e.getArtifactId() + ":"
499                                + e.getVersion()
500                                + ". Falling back to non-transitive mode for initial artifact resolution." );
501         }
502         catch ( ArtifactNotFoundException e )
503         {
504             getLog().info( "Unable to resolve all dependencies for : " + e.getGroupId() + ":" + e.getArtifactId() + ":"
505                                + e.getVersion()
506                                + ". Falling back to non-transitive mode for initial artifact resolution." );
507         }
508 
509         Set<Artifact> resolvedArtifacts = new LinkedHashSet<Artifact>();
510         // Resolve the only poms here instead of the actual artifacts, because the files will be deleted during the
511         // purge anyway
512         for ( Artifact artifact : artifacts )
513         {
514             if ( filter.include( artifact ) )
515             {
516                 try
517                 {
518                     resolvedArtifacts.add( artifact );
519                     resolver.resolve( artifact, remoteRepositories, localRepository );
520                 }
521                 catch ( ArtifactResolutionException e )
522                 {
523                     getLog().debug( "Unable to resolve artifact: " + artifact );
524                 }
525                 catch ( ArtifactNotFoundException e )
526                 {
527                     getLog().debug( "Unable to resolve artifact: " + artifact );
528                 }
529             }
530         }
531         return resolvedArtifacts;
532     }
533 
534     private void purgeArtifacts( Set<Artifact> artifacts )
535         throws MojoFailureException
536     {
537         for ( Artifact artifact : artifacts )
538         {
539             verbose( "Purging artifact: " + artifact.getId() );
540 
541             File deleteTarget = findDeleteTarget( artifact );
542 
543             verbose( "Deleting: " + deleteTarget );
544 
545             if ( deleteTarget.isDirectory() )
546             {
547                 try
548                 {
549                     FileUtils.deleteDirectory( deleteTarget );
550                 }
551                 catch ( IOException e )
552                 {
553                     getLog().warn( "Unable to purge local repository location: " + deleteTarget, e );
554                 }
555             }
556             else
557             {
558                 if ( !deleteTarget.delete() )
559                 {
560                     getLog().warn( "Unable to purge local repository location: " + deleteTarget );
561                 }
562             }
563             artifact.setResolved( false );
564         }
565     }
566 
567     private void reResolveArtifacts( MavenProject project, Set<Artifact> artifacts, ArtifactFilter filter )
568         throws ArtifactResolutionException, ArtifactNotFoundException
569     {
570 
571         // Always need to re-resolve the poms in case they were purged along with the artifact
572         // because Maven 2 will not automatically re-resolve them when resolving the artifact
573         for ( Artifact artifact : artifacts )
574         {
575             try
576             {
577                 Artifact pomArtifact =
578                     factory.createArtifact( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(),
579                                             null, "pom" );
580                 resolver.resolveAlways( pomArtifact, remoteRepositories, localRepository );
581             }
582             catch ( ArtifactResolutionException e )
583             {
584                 verbose( e.getMessage() );
585             }
586             catch ( ArtifactNotFoundException e )
587             {
588                 verbose( e.getMessage() );
589             }
590         }
591 
592         List<Artifact> missingArtifacts = new ArrayList<Artifact>();
593 
594         for ( Artifact artifact : artifacts )
595         {
596             verbose( "Resolving artifact: " + artifact.getId() );
597 
598             try
599             {
600                 resolver.resolveAlways( artifact, project.getRemoteArtifactRepositories(), localRepository );
601             }
602             catch ( ArtifactResolutionException e )
603             {
604                 verbose( e.getMessage() );
605                 missingArtifacts.add( artifact );
606             }
607             catch ( ArtifactNotFoundException e )
608             {
609                 verbose( e.getMessage() );
610                 missingArtifacts.add( artifact );
611             }
612         }
613 
614         if ( missingArtifacts.size() > 0 )
615         {
616             String message = "required artifacts missing:\n";
617             for ( Artifact missingArtifact : missingArtifacts )
618             {
619                 message += "  " + missingArtifact.getId() + "\n";
620             }
621             message += "\nfor the artifact:";
622 
623             throw new ArtifactResolutionException( message, project.getArtifact(),
624                                                    project.getRemoteArtifactRepositories() );
625         }
626     }
627 
628     private File findDeleteTarget( Artifact artifact )
629     {
630         // Use localRepository.pathOf() in case artifact.getFile() is not set
631         File deleteTarget = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
632 
633         if ( GROUP_ID_FUZZINESS.equals( resolutionFuzziness ) )
634         {
635             // get the groupId dir.
636             deleteTarget = deleteTarget.getParentFile().getParentFile().getParentFile();
637         }
638         else if ( ARTIFACT_ID_FUZZINESS.equals( resolutionFuzziness ) )
639         {
640             // get the artifactId dir.
641             deleteTarget = deleteTarget.getParentFile().getParentFile();
642         }
643         else if ( VERSION_FUZZINESS.equals( resolutionFuzziness ) )
644         {
645             // get the version dir.
646             deleteTarget = deleteTarget.getParentFile();
647         }
648         // else it's file fuzziness.
649         return deleteTarget;
650     }
651 
652     private void verbose( String message )
653     {
654         if ( verbose || getLog().isDebugEnabled() )
655         {
656             getLog().info( message );
657         }
658     }
659 
660 }