1 package org.apache.maven.plugins.dependency;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.HashSet;
27 import java.util.LinkedHashSet;
28 import java.util.List;
29 import java.util.Set;
30
31 import org.apache.maven.artifact.Artifact;
32 import org.apache.maven.artifact.ArtifactUtils;
33 import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
34 import org.apache.maven.artifact.repository.ArtifactRepository;
35 import org.apache.maven.artifact.resolver.ArtifactResolutionException;
36 import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
37 import org.apache.maven.execution.MavenSession;
38 import org.apache.maven.model.Dependency;
39 import org.apache.maven.plugin.AbstractMojo;
40 import org.apache.maven.plugin.MojoExecution;
41 import org.apache.maven.plugin.MojoExecution.Source;
42 import org.apache.maven.plugin.MojoExecutionException;
43 import org.apache.maven.plugin.MojoFailureException;
44 import org.apache.maven.plugins.annotations.Component;
45 import org.apache.maven.plugins.annotations.Mojo;
46 import org.apache.maven.plugins.annotations.Parameter;
47 import org.apache.maven.project.MavenProject;
48 import org.apache.maven.shared.artifact.filter.resolve.AbstractFilter;
49 import org.apache.maven.shared.artifact.filter.resolve.AndFilter;
50 import org.apache.maven.shared.artifact.filter.resolve.Node;
51 import org.apache.maven.shared.artifact.filter.resolve.PatternExclusionsFilter;
52 import org.apache.maven.shared.artifact.filter.resolve.PatternInclusionsFilter;
53 import org.apache.maven.shared.artifact.filter.resolve.ScopeFilter;
54 import org.apache.maven.shared.artifact.filter.resolve.TransformableFilter;
55 import org.apache.maven.shared.artifact.filter.resolve.transform.ArtifactIncludeFilterTransformer;
56 import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate;
57 import org.apache.maven.shared.transfer.artifact.TransferUtils;
58 import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
59 import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
60 import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
61 import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
62 import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolverException;
63 import org.apache.maven.shared.utils.logging.MessageBuilder;
64 import org.apache.maven.shared.utils.logging.MessageUtils;
65 import org.codehaus.plexus.util.FileUtils;
66 import org.codehaus.plexus.util.StringUtils;
67
68
69
70
71
72
73
74
75 @Mojo( name = "purge-local-repository", threadSafe = true, requiresProject = false )
76 public class PurgeLocalRepositoryMojo
77 extends AbstractMojo
78 {
79
80 private static final String VERSION_FUZZINESS = "version";
81
82 private static final String ARTIFACT_ID_FUZZINESS = "artifactId";
83
84 private static final String GROUP_ID_FUZZINESS = "groupId";
85
86
87
88
89 @Parameter( defaultValue = "${reactorProjects}", readonly = true, required = true )
90 private List<MavenProject> reactorProjects;
91
92
93
94
95 @Parameter( defaultValue = "${project}", readonly = true, required = true )
96 private MavenProject project;
97
98 @Parameter( defaultValue = "${session}", readonly = true, required = true )
99 private MavenSession session;
100
101
102
103
104 @Parameter( defaultValue = "${mojo}", required = true, readonly = true )
105 private MojoExecution mojoExecution;
106
107
108
109
110 @Component
111 private ArtifactHandlerManager artifactHandlerManager;
112
113
114
115
116
117
118
119
120
121 @Parameter
122 private List<String> manualIncludes;
123
124
125
126
127
128
129
130
131 @Parameter( property = "manualInclude" )
132 private String manualInclude;
133
134
135
136
137
138
139 @Parameter
140 private List<String> includes;
141
142
143
144
145
146
147
148
149 @Parameter( property = "include" )
150 private String include;
151
152
153
154
155 @Parameter
156 private List<String> excludes;
157
158
159
160
161
162
163 @Parameter( property = "exclude" )
164 private String exclude;
165
166
167
168
169
170 @Parameter( property = "reResolve", defaultValue = "true" )
171 private boolean reResolve;
172
173
174
175
176 @Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
177 private ArtifactRepository localRepository;
178
179
180
181
182 @Component
183 private DependencyResolver dependencyResolver;
184
185
186
187
188 @Component
189 private ArtifactResolver artifactResolver;
190
191
192
193
194
195
196
197
198
199
200 @Parameter( property = "resolutionFuzziness", defaultValue = "version" )
201 private String resolutionFuzziness;
202
203
204
205
206 @Parameter( property = "actTransitively", defaultValue = "true" )
207 private boolean actTransitively;
208
209
210
211
212 @Parameter( property = "verbose", defaultValue = "false" )
213 private boolean verbose;
214
215
216
217
218
219
220 @Parameter( property = "snapshotsOnly", defaultValue = "false" )
221 private boolean snapshotsOnly;
222
223
224
225
226
227
228 @Parameter( property = "skip", defaultValue = "false" )
229 private boolean skip;
230
231
232
233
234 private class DirectDependencyFilter
235 extends AbstractFilter
236 {
237 private Artifact projectArtifact;
238
239 private List<Dependency> directDependencies;
240
241
242
243
244
245
246 DirectDependencyFilter( Artifact projectArtifact, List<Dependency> directDependencies )
247 {
248 this.projectArtifact = projectArtifact;
249 this.directDependencies = directDependencies;
250 }
251
252 @Override
253 public boolean accept( Node node, List<Node> parents )
254 {
255
256 if ( artifactsGAMatch( node, projectArtifact.getGroupId(), projectArtifact.getArtifactId() ) )
257 {
258 return true;
259 }
260 for ( Dependency dep : directDependencies )
261 {
262 if ( this.artifactsGAMatch( node, dep.getGroupId(), dep.getArtifactId() ) )
263 {
264 return true;
265 }
266 }
267 return false;
268 }
269
270
271
272
273 private boolean artifactsGAMatch( Node node, String groupId, String artifactId )
274 {
275 if ( node.getDependency() == null )
276 {
277 return false;
278 }
279
280 if ( !node.getDependency().getGroupId().equals( groupId ) )
281 {
282 getLog().debug( "Different groupId: " + node.getDependency() + " " + groupId );
283 return false;
284 }
285 if ( !node.getDependency().getArtifactId().equals( artifactId ) )
286 {
287 getLog().debug( "Different artifactId: " + node.getDependency() + " " + artifactId );
288 return false;
289 }
290 return true;
291 }
292 }
293
294
295
296
297 private static class SnapshotsFilter
298 extends AbstractFilter
299 {
300 @Override
301 public boolean accept( Node node, List<Node> parents )
302 {
303 if ( node.getDependency() == null )
304 {
305 return false;
306 }
307 else
308 {
309 return ArtifactUtils.isSnapshot( node.getDependency().getVersion() );
310 }
311 }
312 }
313
314 @Override
315 public void execute()
316 throws MojoExecutionException, MojoFailureException
317 {
318 if ( isSkip() )
319 {
320 getLog().info( "Skipping plugin execution" );
321 return;
322 }
323
324 if ( !StringUtils.isEmpty( manualInclude ) )
325 {
326 manualIncludes = this.parseIncludes( manualInclude );
327 }
328
329 if ( manualIncludes != null && manualIncludes.size() > 0 )
330 {
331 manualPurge( manualIncludes );
332 return;
333 }
334
335 Set<Artifact> purgedArtifacts = new HashSet<>();
336 if ( shouldPurgeAllProjectsInReactor() )
337 {
338 for ( MavenProject reactorProject : reactorProjects )
339 {
340 purgeLocalRepository( reactorProject, purgedArtifacts );
341 }
342 }
343 else
344 {
345 purgeLocalRepository( project, purgedArtifacts );
346 }
347 }
348
349
350
351
352
353
354
355 private boolean shouldPurgeAllProjectsInReactor()
356 {
357 Source source = mojoExecution.getSource();
358 return reactorProjects.size() > 1 && source == Source.CLI;
359 }
360
361
362
363
364
365
366
367
368 private void purgeLocalRepository( MavenProject theProject, Set<Artifact> purgedArtifacts )
369 throws MojoFailureException
370 {
371 List<Dependency> dependencies = theProject.getDependencies();
372
373 TransformableFilter dependencyFilter = createPurgeArtifactsFilter( theProject, dependencies, purgedArtifacts );
374
375 Set<Artifact> resolvedArtifactsToPurge =
376 getFilteredResolvedArtifacts( theProject, dependencies, dependencyFilter );
377
378 if ( resolvedArtifactsToPurge.isEmpty() )
379 {
380 getLog().info( "No artifacts included for purge for project: " + getProjectKey( theProject ) );
381 return;
382 }
383
384 purgeArtifacts( theProject, resolvedArtifactsToPurge );
385 purgedArtifacts.addAll( resolvedArtifactsToPurge );
386
387 if ( reResolve )
388 {
389 getLog().info( "Re-resolving dependencies" );
390 try
391 {
392 reResolveArtifacts( theProject, resolvedArtifactsToPurge );
393 }
394 catch ( ArtifactResolutionException e )
395 {
396 String failureMessage = "Failed to refresh project dependencies for: " + theProject.getId();
397 throw new MojoFailureException( failureMessage, e );
398 }
399 }
400 }
401
402
403
404
405
406
407
408 private void manualPurge( List<String> theIncludes )
409 throws MojoExecutionException
410 {
411 MessageBuilder messageBuilder = MessageUtils.buffer();
412
413 getLog().info( messageBuilder.a( "Deleting " ).strong( theIncludes.size() )
414 .a( " manual " )
415 .a( theIncludes.size() != 1 ? "dependencies" : "dependency" )
416 .a( " from " )
417 .strong( localRepository.getBasedir() )
418 .toString() );
419
420 for ( String gavPattern : theIncludes )
421 {
422 if ( StringUtils.isEmpty( gavPattern ) )
423 {
424 getLog().debug( "Skipping empty gav pattern" );
425 continue;
426 }
427
428 String relativePath = gavToPath( gavPattern );
429 if ( StringUtils.isEmpty( relativePath ) )
430 {
431 getLog().debug( "Skipping empty relative path for gav pattern: " + gavPattern );
432 continue;
433 }
434
435 File purgeDir = new File( localRepository.getBasedir(), relativePath );
436 if ( purgeDir.exists() )
437 {
438 getLog().debug( "Deleting directory: " + purgeDir );
439 try
440 {
441 FileUtils.deleteDirectory( purgeDir );
442 }
443 catch ( IOException e )
444 {
445 throw new MojoExecutionException( "Unable to purge directory: " + purgeDir );
446 }
447 }
448 else
449 {
450 getLog().debug( "Directory: " + purgeDir + " doesn't exist" );
451 }
452 }
453 }
454
455
456
457
458
459
460
461 private String gavToPath( String gav )
462 {
463 if ( StringUtils.isEmpty( gav ) )
464 {
465 return null;
466 }
467
468 String[] pathComponents = gav.split( ":" );
469
470 StringBuilder path = new StringBuilder( pathComponents[0].replace( '.', '/' ) );
471
472 for ( int i = 1; i < pathComponents.length; ++i )
473 {
474 path.append( "/" ).append( pathComponents[i] );
475 }
476
477 return path.toString();
478 }
479
480
481
482
483
484
485
486
487
488
489 private TransformableFilter createPurgeArtifactsFilter( MavenProject theProject, List<Dependency> dependencies,
490 Set<Artifact> purgedArtifacts )
491 {
492 List<TransformableFilter> subFilters = new ArrayList<>();
493
494
495 subFilters.add( ScopeFilter.excluding( Artifact.SCOPE_SYSTEM ) );
496
497 if ( this.snapshotsOnly )
498 {
499 subFilters.add( new SnapshotsFilter() );
500 }
501
502
503 if ( !StringUtils.isEmpty( this.include ) )
504 {
505 this.includes = parseIncludes( this.include );
506 }
507 if ( this.includes != null )
508 {
509 subFilters.add( new PatternInclusionsFilter( includes ) );
510 }
511
512 if ( !StringUtils.isEmpty( this.exclude ) )
513 {
514 this.excludes = parseIncludes( this.exclude );
515 }
516 if ( this.excludes != null )
517 {
518 subFilters.add( new PatternExclusionsFilter( excludes ) );
519 }
520
521 if ( !actTransitively )
522 {
523 subFilters.add( new DirectDependencyFilter( theProject.getArtifact(), dependencies ) );
524 }
525
526 List<String> exclusions = new ArrayList<>( reactorProjects.size() );
527
528 for ( MavenProject reactorProject : reactorProjects )
529 {
530 exclusions.add( toPatternExcludes( reactorProject.getArtifact() ) );
531 }
532
533 for ( Artifact purgedArtifact : purgedArtifacts )
534 {
535 exclusions.add( toPatternExcludes( purgedArtifact ) );
536 }
537 subFilters.add( new PatternExclusionsFilter( exclusions ) );
538
539 return new AndFilter( subFilters );
540 }
541
542
543
544
545
546
547
548 private String toPatternExcludes( Artifact artifact )
549 {
550 return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
551 + artifact.getArtifactHandler().getExtension() + ":" + artifact.getVersion();
552 }
553
554
555
556
557
558
559
560 private List<String> parseIncludes( String theInclude )
561 {
562 List<String> theIncludes = new ArrayList<>();
563
564 if ( theInclude != null )
565 {
566 String[] elements = theInclude.split( "," );
567 theIncludes.addAll( Arrays.asList( elements ) );
568 }
569
570 return theIncludes;
571 }
572
573 private Set<Artifact> getFilteredResolvedArtifacts( MavenProject theProject, List<Dependency> dependencies,
574 TransformableFilter filter )
575 {
576 try
577 {
578 Iterable<ArtifactResult> results =
579 dependencyResolver.resolveDependencies( session.getProjectBuildingRequest(), theProject.getModel(),
580 filter );
581
582 Set<Artifact> resolvedArtifacts = new LinkedHashSet<>();
583
584 for ( ArtifactResult artResult : results )
585 {
586 resolvedArtifacts.add( artResult.getArtifact() );
587 }
588
589 return resolvedArtifacts;
590 }
591 catch ( DependencyResolverException e )
592 {
593 getLog().info( "Unable to resolve all dependencies for: " + getProjectKey( theProject )
594 + ". Falling back to non-transitive mode for initial artifact resolution." );
595 }
596
597 Set<Artifact> resolvedArtifacts = new LinkedHashSet<>();
598
599 ArtifactFilter artifactFilter = filter.transform( new ArtifactIncludeFilterTransformer() );
600
601 for ( Dependency dependency : dependencies )
602 {
603 DefaultArtifactCoordinate coordinate = new DefaultArtifactCoordinate();
604 coordinate.setGroupId( dependency.getGroupId() );
605 coordinate.setArtifactId( dependency.getArtifactId() );
606 coordinate.setVersion( dependency.getVersion() );
607 coordinate.setExtension( artifactHandlerManager.getArtifactHandler( dependency.getType() ).getExtension() );
608 try
609 {
610 Artifact artifact =
611 artifactResolver.resolveArtifact( session.getProjectBuildingRequest(), coordinate ).getArtifact();
612 if ( artifactFilter.include( artifact ) )
613 {
614 resolvedArtifacts.add( artifact );
615 }
616 }
617 catch ( ArtifactResolverException e )
618 {
619 getLog().debug( "Unable to resolve artifact: " + coordinate );
620 }
621 }
622 return resolvedArtifacts;
623 }
624
625 private void purgeArtifacts( MavenProject theProject, Set<Artifact> artifacts )
626 {
627 MessageBuilder messageBuilder = MessageUtils.buffer();
628
629 getLog().info( messageBuilder.a( "Deleting " ).strong( artifacts.size() )
630 .a( " " ).strong( actTransitively ? "transitive" : "direct" )
631 .a( artifacts.size() != 1 ? " dependencies" : " dependency" )
632 .a( " for project " ).strong( getProjectKey( theProject ) )
633 .a( " from " ).strong( localRepository.getBasedir() )
634 .a( " with artifact " ).strong( resolutionFuzziness ).a( " resolution fuzziness" )
635 .toString() );
636
637 for ( Artifact artifact : artifacts )
638 {
639 verbose( "Purging artifact: " + artifact.getId() );
640
641 File deleteTarget = findDeleteTarget( artifact );
642
643 verbose( "Deleting: " + deleteTarget );
644
645 if ( deleteTarget.isDirectory() )
646 {
647 try
648 {
649 FileUtils.deleteDirectory( deleteTarget );
650 }
651 catch ( IOException e )
652 {
653 getLog().warn( "Unable to purge local repository location: " + deleteTarget, e );
654 }
655 }
656 else
657 {
658 if ( !deleteTarget.delete() )
659 {
660 deleteTarget.deleteOnExit();
661 getLog().warn( "Unable to purge local repository location immediately: " + deleteTarget );
662 }
663 }
664 artifact.setResolved( false );
665 }
666 }
667
668 private void reResolveArtifacts( MavenProject theProject, Set<Artifact> artifacts )
669 throws ArtifactResolutionException
670 {
671
672
673 for ( Artifact artifact : artifacts )
674 {
675 verbose( "Resolving artifact: " + artifact.getId() );
676
677 try
678 {
679
680 artifactResolver.resolveArtifact( session.getProjectBuildingRequest(),
681 TransferUtils.toArtifactCoordinate( artifact ) );
682
683 }
684 catch ( ArtifactResolverException e )
685 {
686 verbose( e.getMessage() );
687 }
688 }
689
690 List<Artifact> missingArtifacts = new ArrayList<>();
691
692 for ( Artifact artifact : artifacts )
693 {
694 try
695 {
696 artifactResolver.resolveArtifact( session.getProjectBuildingRequest(), artifact );
697 }
698 catch ( ArtifactResolverException e )
699 {
700 verbose( e.getMessage() );
701 missingArtifacts.add( artifact );
702 }
703 }
704
705 if ( missingArtifacts.size() > 0 )
706 {
707 StringBuilder message = new StringBuilder( "required artifacts missing:" );
708 message.append( System.lineSeparator() );
709 for ( Artifact missingArtifact : missingArtifacts )
710 {
711 message.append( " " ).append( missingArtifact.getId() ).append( System.lineSeparator() );
712 }
713 message.append( System.lineSeparator() );
714 message.append( "for the artifact:" );
715
716 throw new ArtifactResolutionException( message.toString(), theProject.getArtifact(),
717 theProject.getRemoteArtifactRepositories() );
718 }
719 }
720
721 private File findDeleteTarget( Artifact artifact )
722 {
723
724 File deleteTarget = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
725
726 if ( GROUP_ID_FUZZINESS.equals( resolutionFuzziness ) )
727 {
728
729 deleteTarget = deleteTarget.getParentFile().getParentFile().getParentFile();
730 }
731 else if ( ARTIFACT_ID_FUZZINESS.equals( resolutionFuzziness ) )
732 {
733
734 deleteTarget = deleteTarget.getParentFile().getParentFile();
735 }
736 else if ( VERSION_FUZZINESS.equals( resolutionFuzziness ) )
737 {
738
739 deleteTarget = deleteTarget.getParentFile();
740 }
741
742 return deleteTarget;
743 }
744
745 private void verbose( String message )
746 {
747 if ( verbose || getLog().isDebugEnabled() )
748 {
749 getLog().info( message );
750 }
751 }
752
753 private String getProjectKey( MavenProject project )
754 {
755 return project.getArtifactId();
756 }
757
758
759
760
761 public boolean isSkip()
762 {
763 return skip;
764 }
765
766
767
768
769 public void setSkip( boolean skip )
770 {
771 this.skip = skip;
772 }
773 }