1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven;
20
21 import javax.inject.Inject;
22 import javax.inject.Named;
23 import javax.inject.Singleton;
24
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.file.DirectoryNotEmptyException;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.nio.file.StandardCopyOption;
32 import java.util.ArrayDeque;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.Deque;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.concurrent.ConcurrentHashMap;
43 import java.util.stream.Collectors;
44 import java.util.stream.Stream;
45
46 import org.apache.maven.api.model.Model;
47 import org.apache.maven.api.services.Lookup;
48 import org.apache.maven.eventspy.EventSpy;
49 import org.apache.maven.execution.ExecutionEvent;
50 import org.apache.maven.execution.MavenSession;
51 import org.apache.maven.impl.resolver.MavenWorkspaceReader;
52 import org.apache.maven.project.MavenProject;
53 import org.apache.maven.project.artifact.ProjectArtifact;
54 import org.eclipse.aether.artifact.Artifact;
55 import org.eclipse.aether.repository.WorkspaceRepository;
56 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60
61
62
63
64
65 @Named(ReactorReader.HINT)
66 @SessionScoped
67 class ReactorReader implements MavenWorkspaceReader {
68 public static final String HINT = "reactor";
69
70 public static final String PROJECT_LOCAL_REPO = "project-local-repo";
71
72 private static final Collection<String> COMPILE_PHASE_TYPES = new HashSet<>(
73 Arrays.asList("jar", "ejb-client", "war", "rar", "ejb", "par", "sar", "wsr", "har", "app-client"));
74
75 private static final Logger LOGGER = LoggerFactory.getLogger(ReactorReader.class);
76
77 private final MavenSession session;
78 private final WorkspaceRepository repository;
79
80 private Map<String, Map<String, Map<String, MavenProject>>> projects;
81 private Map<String, Map<String, Map<String, MavenProject>>> allProjects;
82 private Path projectLocalRepository;
83
84 private final Map<String, Deque<String>> lifecycles = new ConcurrentHashMap<>();
85
86 @Inject
87 ReactorReader(MavenSession session) {
88 this.session = session;
89 this.repository = new WorkspaceRepository("reactor", null);
90 }
91
92
93
94
95
96 @Override
97 public WorkspaceRepository getRepository() {
98 return repository;
99 }
100
101 @Override
102 public File findArtifact(Artifact artifact) {
103 MavenProject project = getProject(artifact);
104
105 if (project != null) {
106 File file = findArtifact(project, artifact, true);
107 if (file == null && project != project.getExecutionProject()) {
108 file = findArtifact(project.getExecutionProject(), artifact, true);
109 }
110 return file;
111 }
112
113
114 File packagedArtifactFile = findInProjectLocalRepository(artifact);
115 if (packagedArtifactFile != null && packagedArtifactFile.exists()) {
116
117 if (!"pom".equals(artifact.getExtension())) {
118 project = getProject(artifact, getAllProjects());
119 if (project != null) {
120 isPackagedArtifactUpToDate(project, packagedArtifactFile);
121 }
122 }
123 return packagedArtifactFile;
124 }
125
126 return null;
127 }
128
129 @Override
130 public List<String> findVersions(Artifact artifact) {
131 List<String> versions = getProjects()
132 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
133 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
134 .values()
135 .stream()
136 .map(MavenProject::getVersion)
137 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
138 if (!versions.isEmpty()) {
139 return versions;
140 }
141 return getAllProjects()
142 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
143 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
144 .values()
145 .stream()
146 .filter(p -> Objects.nonNull(findArtifact(p, artifact, false)))
147 .map(MavenProject::getVersion)
148 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
149 }
150
151 @Override
152 public Model findModel(Artifact artifact) {
153 MavenProject project = getProject(artifact);
154 return project == null ? null : project.getModel().getDelegate();
155 }
156
157
158
159
160
161 private File findArtifact(MavenProject project, Artifact artifact, boolean checkUptodate) {
162
163 if ("pom".equals(artifact.getExtension())) {
164 return project.getFile();
165 }
166
167
168 Artifact projectArtifact = findMatchingArtifact(project, artifact);
169 if (projectArtifact != null) {
170
171 File packagedArtifactFile = projectArtifact.getFile();
172 if (packagedArtifactFile != null && packagedArtifactFile.exists()) {
173 return packagedArtifactFile;
174 }
175 }
176
177
178 File packagedArtifactFile = findInProjectLocalRepository(artifact);
179 if (packagedArtifactFile != null
180 && packagedArtifactFile.exists()
181 && (!checkUptodate
182 || "pom".equals(artifact.getExtension())
183 || isPackagedArtifactUpToDate(project, packagedArtifactFile))) {
184 return packagedArtifactFile;
185 }
186
187 if (!hasBeenPackagedDuringThisSession(project)) {
188
189
190 return determineBuildOutputDirectoryForArtifact(project, artifact);
191 }
192
193
194
195 return null;
196 }
197
198 private File determineBuildOutputDirectoryForArtifact(final MavenProject project, final Artifact artifact) {
199 if (isTestArtifact(artifact)) {
200 if (project.hasLifecyclePhase("test-compile")) {
201 return new File(project.getBuild().getTestOutputDirectory());
202 }
203 } else {
204 String type = artifact.getProperty("type", "");
205 File outputDirectory = new File(project.getBuild().getOutputDirectory());
206
207
208
209 boolean projectCompiledDuringThisSession =
210 project.hasLifecyclePhase("compile") && COMPILE_PHASE_TYPES.contains(type);
211
212
213
214 boolean projectHasOutputFromPreviousSession =
215 !session.getProjects().contains(project) && outputDirectory.exists();
216
217 if (projectHasOutputFromPreviousSession || projectCompiledDuringThisSession) {
218 return outputDirectory;
219 }
220 }
221
222
223
224 return null;
225 }
226
227 private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile) {
228 Path outputDirectory = Paths.get(project.getBuild().getOutputDirectory());
229 if (!outputDirectory.toFile().exists()) {
230 return true;
231 }
232
233 try (Stream<Path> outputFiles = Files.walk(outputDirectory)) {
234
235 long artifactLastModified =
236 Files.getLastModifiedTime(packagedArtifactFile.toPath()).toMillis();
237
238 if (session.getProjectBuildingRequest().getBuildStartTime() != null) {
239 long buildStartTime =
240 session.getProjectBuildingRequest().getBuildStartTime().getTime();
241 if (artifactLastModified > buildStartTime) {
242 return true;
243 }
244 }
245
246 for (Path outputFile : (Iterable<Path>) outputFiles::iterator) {
247 if (Files.isDirectory(outputFile)) {
248 continue;
249 }
250
251 long outputFileLastModified =
252 Files.getLastModifiedTime(outputFile).toMillis();
253 if (outputFileLastModified > artifactLastModified) {
254 LOGGER.warn(
255 "File '{}' is more recent than the packaged artifact for '{}', "
256 + "please run a full `mvn package` build",
257 relativizeOutputFile(outputFile),
258 project.getArtifactId());
259 return false;
260 } else if (LOGGER.isDebugEnabled()) {
261 LOGGER.debug(
262 "File '{}' timestamp {} vs artifact timestamp {} for '{}'",
263 relativizeOutputFile(outputFile),
264 outputFileLastModified,
265 artifactLastModified,
266 project.getArtifactId());
267 }
268 }
269
270 return true;
271 } catch (IOException e) {
272 LOGGER.warn(
273 "An I/O error occurred while checking if the packaged artifact is up-to-date "
274 + "against the build output directory. "
275 + "Continuing with the assumption that it is up-to-date.",
276 e);
277 return true;
278 }
279 }
280
281 private boolean hasBeenPackagedDuringThisSession(MavenProject project) {
282 boolean packaged = false;
283 for (String phase : getLifecycles(project)) {
284 switch (phase) {
285 case "clean":
286 packaged = false;
287 break;
288 case "package":
289 case "install":
290 case "deploy":
291 packaged = true;
292 break;
293 default:
294 break;
295 }
296 }
297 return packaged;
298 }
299
300 private Path relativizeOutputFile(final Path outputFile) {
301 Path rootDirectory = session.getRequest().getRootDirectory();
302 return rootDirectory.relativize(outputFile);
303 }
304
305
306
307
308
309
310
311
312 private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) {
313 String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact);
314 return getProjectArtifacts(project)
315 .filter(artifact ->
316 Objects.equals(requestedRepositoryConflictId, ArtifactIdUtils.toVersionlessId(artifact)))
317 .findFirst()
318 .orElse(null);
319 }
320
321
322
323
324
325
326
327 private static boolean isTestArtifact(Artifact artifact) {
328 return ("test-jar".equals(artifact.getProperty("type", "")))
329 || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier()));
330 }
331
332 private File findInProjectLocalRepository(Artifact artifact) {
333
334
335 if ("pom".equals(artifact.getExtension())) {
336 String classifier = artifact.getClassifier();
337 if (classifier == null || classifier.isEmpty()) {
338 Path consumer = getArtifactPath(
339 artifact.getGroupId(), artifact.getArtifactId(), artifact.getBaseVersion(), "consumer", "pom");
340 if (Files.isRegularFile(consumer)) {
341 return consumer.toFile();
342 }
343 }
344 }
345 Path target = getArtifactPath(artifact);
346 return Files.isRegularFile(target) ? target.toFile() : null;
347 }
348
349
350
351
352
353
354
355
356
357 private void processEvent(ExecutionEvent event) {
358 MavenProject project = event.getProject();
359 switch (event.getType()) {
360 case MojoStarted:
361 String phase = event.getMojoExecution().getLifecyclePhase();
362 if (phase != null) {
363 Deque<String> phases = getLifecycles(project);
364 if (!Objects.equals(phase, phases.peekLast())) {
365 phases.addLast(phase);
366 if ("clean".equals(phase)) {
367 synchronized (project) {
368 cleanProjectLocalRepository(project);
369 }
370 }
371 }
372 }
373 break;
374 case ProjectSucceeded:
375 case ForkedProjectSucceeded:
376 synchronized (project) {
377 installIntoProjectLocalRepository(project);
378 }
379 break;
380 default:
381 break;
382 }
383 }
384
385 private Deque<String> getLifecycles(MavenProject project) {
386 return lifecycles.computeIfAbsent(project.getId(), k -> new ArrayDeque<>());
387 }
388
389
390
391
392
393
394
395
396
397 private void installIntoProjectLocalRepository(MavenProject project) {
398 if ("pom".equals(project.getPackaging())
399 && !"clean".equals(getLifecycles(project).peekLast())
400 || hasBeenPackagedDuringThisSession(project)) {
401 getProjectArtifacts(project).filter(this::isRegularFile).forEach(this::installIntoProjectLocalRepository);
402 }
403 }
404
405 private void cleanProjectLocalRepository(MavenProject project) {
406 try {
407 Path artifactPath = getProjectLocalRepo()
408 .resolve(project.getGroupId())
409 .resolve(project.getArtifactId())
410 .resolve(project.getVersion());
411 if (Files.isDirectory(artifactPath)) {
412 try (Stream<Path> paths = Files.list(artifactPath)) {
413 for (Path path : (Iterable<Path>) paths::iterator) {
414 Files.delete(path);
415 }
416 }
417 try {
418 Files.delete(artifactPath);
419 Files.delete(artifactPath.getParent());
420 Files.delete(artifactPath.getParent().getParent());
421 } catch (DirectoryNotEmptyException e) {
422
423 }
424 }
425 } catch (IOException e) {
426 LOGGER.error("Error while cleaning project local repository", e);
427 }
428 }
429
430
431
432
433 private Stream<Artifact> getProjectArtifacts(MavenProject project) {
434 Stream<org.apache.maven.artifact.Artifact> artifacts = Stream.concat(
435 Stream.concat(
436
437 Stream.of(new ProjectArtifact(project)),
438
439 "pom".equals(project.getPackaging()) ? Stream.empty() : Stream.of(project.getArtifact())),
440
441 project.getAttachedArtifacts().stream());
442 return artifacts.map(RepositoryUtils::toArtifact);
443 }
444
445 private boolean isRegularFile(Artifact artifact) {
446 return artifact.getFile() != null && artifact.getFile().isFile();
447 }
448
449 private void installIntoProjectLocalRepository(Artifact artifact) {
450 String extension = artifact.getExtension();
451 String classifier = artifact.getClassifier();
452
453 Path target = getArtifactPath(
454 artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), classifier, extension);
455 try {
456
457 Path source = artifact.getPath();
458 if (!(Files.isRegularFile(target) && Files.isSameFile(source, target))) {
459 Files.createDirectories(target.getParent());
460 try {
461 Files.deleteIfExists(target);
462 Files.createLink(target, source);
463 } catch (UnsupportedOperationException | IOException suppressed) {
464 LOGGER.info("Copying {} to project local repository.", artifact);
465 try {
466 Files.copy(
467 source,
468 target,
469 StandardCopyOption.REPLACE_EXISTING,
470 StandardCopyOption.COPY_ATTRIBUTES);
471 } catch (IOException e) {
472 e.addSuppressed(suppressed);
473 throw e;
474 }
475 }
476 }
477 } catch (IOException e) {
478 LOGGER.error("Error while copying artifact " + artifact + " to project local repository.", e);
479 }
480 }
481
482 private Path getArtifactPath(Artifact artifact) {
483 String groupId = artifact.getGroupId();
484 String artifactId = artifact.getArtifactId();
485 String version = artifact.getBaseVersion();
486 String classifier = artifact.getClassifier();
487 String extension = artifact.getExtension();
488 return getArtifactPath(groupId, artifactId, version, classifier, extension);
489 }
490
491 private Path getArtifactPath(
492 String groupId, String artifactId, String version, String classifier, String extension) {
493 Path repo = getProjectLocalRepo();
494 return repo.resolve(groupId)
495 .resolve(artifactId)
496 .resolve(version)
497 .resolve(artifactId
498 + '-'
499 + version
500 + (classifier != null && !classifier.isEmpty() ? "-" + classifier : "")
501 + '.'
502 + extension);
503 }
504
505 private Path getProjectLocalRepo() {
506 if (projectLocalRepository == null) {
507 Path root = session.getRequest().getRootDirectory();
508 List<MavenProject> projects = session.getProjects();
509 if (projects != null) {
510 projectLocalRepository = projects.stream()
511 .filter(project -> Objects.equals(root.toFile(), project.getBasedir()))
512 .findFirst()
513 .map(project -> project.getBuild().getDirectory())
514 .map(Paths::get)
515 .orElseGet(() -> root.resolve("target"))
516 .resolve(PROJECT_LOCAL_REPO);
517 } else {
518 return root.resolve("target").resolve(PROJECT_LOCAL_REPO);
519 }
520 }
521 return projectLocalRepository;
522 }
523
524 private MavenProject getProject(Artifact artifact) {
525 return getProject(artifact, getProjects());
526 }
527
528 private MavenProject getProject(Artifact artifact, Map<String, Map<String, Map<String, MavenProject>>> projects) {
529 return projects.getOrDefault(artifact.getGroupId(), Collections.emptyMap())
530 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
531 .getOrDefault(artifact.getBaseVersion(), null);
532 }
533
534
535 private Map<String, Map<String, Map<String, MavenProject>>> getAllProjects() {
536
537 if (allProjects == null) {
538 List<MavenProject> allProjects = session.getAllProjects();
539 if (allProjects != null) {
540 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
541 allProjects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
542 .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
543 .put(project.getVersion(), project));
544 this.allProjects = map;
545 } else {
546 return Collections.emptyMap();
547 }
548 }
549 return allProjects;
550 }
551
552 private Map<String, Map<String, Map<String, MavenProject>>> getProjects() {
553
554 if (projects == null) {
555 List<MavenProject> projects = session.getProjects();
556 if (projects != null) {
557 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
558 projects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
559 .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
560 .put(project.getVersion(), project));
561 this.projects = map;
562 } else {
563 return Collections.emptyMap();
564 }
565 }
566 return projects;
567 }
568
569
570
571
572
573 @Named
574 @Singleton
575 @SuppressWarnings("unused")
576 static class ReactorReaderSpy implements EventSpy {
577
578 private final Lookup lookup;
579
580 @Inject
581 ReactorReaderSpy(Lookup lookup) {
582 this.lookup = lookup;
583 }
584
585 @Override
586 public void init(Context context) throws Exception {}
587
588 @Override
589 public void onEvent(Object event) throws Exception {
590 if (event instanceof ExecutionEvent executionEvent) {
591 ReactorReader reactorReader = lookup.lookup(ReactorReader.class);
592 reactorReader.processEvent(executionEvent);
593 }
594 }
595
596 @Override
597 public void close() throws Exception {}
598 }
599 }