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