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 }