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