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