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