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.html 937155 2015-01-21 21:53:50Z khmarbaise $
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      @Parameter( defaultValue = "${project}", readonly = true, required = true )
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      * Skip plugin execution completely.
203      *
204      * @since 2.7
205      */
206     @Parameter( property = "skip", defaultValue = "false" )
207     private boolean skip;
208 
209     /**
210      * Includes only direct project dependencies.
211      */
212     private class DirectDependencyFilter
213         implements ArtifactFilter
214     {
215 
216         private Artifact projectArtifact;
217 
218         private Set<Artifact> directDependencyArtifacts;
219 
220         /**
221          * Default constructor
222          * 
223          * @param directDependencyArtifacts Set of Artifact objects which represent the direct dependencies of the
224          *            project
225          */
226         public DirectDependencyFilter( Artifact projectArtifact, Set<Artifact> directDependencyArtifacts )
227         {
228             this.projectArtifact = projectArtifact;
229             this.directDependencyArtifacts = directDependencyArtifacts;
230         }
231 
232         public boolean include( Artifact artifact )
233         {
234             if ( artifactsGAMatch( artifact, projectArtifact ) )
235             {
236                 return true;
237             }
238             for ( Artifact depArtifact : directDependencyArtifacts )
239             {
240                 if ( this.artifactsGAMatch( artifact, depArtifact ) )
241                 {
242                     return true;
243                 }
244             }
245             return false;
246         }
247 
248         /*
249          * Compare the groupId:artifactId of two artifacts.
250          */
251         private boolean artifactsGAMatch( Artifact artifact1, Artifact artifact2 )
252         {
253             if ( artifact1 == artifact2 )
254             {
255                 return true;
256             }
257 
258             if ( !artifact1.getGroupId().equals( artifact2.getGroupId() ) )
259             {
260                 getLog().debug( "Different groupId: " + artifact1 + "  " + artifact2 );
261                 return false;
262             }
263             if ( !artifact1.getArtifactId().equals( artifact2.getArtifactId() ) )
264             {
265                 getLog().debug( "Different artifactId: " + artifact1 + "  " + artifact2 );
266                 return false;
267             }
268             return true;
269         }
270     }
271 
272     /**
273      * Includes only artifacts that do not use system scope
274      */
275     private class SystemScopeExcludeFilter
276         implements ArtifactFilter
277     {
278         public boolean include( Artifact artifact )
279         {
280             return !Artifact.SCOPE_SYSTEM.equals( artifact.getScope() );
281         }
282     }
283 
284     /**
285      * Includes only snapshot artifacts
286      */
287     private class SnapshotsFilter
288         implements ArtifactFilter
289     {
290         public boolean include( Artifact artifact )
291         {
292             return artifact.isSnapshot();
293         }
294     }
295 
296     public void execute()
297         throws MojoExecutionException, MojoFailureException
298     {
299         if ( isSkip() )
300         {
301             getLog().info( "Skipping plugin execution" );
302             return;
303         }
304 
305         if ( !StringUtils.isEmpty( manualInclude ) )
306         {
307             manualIncludes = this.parseIncludes( manualInclude );
308         }
309         // If it's a manual purge, the only step is to delete from the local repo
310         if ( manualIncludes != null && manualIncludes.size() > 0 )
311         {
312             manualPurge( manualIncludes );
313             return;
314         }
315 
316         Set<Artifact> dependencyArtifacts;
317 
318         try
319         {
320             dependencyArtifacts = project.createArtifacts( factory, null, null );
321         }
322         catch ( InvalidDependencyVersionException e )
323         {
324             throw new MojoFailureException( "Unable to purge dependencies due to invalid dependency version ", e );
325         }
326 
327         ArtifactFilter artifactFilter = createPurgeArtifactsFilter( dependencyArtifacts );
328 
329         Set<Artifact> resolvedArtifactsToPurge =
330             getFilteredResolvedArtifacts( project, dependencyArtifacts, artifactFilter );
331 
332         if ( resolvedArtifactsToPurge.isEmpty() )
333         {
334             getLog().info( "No artifacts included for purge for project: " + project.getId() );
335             return;
336         }
337 
338         verbose( "Purging dependencies for project: " + project.getId() );
339         purgeArtifacts( resolvedArtifactsToPurge );
340 
341         if ( reResolve )
342         {
343             try
344             {
345                 reResolveArtifacts( project, resolvedArtifactsToPurge, artifactFilter );
346             }
347             catch ( ArtifactResolutionException e )
348             {
349                 String failureMessage = "Failed to refresh project dependencies for: " + project.getId();
350                 MojoFailureException failure = new MojoFailureException( failureMessage );
351                 failure.initCause( e );
352 
353                 throw failure;
354             }
355             catch ( ArtifactNotFoundException e )
356             {
357                 String failureMessage = "Failed to refresh project dependencies for: " + project.getId();
358                 MojoFailureException failure = new MojoFailureException( failureMessage );
359                 failure.initCause( e );
360 
361                 throw failure;
362             }
363         }
364     }
365 
366     /**
367      * Purge/Delete artifacts from the local repository according to the given patterns.
368      * 
369      * @param inclusionPatterns
370      * @throws MojoExecutionException
371      */
372     private void manualPurge( List<String> includes )
373         throws MojoExecutionException
374     {
375         for ( String gavPattern : includes )
376         {
377             if ( StringUtils.isEmpty( gavPattern ) )
378             {
379                 getLog().debug( "Skipping empty gav pattern: " + gavPattern );
380                 continue;
381             }
382 
383             String relativePath = gavToPath( gavPattern );
384             if ( StringUtils.isEmpty( relativePath ) )
385             {
386                 continue;
387             }
388 
389             File purgeDir = new File( localRepository.getBasedir(), relativePath );
390             if ( purgeDir.exists() )
391             {
392                 getLog().debug( "Deleting directory: " + purgeDir );
393                 try
394                 {
395                     FileUtils.deleteDirectory( purgeDir );
396                 }
397                 catch ( IOException e )
398                 {
399                     throw new MojoExecutionException( "Unable to purge directory: " + purgeDir );
400                 }
401             }
402         }
403     }
404 
405     /**
406      * Convert a groupId:artifactId:version to a file system path
407      * 
408      * @param gav, the groupId:artifactId:version string
409      * @return
410      */
411     private String gavToPath( String gav )
412     {
413         if ( StringUtils.isEmpty( gav ) )
414         {
415             return null;
416         }
417 
418         String[] pathComponents = gav.split( ":" );
419 
420         StringBuilder path = new StringBuilder( pathComponents[0].replace( '.', '/' ) );
421 
422         for ( int i = 1; i < pathComponents.length; ++i )
423         {
424             path.append( "/" ).append( pathComponents[i] );
425         }
426 
427         return path.toString();
428     }
429 
430     /**
431      * Create the includes exclude filter to use when resolving and purging dependencies Also excludes any "system"
432      * scope dependencies
433      * 
434      * @param dependencyArtifacts The dependency artifacts to use as a reference if we're excluding transitive
435      *            dependencies
436      * @return
437      */
438     private ArtifactFilter createPurgeArtifactsFilter( Set<Artifact> dependencyArtifacts )
439     {
440         AndArtifactFilter andFilter = new AndArtifactFilter();
441 
442         // System dependencies should never be purged
443         andFilter.add( new SystemScopeExcludeFilter() );
444 
445         if ( this.snapshotsOnly )
446         {
447             andFilter.add( new SnapshotsFilter() );
448         }
449 
450         // The CLI includes/excludes overrides configuration in the pom
451         if ( !StringUtils.isEmpty( this.include ) )
452         {
453             this.includes = parseIncludes( this.include );
454         }
455         if ( this.includes != null )
456         {
457             andFilter.add( new PatternIncludesArtifactFilter( includes ) );
458         }
459 
460         if ( !StringUtils.isEmpty( this.exclude ) )
461         {
462             this.excludes = parseIncludes( this.exclude );
463         }
464         if ( this.excludes != null )
465         {
466             andFilter.add( new PatternExcludesArtifactFilter( excludes ) );
467         }
468 
469         if ( !actTransitively )
470         {
471             andFilter.add( new DirectDependencyFilter( project.getArtifact(), dependencyArtifacts ) );
472         }
473 
474         return andFilter;
475     }
476 
477     /**
478      * Convert comma separated list of includes to List object
479      * 
480      * @param include
481      * @return the includes list
482      */
483     private List<String> parseIncludes( String include )
484     {
485         List<String> includes = new ArrayList<String>();
486 
487         if ( include != null )
488         {
489             String[] elements = include.split( "," );
490             includes.addAll( Arrays.asList( elements ) );
491         }
492 
493         return includes;
494     }
495 
496     private Set<Artifact> getFilteredResolvedArtifacts( MavenProject project, Set<Artifact> artifacts,
497                                                         ArtifactFilter filter )
498     {
499         try
500         {
501             ArtifactResolutionResult result =
502                 resolver.resolveTransitively( artifacts, project.getArtifact(), localRepository, remoteRepositories,
503                                               metadataSource, filter );
504 
505             @SuppressWarnings( "unchecked" )
506             Set<Artifact> resolvedArtifacts = result.getArtifacts();
507 
508             return resolvedArtifacts;
509         }
510         catch ( ArtifactResolutionException e )
511         {
512             getLog().info( "Unable to resolve all dependencies for : " + e.getGroupId() + ":" + e.getArtifactId() + ":"
513                                + e.getVersion()
514                                + ". Falling back to non-transitive mode for initial artifact resolution." );
515         }
516         catch ( ArtifactNotFoundException e )
517         {
518             getLog().info( "Unable to resolve all dependencies for : " + e.getGroupId() + ":" + e.getArtifactId() + ":"
519                                + e.getVersion()
520                                + ". Falling back to non-transitive mode for initial artifact resolution." );
521         }
522 
523         Set<Artifact> resolvedArtifacts = new LinkedHashSet<Artifact>();
524         // Resolve the only poms here instead of the actual artifacts, because the files will be deleted during the
525         // purge anyway
526         for ( Artifact artifact : artifacts )
527         {
528             if ( filter.include( artifact ) )
529             {
530                 try
531                 {
532                     resolvedArtifacts.add( artifact );
533                     resolver.resolve( artifact, remoteRepositories, localRepository );
534                 }
535                 catch ( ArtifactResolutionException e )
536                 {
537                     getLog().debug( "Unable to resolve artifact: " + artifact );
538                 }
539                 catch ( ArtifactNotFoundException e )
540                 {
541                     getLog().debug( "Unable to resolve artifact: " + artifact );
542                 }
543             }
544         }
545         return resolvedArtifacts;
546     }
547 
548     private void purgeArtifacts( Set<Artifact> artifacts )
549         throws MojoFailureException
550     {
551         for ( Artifact artifact : artifacts )
552         {
553             verbose( "Purging artifact: " + artifact.getId() );
554 
555             File deleteTarget = findDeleteTarget( artifact );
556 
557             verbose( "Deleting: " + deleteTarget );
558 
559             if ( deleteTarget.isDirectory() )
560             {
561                 try
562                 {
563                     FileUtils.deleteDirectory( deleteTarget );
564                 }
565                 catch ( IOException e )
566                 {
567                     getLog().warn( "Unable to purge local repository location: " + deleteTarget, e );
568                 }
569             }
570             else
571             {
572                 if ( !deleteTarget.delete() )
573                 {
574                     getLog().warn( "Unable to purge local repository location: " + deleteTarget );
575                 }
576             }
577             artifact.setResolved( false );
578         }
579     }
580 
581     private void reResolveArtifacts( MavenProject project, Set<Artifact> artifacts, ArtifactFilter filter )
582         throws ArtifactResolutionException, ArtifactNotFoundException
583     {
584 
585         // Always need to re-resolve the poms in case they were purged along with the artifact
586         // because Maven 2 will not automatically re-resolve them when resolving the artifact
587         for ( Artifact artifact : artifacts )
588         {
589             try
590             {
591                 Artifact pomArtifact =
592                     factory.createArtifact( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(),
593                                             null, "pom" );
594                 resolver.resolveAlways( pomArtifact, remoteRepositories, localRepository );
595             }
596             catch ( ArtifactResolutionException e )
597             {
598                 verbose( e.getMessage() );
599             }
600             catch ( ArtifactNotFoundException e )
601             {
602                 verbose( e.getMessage() );
603             }
604         }
605 
606         List<Artifact> missingArtifacts = new ArrayList<Artifact>();
607 
608         for ( Artifact artifact : artifacts )
609         {
610             verbose( "Resolving artifact: " + artifact.getId() );
611 
612             try
613             {
614                 resolver.resolveAlways( artifact, project.getRemoteArtifactRepositories(), localRepository );
615             }
616             catch ( ArtifactResolutionException e )
617             {
618                 verbose( e.getMessage() );
619                 missingArtifacts.add( artifact );
620             }
621             catch ( ArtifactNotFoundException e )
622             {
623                 verbose( e.getMessage() );
624                 missingArtifacts.add( artifact );
625             }
626         }
627 
628         if ( missingArtifacts.size() > 0 )
629         {
630             StringBuffer message = new StringBuffer( "required artifacts missing:\n" );
631             for ( Artifact missingArtifact : missingArtifacts )
632             {
633                 message.append( "  " ).append( missingArtifact.getId() ).append( '\n' );
634             }
635             message.append( "\nfor the artifact:" );
636 
637             throw new ArtifactResolutionException( message.toString(), project.getArtifact(),
638                                                    project.getRemoteArtifactRepositories() );
639         }
640     }
641 
642     private File findDeleteTarget( Artifact artifact )
643     {
644         // Use localRepository.pathOf() in case artifact.getFile() is not set
645         File deleteTarget = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
646 
647         if ( GROUP_ID_FUZZINESS.equals( resolutionFuzziness ) )
648         {
649             // get the groupId dir.
650             deleteTarget = deleteTarget.getParentFile().getParentFile().getParentFile();
651         }
652         else if ( ARTIFACT_ID_FUZZINESS.equals( resolutionFuzziness ) )
653         {
654             // get the artifactId dir.
655             deleteTarget = deleteTarget.getParentFile().getParentFile();
656         }
657         else if ( VERSION_FUZZINESS.equals( resolutionFuzziness ) )
658         {
659             // get the version dir.
660             deleteTarget = deleteTarget.getParentFile();
661         }
662         // else it's file fuzziness.
663         return deleteTarget;
664     }
665 
666     private void verbose( String message )
667     {
668         if ( verbose || getLog().isDebugEnabled() )
669         {
670             getLog().info( message );
671         }
672     }
673 
674     public boolean isSkip()
675     {
676         return skip;
677     }
678 
679     public void setSkip( boolean skip )
680     {
681         this.skip = skip;
682     }
683 }