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 project = getProject(artifact, getAllProjects());
118 if (project != null) {
119 isPackagedArtifactUpToDate(project, packagedArtifactFile);
120 }
121 return packagedArtifactFile;
122 }
123
124 return null;
125 }
126
127 @Override
128 public List<String> findVersions(Artifact artifact) {
129 List<String> versions = getProjects()
130 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
131 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
132 .values()
133 .stream()
134 .map(MavenProject::getVersion)
135 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
136 if (!versions.isEmpty()) {
137 return versions;
138 }
139 return getAllProjects()
140 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
141 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
142 .values()
143 .stream()
144 .filter(p -> Objects.nonNull(findArtifact(p, artifact, false)))
145 .map(MavenProject::getVersion)
146 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
147 }
148
149 @Override
150 public Model findModel(Artifact artifact) {
151 MavenProject project = getProject(artifact);
152 return project == null ? null : project.getModel().getDelegate();
153 }
154
155
156
157
158
159 private File findArtifact(MavenProject project, Artifact artifact, boolean checkUptodate) {
160
161 if ("pom".equals(artifact.getExtension())) {
162 return project.getFile();
163 }
164
165
166 Artifact projectArtifact = findMatchingArtifact(project, artifact);
167 if (projectArtifact != null) {
168
169 File packagedArtifactFile = projectArtifact.getFile();
170 if (packagedArtifactFile != null && packagedArtifactFile.exists()) {
171 return packagedArtifactFile;
172 }
173 }
174
175
176 File packagedArtifactFile = findInProjectLocalRepository(artifact);
177 if (packagedArtifactFile != null
178 && packagedArtifactFile.exists()
179 && (!checkUptodate || isPackagedArtifactUpToDate(project, packagedArtifactFile))) {
180 return packagedArtifactFile;
181 }
182
183 if (!hasBeenPackagedDuringThisSession(project)) {
184
185
186 return determineBuildOutputDirectoryForArtifact(project, artifact);
187 }
188
189
190
191 return null;
192 }
193
194 private File determineBuildOutputDirectoryForArtifact(final MavenProject project, final Artifact artifact) {
195 if (isTestArtifact(artifact)) {
196 if (project.hasLifecyclePhase("test-compile")) {
197 return new File(project.getBuild().getTestOutputDirectory());
198 }
199 } else {
200 String type = artifact.getProperty("type", "");
201 File outputDirectory = new File(project.getBuild().getOutputDirectory());
202
203
204
205 boolean projectCompiledDuringThisSession =
206 project.hasLifecyclePhase("compile") && COMPILE_PHASE_TYPES.contains(type);
207
208
209
210 boolean projectHasOutputFromPreviousSession =
211 !session.getProjects().contains(project) && outputDirectory.exists();
212
213 if (projectHasOutputFromPreviousSession || projectCompiledDuringThisSession) {
214 return outputDirectory;
215 }
216 }
217
218
219
220 return null;
221 }
222
223 private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile) {
224 Path outputDirectory = Paths.get(project.getBuild().getOutputDirectory());
225 if (!outputDirectory.toFile().exists()) {
226 return true;
227 }
228
229 try (Stream<Path> outputFiles = Files.walk(outputDirectory)) {
230
231 long artifactLastModified =
232 Files.getLastModifiedTime(packagedArtifactFile.toPath()).toMillis();
233
234 if (session.getProjectBuildingRequest().getBuildStartTime() != null) {
235 long buildStartTime =
236 session.getProjectBuildingRequest().getBuildStartTime().getTime();
237 if (artifactLastModified > buildStartTime) {
238 return true;
239 }
240 }
241
242 for (Path outputFile : (Iterable<Path>) outputFiles::iterator) {
243 if (Files.isDirectory(outputFile)) {
244 continue;
245 }
246
247 long outputFileLastModified =
248 Files.getLastModifiedTime(outputFile).toMillis();
249 if (outputFileLastModified > artifactLastModified) {
250 LOGGER.warn(
251 "File '{}' is more recent than the packaged artifact for '{}', "
252 + "please run a full `mvn package` build",
253 relativizeOutputFile(outputFile),
254 project.getArtifactId());
255 return true;
256 }
257 }
258
259 return true;
260 } catch (IOException e) {
261 LOGGER.warn(
262 "An I/O error occurred while checking if the packaged artifact is up-to-date "
263 + "against the build output directory. "
264 + "Continuing with the assumption that it is up-to-date.",
265 e);
266 return true;
267 }
268 }
269
270 private boolean hasBeenPackagedDuringThisSession(MavenProject project) {
271 boolean packaged = false;
272 for (String phase : getLifecycles(project)) {
273 switch (phase) {
274 case "clean":
275 packaged = false;
276 break;
277 case "package":
278 case "install":
279 case "deploy":
280 packaged = true;
281 break;
282 default:
283 break;
284 }
285 }
286 return packaged;
287 }
288
289 private Path relativizeOutputFile(final Path outputFile) {
290 Path rootDirectory = session.getRequest().getRootDirectory();
291 return rootDirectory.relativize(outputFile);
292 }
293
294
295
296
297
298
299
300
301 private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) {
302 String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact);
303 return getProjectArtifacts(project)
304 .filter(artifact ->
305 Objects.equals(requestedRepositoryConflictId, ArtifactIdUtils.toVersionlessId(artifact)))
306 .findFirst()
307 .orElse(null);
308 }
309
310
311
312
313
314
315
316 private static boolean isTestArtifact(Artifact artifact) {
317 return ("test-jar".equals(artifact.getProperty("type", "")))
318 || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier()));
319 }
320
321 private File findInProjectLocalRepository(Artifact artifact) {
322 Path target = getArtifactPath(artifact);
323 return Files.isRegularFile(target) ? target.toFile() : null;
324 }
325
326
327
328
329
330
331
332
333
334 private void processEvent(ExecutionEvent event) {
335 MavenProject project = event.getProject();
336 switch (event.getType()) {
337 case MojoStarted:
338 String phase = event.getMojoExecution().getLifecyclePhase();
339 if (phase != null) {
340 Deque<String> phases = getLifecycles(project);
341 if (!Objects.equals(phase, phases.peekLast())) {
342 phases.addLast(phase);
343 if ("clean".equals(phase)) {
344 synchronized (project) {
345 cleanProjectLocalRepository(project);
346 }
347 }
348 }
349 }
350 break;
351 case ProjectSucceeded:
352 case ForkedProjectSucceeded:
353 synchronized (project) {
354 installIntoProjectLocalRepository(project);
355 }
356 break;
357 default:
358 break;
359 }
360 }
361
362 private Deque<String> getLifecycles(MavenProject project) {
363 return lifecycles.computeIfAbsent(project.getId(), k -> new ArrayDeque<>());
364 }
365
366
367
368
369
370
371
372
373
374 private void installIntoProjectLocalRepository(MavenProject project) {
375 if ("pom".equals(project.getPackaging())
376 && !"clean".equals(getLifecycles(project).peekLast())
377 || hasBeenPackagedDuringThisSession(project)) {
378 getProjectArtifacts(project).filter(this::isRegularFile).forEach(this::installIntoProjectLocalRepository);
379 }
380 }
381
382 private void cleanProjectLocalRepository(MavenProject project) {
383 try {
384 Path artifactPath = getProjectLocalRepo()
385 .resolve(project.getGroupId())
386 .resolve(project.getArtifactId())
387 .resolve(project.getVersion());
388 if (Files.isDirectory(artifactPath)) {
389 try (Stream<Path> paths = Files.list(artifactPath)) {
390 for (Path path : (Iterable<Path>) paths::iterator) {
391 Files.delete(path);
392 }
393 }
394 try {
395 Files.delete(artifactPath);
396 Files.delete(artifactPath.getParent());
397 Files.delete(artifactPath.getParent().getParent());
398 } catch (DirectoryNotEmptyException e) {
399
400 }
401 }
402 } catch (IOException e) {
403 LOGGER.error("Error while cleaning project local repository", e);
404 }
405 }
406
407
408
409
410 private Stream<Artifact> getProjectArtifacts(MavenProject project) {
411 Stream<org.apache.maven.artifact.Artifact> artifacts = Stream.concat(
412 Stream.concat(
413
414 Stream.of(new ProjectArtifact(project)),
415
416 "pom".equals(project.getPackaging()) ? Stream.empty() : Stream.of(project.getArtifact())),
417
418 project.getAttachedArtifacts().stream());
419 return artifacts.map(RepositoryUtils::toArtifact);
420 }
421
422 private boolean isRegularFile(Artifact artifact) {
423 return artifact.getFile() != null && artifact.getFile().isFile();
424 }
425
426 private void installIntoProjectLocalRepository(Artifact artifact) {
427 String extension = artifact.getExtension();
428 String classifier = artifact.getClassifier();
429
430 Path target = getArtifactPath(
431 artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), classifier, extension);
432 try {
433 LOGGER.info("Copying {} to project local repository", artifact);
434 Files.createDirectories(target.getParent());
435 Files.copy(
436 artifact.getPath(),
437 target,
438 StandardCopyOption.REPLACE_EXISTING,
439 StandardCopyOption.COPY_ATTRIBUTES);
440 } catch (IOException e) {
441 LOGGER.error("Error while copying artifact to project local repository", e);
442 }
443 }
444
445 private Path getArtifactPath(Artifact artifact) {
446 String groupId = artifact.getGroupId();
447 String artifactId = artifact.getArtifactId();
448 String version = artifact.getBaseVersion();
449 String classifier = artifact.getClassifier();
450 String extension = artifact.getExtension();
451 return getArtifactPath(groupId, artifactId, version, classifier, extension);
452 }
453
454 private Path getArtifactPath(
455 String groupId, String artifactId, String version, String classifier, String extension) {
456 Path repo = getProjectLocalRepo();
457 return repo.resolve(groupId)
458 .resolve(artifactId)
459 .resolve(version)
460 .resolve(artifactId
461 + "-" + version
462 + (classifier != null && !classifier.isEmpty() ? "-" + classifier : "")
463 + "." + extension);
464 }
465
466 private Path getProjectLocalRepo() {
467 if (projectLocalRepository == null) {
468 Path root = session.getRequest().getRootDirectory();
469 List<MavenProject> projects = session.getProjects();
470 if (projects != null) {
471 projectLocalRepository = projects.stream()
472 .filter(project -> Objects.equals(root.toFile(), project.getBasedir()))
473 .findFirst()
474 .map(project -> project.getBuild().getDirectory())
475 .map(Paths::get)
476 .orElseGet(() -> root.resolve("target"))
477 .resolve(PROJECT_LOCAL_REPO);
478 } else {
479 return root.resolve("target").resolve(PROJECT_LOCAL_REPO);
480 }
481 }
482 return projectLocalRepository;
483 }
484
485 private MavenProject getProject(Artifact artifact) {
486 return getProject(artifact, getProjects());
487 }
488
489 private MavenProject getProject(Artifact artifact, Map<String, Map<String, Map<String, MavenProject>>> projects) {
490 return projects.getOrDefault(artifact.getGroupId(), Collections.emptyMap())
491 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
492 .getOrDefault(artifact.getBaseVersion(), null);
493 }
494
495
496 private Map<String, Map<String, Map<String, MavenProject>>> getAllProjects() {
497
498 if (allProjects == null) {
499 List<MavenProject> allProjects = session.getAllProjects();
500 if (allProjects != null) {
501 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
502 allProjects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
503 .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
504 .put(project.getVersion(), project));
505 this.allProjects = map;
506 } else {
507 return Collections.emptyMap();
508 }
509 }
510 return allProjects;
511 }
512
513 private Map<String, Map<String, Map<String, MavenProject>>> getProjects() {
514
515 if (projects == null) {
516 List<MavenProject> projects = session.getProjects();
517 if (projects != null) {
518 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
519 projects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
520 .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
521 .put(project.getVersion(), project));
522 this.projects = map;
523 } else {
524 return Collections.emptyMap();
525 }
526 }
527 return projects;
528 }
529
530
531
532
533
534 @Named
535 @Singleton
536 @SuppressWarnings("unused")
537 static class ReactorReaderSpy implements EventSpy {
538
539 private final Lookup lookup;
540
541 @Inject
542 ReactorReaderSpy(Lookup lookup) {
543 this.lookup = lookup;
544 }
545
546 @Override
547 public void init(Context context) throws Exception {}
548
549 @Override
550 public void onEvent(Object event) throws Exception {
551 if (event instanceof ExecutionEvent executionEvent) {
552 ReactorReader reactorReader = lookup.lookup(ReactorReader.class);
553 reactorReader.processEvent(executionEvent);
554 }
555 }
556
557 @Override
558 public void close() throws Exception {}
559 }
560 }