| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven; |
| |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| import javax.inject.Singleton; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.DirectoryNotEmptyException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.StandardCopyOption; |
| import java.util.ArrayDeque; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| import org.apache.maven.eventspy.EventSpy; |
| import org.apache.maven.execution.ExecutionEvent; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.model.Model; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.project.artifact.ProjectArtifact; |
| import org.apache.maven.repository.internal.MavenWorkspaceReader; |
| import org.codehaus.plexus.PlexusContainer; |
| import org.eclipse.aether.artifact.Artifact; |
| import org.eclipse.aether.repository.WorkspaceRepository; |
| import org.eclipse.aether.util.artifact.ArtifactIdUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * An implementation of a workspace reader that knows how to search the Maven reactor for artifacts, either as packaged |
| * jar if it has been built, or only compile output directory if packaging hasn't happened yet. |
| * |
| */ |
| @Named(ReactorReader.HINT) |
| @SessionScoped |
| class ReactorReader implements MavenWorkspaceReader { |
| public static final String HINT = "reactor"; |
| |
| public static final String PROJECT_LOCAL_REPO = "project-local-repo"; |
| |
| private static final Collection<String> COMPILE_PHASE_TYPES = new HashSet<>( |
| Arrays.asList("jar", "ejb-client", "war", "rar", "ejb3", "par", "sar", "wsr", "har", "app-client")); |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(ReactorReader.class); |
| |
| private final MavenSession session; |
| private final WorkspaceRepository repository; |
| // groupId -> (artifactId -> (version -> project))) |
| private Map<String, Map<String, Map<String, MavenProject>>> projects; |
| private Path projectLocalRepository; |
| // projectId -> Deque<lifecycle> |
| private final Map<String, Deque<String>> lifecycles = new ConcurrentHashMap<>(); |
| |
| @Inject |
| ReactorReader(MavenSession session) { |
| this.session = session; |
| this.repository = new WorkspaceRepository("reactor", null); |
| } |
| |
| // |
| // Public API |
| // |
| |
| public WorkspaceRepository getRepository() { |
| return repository; |
| } |
| |
| public File findArtifact(Artifact artifact) { |
| MavenProject project = getProject(artifact); |
| |
| if (project != null) { |
| File file = findArtifact(project, artifact); |
| if (file == null && project != project.getExecutionProject()) { |
| file = findArtifact(project.getExecutionProject(), artifact); |
| } |
| return file; |
| } |
| |
| // No project, but most certainly a dependency which has been built previously |
| File packagedArtifactFile = findInProjectLocalRepository(artifact); |
| if (packagedArtifactFile != null && packagedArtifactFile.exists()) { |
| return packagedArtifactFile; |
| } |
| |
| return null; |
| } |
| |
| public List<String> findVersions(Artifact artifact) { |
| return getProjects() |
| .getOrDefault(artifact.getGroupId(), Collections.emptyMap()) |
| .getOrDefault(artifact.getArtifactId(), Collections.emptyMap()) |
| .values() |
| .stream() |
| .filter(p -> Objects.nonNull(findArtifact(p, artifact))) |
| .map(MavenProject::getVersion) |
| .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); |
| } |
| |
| @Override |
| public Model findModel(Artifact artifact) { |
| MavenProject project = getProject(artifact); |
| return project == null ? null : project.getModel(); |
| } |
| |
| // |
| // Implementation |
| // |
| |
| private File findArtifact(MavenProject project, Artifact artifact) { |
| // POMs are always returned from the file system |
| if ("pom".equals(artifact.getExtension())) { |
| return project.getFile(); |
| } |
| |
| // First check in the project local repository |
| File packagedArtifactFile = findInProjectLocalRepository(artifact); |
| if (packagedArtifactFile != null |
| && packagedArtifactFile.exists() |
| && isPackagedArtifactUpToDate(project, packagedArtifactFile)) { |
| return packagedArtifactFile; |
| } |
| |
| // Get the matching artifact from the project |
| Artifact projectArtifact = findMatchingArtifact(project, artifact); |
| if (projectArtifact != null) { |
| // If the artifact has been associated to a file, use it |
| packagedArtifactFile = projectArtifact.getFile(); |
| if (packagedArtifactFile != null && packagedArtifactFile.exists()) { |
| return packagedArtifactFile; |
| } |
| } |
| |
| if (!hasBeenPackagedDuringThisSession(project)) { |
| // fallback to loose class files only if artifacts haven't been packaged yet |
| // and only for plain old jars. Not war files, not ear files, not anything else. |
| return determineBuildOutputDirectoryForArtifact(project, artifact); |
| } |
| |
| // The fall-through indicates that the artifact cannot be found; |
| // for instance if package produced nothing or classifier problems. |
| return null; |
| } |
| |
| private File determineBuildOutputDirectoryForArtifact(final MavenProject project, final Artifact artifact) { |
| if (isTestArtifact(artifact)) { |
| if (project.hasLifecyclePhase("test-compile")) { |
| return new File(project.getBuild().getTestOutputDirectory()); |
| } |
| } else { |
| String type = artifact.getProperty("type", ""); |
| File outputDirectory = new File(project.getBuild().getOutputDirectory()); |
| |
| // Check if the project is being built during this session, and if we can expect any output. |
| // There is no need to check if the build has created any outputs, see MNG-2222. |
| boolean projectCompiledDuringThisSession = |
| project.hasLifecyclePhase("compile") && COMPILE_PHASE_TYPES.contains(type); |
| |
| // Check if the project is part of the session (not filtered by -pl, -rf, etc). If so, we check |
| // if a possible earlier Maven invocation produced some output for that project which we can use. |
| boolean projectHasOutputFromPreviousSession = |
| !session.getProjects().contains(project) && outputDirectory.exists(); |
| |
| if (projectHasOutputFromPreviousSession || projectCompiledDuringThisSession) { |
| return outputDirectory; |
| } |
| } |
| |
| // The fall-through indicates that the artifact cannot be found; |
| // for instance if package produced nothing or classifier problems. |
| return null; |
| } |
| |
| private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile) { |
| Path outputDirectory = Paths.get(project.getBuild().getOutputDirectory()); |
| if (!outputDirectory.toFile().exists()) { |
| return true; |
| } |
| |
| try (Stream<Path> outputFiles = Files.walk(outputDirectory)) { |
| // Not using File#lastModified() to avoid a Linux JDK8 milliseconds precision bug: JDK-8177809. |
| long artifactLastModified = |
| Files.getLastModifiedTime(packagedArtifactFile.toPath()).toMillis(); |
| |
| if (session.getProjectBuildingRequest().getBuildStartTime() != null) { |
| long buildStartTime = |
| session.getProjectBuildingRequest().getBuildStartTime().getTime(); |
| if (artifactLastModified > buildStartTime) { |
| return true; |
| } |
| } |
| |
| for (Path outputFile : (Iterable<Path>) outputFiles::iterator) { |
| if (Files.isDirectory(outputFile)) { |
| continue; |
| } |
| |
| long outputFileLastModified = |
| Files.getLastModifiedTime(outputFile).toMillis(); |
| if (outputFileLastModified > artifactLastModified) { |
| LOGGER.warn( |
| "File '{}' is more recent than the packaged artifact for '{}', " |
| + "please run a full `mvn package` build", |
| relativizeOutputFile(outputFile), |
| project.getArtifactId()); |
| return true; |
| } |
| } |
| |
| return true; |
| } catch (IOException e) { |
| LOGGER.warn( |
| "An I/O error occurred while checking if the packaged artifact is up-to-date " |
| + "against the build output directory. " |
| + "Continuing with the assumption that it is up-to-date.", |
| e); |
| return true; |
| } |
| } |
| |
| private boolean hasBeenPackagedDuringThisSession(MavenProject project) { |
| boolean packaged = false; |
| for (String phase : getLifecycles(project)) { |
| switch (phase) { |
| case "clean": |
| packaged = false; |
| break; |
| case "package": |
| case "install": |
| case "deploy": |
| packaged = true; |
| break; |
| default: |
| break; |
| } |
| } |
| return packaged; |
| } |
| |
| private Path relativizeOutputFile(final Path outputFile) { |
| Path projectBaseDirectory = |
| Paths.get(session.getRequest().getMultiModuleProjectDirectory().toURI()); |
| return projectBaseDirectory.relativize(outputFile); |
| } |
| |
| /** |
| * Tries to resolve the specified artifact from the artifacts of the given project. |
| * |
| * @param project The project to try to resolve the artifact from, must not be <code>null</code>. |
| * @param requestedArtifact The artifact to resolve, must not be <code>null</code>. |
| * @return The matching artifact from the project or <code>null</code> if not found. Note that this |
| */ |
| private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) { |
| String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact); |
| return getProjectArtifacts(project) |
| .filter(artifact -> |
| Objects.equals(requestedRepositoryConflictId, ArtifactIdUtils.toVersionlessId(artifact))) |
| .findFirst() |
| .orElse(null); |
| } |
| |
| /** |
| * Determines whether the specified artifact refers to test classes. |
| * |
| * @param artifact The artifact to check, must not be {@code null}. |
| * @return {@code true} if the artifact refers to test classes, {@code false} otherwise. |
| */ |
| private static boolean isTestArtifact(Artifact artifact) { |
| return ("test-jar".equals(artifact.getProperty("type", ""))) |
| || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier())); |
| } |
| |
| private File findInProjectLocalRepository(Artifact artifact) { |
| Path target = getArtifactPath(artifact); |
| return Files.isRegularFile(target) ? target.toFile() : null; |
| } |
| |
| /** |
| * We are interested in project success events, in which case we call |
| * the {@link #installIntoProjectLocalRepository(MavenProject)} method. |
| * The mojo started event is also captured to determine the lifecycle |
| * phases the project has been through. |
| * |
| * @param event the execution event |
| */ |
| private void processEvent(ExecutionEvent event) { |
| MavenProject project = event.getProject(); |
| switch (event.getType()) { |
| case MojoStarted: |
| String phase = event.getMojoExecution().getLifecyclePhase(); |
| if (phase != null) { |
| Deque<String> phases = getLifecycles(project); |
| if (!Objects.equals(phase, phases.peekLast())) { |
| phases.addLast(phase); |
| if ("clean".equals(phase)) { |
| cleanProjectLocalRepository(project); |
| } |
| } |
| } |
| break; |
| case ProjectSucceeded: |
| case ForkedProjectSucceeded: |
| installIntoProjectLocalRepository(project); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| private Deque<String> getLifecycles(MavenProject project) { |
| return lifecycles.computeIfAbsent(project.getId(), k -> new ArrayDeque<>()); |
| } |
| |
| /** |
| * Copy packaged and attached artifacts from this project to the |
| * project local repository. |
| * This allows a subsequent build to resume while still being able |
| * to locate attached artifacts. |
| * |
| * @param project the project to copy artifacts from |
| */ |
| private void installIntoProjectLocalRepository(MavenProject project) { |
| if ("pom".equals(project.getPackaging()) |
| && !"clean".equals(getLifecycles(project).peekLast()) |
| || hasBeenPackagedDuringThisSession(project)) { |
| getProjectArtifacts(project).filter(this::isRegularFile).forEach(this::installIntoProjectLocalRepository); |
| } |
| } |
| |
| private void cleanProjectLocalRepository(MavenProject project) { |
| try { |
| Path artifactPath = getProjectLocalRepo() |
| .resolve(project.getGroupId()) |
| .resolve(project.getArtifactId()) |
| .resolve(project.getVersion()); |
| if (Files.isDirectory(artifactPath)) { |
| try (Stream<Path> paths = Files.list(artifactPath)) { |
| for (Path path : (Iterable<Path>) paths::iterator) { |
| Files.delete(path); |
| } |
| } |
| try { |
| Files.delete(artifactPath); |
| Files.delete(artifactPath.getParent()); |
| Files.delete(artifactPath.getParent().getParent()); |
| } catch (DirectoryNotEmptyException e) { |
| // ignore |
| } |
| } |
| } catch (IOException e) { |
| LOGGER.error("Error while cleaning project local repository", e); |
| } |
| } |
| |
| /** |
| * Retrieve a stream of the project's artifacts |
| */ |
| private Stream<Artifact> getProjectArtifacts(MavenProject project) { |
| Stream<org.apache.maven.artifact.Artifact> artifacts = Stream.concat( |
| Stream.concat( |
| // pom artifact |
| Stream.of(new ProjectArtifact(project)), |
| // main project artifact if not a pom |
| "pom".equals(project.getPackaging()) ? Stream.empty() : Stream.of(project.getArtifact())), |
| // attached artifacts |
| project.getAttachedArtifacts().stream()); |
| return artifacts.map(RepositoryUtils::toArtifact); |
| } |
| |
| private boolean isRegularFile(Artifact artifact) { |
| return artifact.getFile() != null && artifact.getFile().isFile(); |
| } |
| |
| private void installIntoProjectLocalRepository(Artifact artifact) { |
| Path target = getArtifactPath(artifact); |
| try { |
| LOGGER.info("Copying {} to project local repository", artifact); |
| Files.createDirectories(target.getParent()); |
| Files.copy( |
| artifact.getFile().toPath(), |
| target, |
| StandardCopyOption.REPLACE_EXISTING, |
| StandardCopyOption.COPY_ATTRIBUTES); |
| } catch (IOException e) { |
| LOGGER.error("Error while copying artifact to project local repository", e); |
| } |
| } |
| |
| private Path getArtifactPath(Artifact artifact) { |
| String groupId = artifact.getGroupId(); |
| String artifactId = artifact.getArtifactId(); |
| String version = artifact.getBaseVersion(); |
| String classifier = artifact.getClassifier(); |
| String extension = artifact.getExtension(); |
| Path repo = getProjectLocalRepo(); |
| return repo.resolve(groupId) |
| .resolve(artifactId) |
| .resolve(version) |
| .resolve(artifactId |
| + "-" + version |
| + (classifier != null && !classifier.isEmpty() ? "-" + classifier : "") |
| + "." + extension); |
| } |
| |
| private Path getProjectLocalRepo() { |
| if (projectLocalRepository == null) { |
| Path root = session.getRequest().getMultiModuleProjectDirectory().toPath(); |
| List<MavenProject> projects = session.getProjects(); |
| if (projects != null) { |
| projectLocalRepository = projects.stream() |
| .filter(project -> Objects.equals(root.toFile(), project.getBasedir())) |
| .findFirst() |
| .map(project -> project.getBuild().getDirectory()) |
| .map(Paths::get) |
| .orElseGet(() -> root.resolve("target")) |
| .resolve(PROJECT_LOCAL_REPO); |
| } else { |
| return root.resolve("target").resolve(PROJECT_LOCAL_REPO); |
| } |
| } |
| return projectLocalRepository; |
| } |
| |
| private MavenProject getProject(Artifact artifact) { |
| return getProjects() |
| .getOrDefault(artifact.getGroupId(), Collections.emptyMap()) |
| .getOrDefault(artifact.getArtifactId(), Collections.emptyMap()) |
| .getOrDefault(artifact.getBaseVersion(), null); |
| } |
| |
| // groupId -> (artifactId -> (version -> project))) |
| private Map<String, Map<String, Map<String, MavenProject>>> getProjects() { |
| // compute the projects mapping |
| if (projects == null) { |
| List<MavenProject> allProjects = session.getAllProjects(); |
| if (allProjects != null) { |
| Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>(); |
| allProjects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>()) |
| .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>()) |
| .put(project.getVersion(), project)); |
| this.projects = map; |
| } else { |
| return Collections.emptyMap(); |
| } |
| } |
| return projects; |
| } |
| |
| /** |
| * Singleton class used to receive events by implementing the EventSpy. |
| * It simply forwards all {@code ExecutionEvent}s to the {@code ReactorReader}. |
| */ |
| @Named |
| @Singleton |
| @SuppressWarnings("unused") |
| static class ReactorReaderSpy implements EventSpy { |
| |
| final PlexusContainer container; |
| |
| @Inject |
| ReactorReaderSpy(PlexusContainer container) { |
| this.container = container; |
| } |
| |
| @Override |
| public void init(Context context) throws Exception {} |
| |
| @Override |
| @SuppressWarnings("checkstyle:MissingSwitchDefault") |
| public void onEvent(Object event) throws Exception { |
| if (event instanceof ExecutionEvent) { |
| ReactorReader reactorReader = container.lookup(ReactorReader.class); |
| reactorReader.processEvent((ExecutionEvent) event); |
| } |
| } |
| |
| @Override |
| public void close() throws Exception {} |
| } |
| } |