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