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.List;
39 import java.util.Map;
40 import java.util.Objects;
41 import java.util.concurrent.ConcurrentHashMap;
42 import java.util.stream.Collectors;
43 import java.util.stream.Stream;
44
45 import org.apache.maven.eventspy.EventSpy;
46 import org.apache.maven.execution.ExecutionEvent;
47 import org.apache.maven.execution.MavenSession;
48 import org.apache.maven.model.Model;
49 import org.apache.maven.project.MavenProject;
50 import org.apache.maven.project.artifact.ProjectArtifact;
51 import org.apache.maven.repository.internal.MavenWorkspaceReader;
52 import org.codehaus.plexus.PlexusContainer;
53 import org.eclipse.aether.artifact.Artifact;
54 import org.eclipse.aether.repository.WorkspaceRepository;
55 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
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 =
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 Deque<String> phases = getLifecycles(project);
322 if (!Objects.equals(phase, phases.peekLast())) {
323 phases.addLast(phase);
324 if ("clean".equals(phase)) {
325 cleanProjectLocalRepository(project);
326 }
327 }
328 break;
329 case ProjectSucceeded:
330 case ForkedProjectSucceeded:
331 installIntoProjectLocalRepository(project);
332 break;
333 default:
334 break;
335 }
336 }
337
338 private Deque<String> getLifecycles(MavenProject project) {
339 return lifecycles.computeIfAbsent(project.getId(), k -> new ArrayDeque<>());
340 }
341
342
343
344
345
346
347
348
349
350 private void installIntoProjectLocalRepository(MavenProject project) {
351 if ("pom".equals(project.getPackaging())
352 && !"clean".equals(getLifecycles(project).peekLast())
353 || hasBeenPackagedDuringThisSession(project)) {
354 getProjectArtifacts(project).filter(this::isRegularFile).forEach(this::installIntoProjectLocalRepository);
355 }
356 }
357
358 private void cleanProjectLocalRepository(MavenProject project) {
359 try {
360 Path artifactPath = getProjectLocalRepo()
361 .resolve(project.getGroupId())
362 .resolve(project.getArtifactId())
363 .resolve(project.getVersion());
364 if (Files.isDirectory(artifactPath)) {
365 try (Stream<Path> paths = Files.list(artifactPath)) {
366 for (Path path : (Iterable<Path>) paths::iterator) {
367 Files.delete(path);
368 }
369 }
370 try {
371 Files.delete(artifactPath);
372 Files.delete(artifactPath.getParent());
373 Files.delete(artifactPath.getParent().getParent());
374 } catch (DirectoryNotEmptyException e) {
375
376 }
377 }
378 } catch (IOException e) {
379 LOGGER.error("Error while cleaning project local repository", e);
380 }
381 }
382
383
384
385
386 private Stream<Artifact> getProjectArtifacts(MavenProject project) {
387 Stream<org.apache.maven.artifact.Artifact> artifacts = Stream.concat(
388 Stream.concat(
389
390 Stream.of(new ProjectArtifact(project)),
391
392 "pom".equals(project.getPackaging()) ? Stream.empty() : Stream.of(project.getArtifact())),
393
394 project.getAttachedArtifacts().stream());
395 return artifacts.map(RepositoryUtils::toArtifact);
396 }
397
398 private boolean isRegularFile(Artifact artifact) {
399 return artifact.getFile() != null && artifact.getFile().isFile();
400 }
401
402 private void installIntoProjectLocalRepository(Artifact artifact) {
403 Path target = getArtifactPath(artifact);
404 try {
405 LOGGER.info("Copying {} to project local repository", artifact);
406 Files.createDirectories(target.getParent());
407 Files.copy(
408 artifact.getFile().toPath(),
409 target,
410 StandardCopyOption.REPLACE_EXISTING,
411 StandardCopyOption.COPY_ATTRIBUTES);
412 } catch (IOException e) {
413 LOGGER.error("Error while copying artifact to project local repository", e);
414 }
415 }
416
417 private Path getArtifactPath(Artifact artifact) {
418 String groupId = artifact.getGroupId();
419 String artifactId = artifact.getArtifactId();
420 String version = artifact.getBaseVersion();
421 String classifier = artifact.getClassifier();
422 String extension = artifact.getExtension();
423 Path repo = getProjectLocalRepo();
424 return repo.resolve(groupId)
425 .resolve(artifactId)
426 .resolve(version)
427 .resolve(artifactId
428 + "-" + version
429 + (classifier != null && !classifier.isEmpty() ? "-" + classifier : "")
430 + "." + extension);
431 }
432
433 private Path getProjectLocalRepo() {
434 if (projectLocalRepository == null) {
435 Path root = session.getRequest().getMultiModuleProjectDirectory().toPath();
436 List<MavenProject> projects = session.getProjects();
437 if (projects != null) {
438 projectLocalRepository = projects.stream()
439 .filter(project -> Objects.equals(root.toFile(), project.getBasedir()))
440 .findFirst()
441 .map(project -> project.getBuild().getDirectory())
442 .map(Paths::get)
443 .orElseGet(() -> root.resolve("target"))
444 .resolve(PROJECT_LOCAL_REPO);
445 } else {
446 return root.resolve("target").resolve(PROJECT_LOCAL_REPO);
447 }
448 }
449 return projectLocalRepository;
450 }
451
452 private MavenProject getProject(Artifact artifact) {
453 return getProjects()
454 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
455 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
456 .getOrDefault(artifact.getBaseVersion(), null);
457 }
458
459
460 private Map<String, Map<String, Map<String, MavenProject>>> getProjects() {
461
462 if (projects == null) {
463 List<MavenProject> allProjects = session.getAllProjects();
464 if (allProjects != null) {
465 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
466 allProjects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
467 .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
468 .put(project.getVersion(), project));
469 this.projects = map;
470 } else {
471 return Collections.emptyMap();
472 }
473 }
474 return projects;
475 }
476
477
478
479
480
481 @Named
482 @Singleton
483 @SuppressWarnings("unused")
484 static class ReactorReaderSpy implements EventSpy {
485
486 final PlexusContainer container;
487
488 @Inject
489 ReactorReaderSpy(PlexusContainer container) {
490 this.container = container;
491 }
492
493 @Override
494 public void init(Context context) throws Exception {}
495
496 @Override
497 @SuppressWarnings("checkstyle:MissingSwitchDefault")
498 public void onEvent(Object event) throws Exception {
499 if (event instanceof ExecutionEvent) {
500 ReactorReader reactorReader = container.lookup(ReactorReader.class);
501 reactorReader.processEvent((ExecutionEvent) event);
502 }
503 }
504
505 @Override
506 public void close() throws Exception {}
507 }
508 }