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