diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java b/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java index e1fc54ba5f53..9ebc46eff314 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/CLIManager.java @@ -123,6 +123,8 @@ public class CLIManager { public static final String IGNORE_TRANSITIVE_REPOSITORIES = "itr"; + public static final String INSTALLATION_STATUS = "status"; + /** This option is deprecated and may be repurposed as Java debug in a future version. * Use {@code -X/--verbose} instead. */ @Deprecated @@ -335,6 +337,10 @@ public CLIManager() { .longOpt("legacy-local-repository") .desc("UNSUPPORTED: Use of this option will make Maven invocation fail.") .build()); + options.addOption(Option.builder() + .longOpt(INSTALLATION_STATUS) + .desc("Display Maven installation status") + .build()); // Deprecated options.addOption(Option.builder() diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index b4219e66f0df..d8b460112604 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -36,6 +36,7 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.IntStream; import java.util.stream.Stream; import com.google.inject.AbstractModule; @@ -179,6 +180,8 @@ public class MavenCli { private MessageBuilderFactory messageBuilderFactory; + private PlexusContainer plexusContainer; + private static final Pattern NEXT_LINE = Pattern.compile("\r?\n"); public MavenCli() { @@ -284,6 +287,7 @@ public int doMain(CliRequest cliRequest) { configure(cliRequest); toolchains(cliRequest); populateRequest(cliRequest); + status(cliRequest); encryption(cliRequest); return execute(cliRequest); } catch (ExitException e) { @@ -451,6 +455,23 @@ private void informativeCommands(CliRequest cliRequest) throws ExitException { } } + private void status(CliRequest cliRequest) throws Exception { + slf4jLoggerFactory = LoggerFactory.getILoggerFactory(); + if (cliRequest.commandLine.hasOption(CLIManager.INSTALLATION_STATUS)) { + MavenStatusCommand mavenStatusCommand = new MavenStatusCommand(plexusContainer); + final List issues = mavenStatusCommand.verify(cliRequest.getRequest()); + if (!issues.isEmpty()) { + slf4jLogger.info(""); + slf4jLogger.error("The following issues where found"); + IntStream.range(0, issues.size()).forEach(i -> slf4jLogger.error("{}. {}", i + 1, issues.get(i))); + throw new ExitException(1); + } + + slf4jLogger.info("No installation issues found."); + throw new ExitException(0); + } + } + private CommandLine cliMerge(CommandLine mavenConfig, CommandLine mavenCli) { CommandLine.Builder commandLineBuilder = new CommandLine.Builder(); @@ -740,6 +761,8 @@ public Object getValue(String expression) { dispatcher = (DefaultSecDispatcher) container.lookup(SecDispatcher.class, "maven"); + plexusContainer = container; + return container; } diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenStatusCommand.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenStatusCommand.java new file mode 100644 index 000000000000..2b3cc88bc19d --- /dev/null +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenStatusCommand.java @@ -0,0 +1,214 @@ +/* + * 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.cli; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.api.ArtifactCoordinate; +import org.apache.maven.api.Session; +import org.apache.maven.api.services.ArtifactResolver; +import org.apache.maven.api.services.ArtifactResolverException; +import org.apache.maven.api.services.ArtifactResolverResult; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.bridge.MavenRepositorySystem; +import org.apache.maven.execution.DefaultMavenExecutionResult; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionRequestPopulationException; +import org.apache.maven.execution.MavenExecutionRequestPopulator; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.internal.impl.DefaultArtifactCoordinate; +import org.apache.maven.internal.impl.DefaultSessionFactory; +import org.apache.maven.internal.impl.InternalMavenSession; +import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.resolver.RepositorySystemSessionFactory; +import org.apache.maven.session.scope.internal.SessionScope; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MavenStatusCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(MavenStatusCommand.class); + + /** + * In order to verify artifacts can be downloaded from the remote repositories we want to resolve an actual + * artifact. The Apache Maven artifact was chosen as it eventually, be it by proxy, mirror or directly, will be + * gathered from the central repository. The version is chosen arbitrarily since any listed should work. + */ + public static final Artifact APACHE_MAVEN_ARTIFACT = + new DefaultArtifact("org.apache.maven", "apache-maven", null, "pom", "3.8.6"); + + private final MavenExecutionRequestPopulator mavenExecutionRequestPopulator; + private final ArtifactResolver artifactResolver; + private final RemoteRepositoryConnectionVerifier remoteRepositoryConnectionVerifier; + private final DefaultSessionFactory defaultSessionFactory; + private final RepositorySystemSessionFactory repoSession; + private final MavenRepositorySystem repositorySystem; + private final SessionScope sessionScope; + private Path tempLocalRepository; + + public MavenStatusCommand(final PlexusContainer container) throws ComponentLookupException { + this.remoteRepositoryConnectionVerifier = new RemoteRepositoryConnectionVerifier(container); + this.mavenExecutionRequestPopulator = container.lookup(MavenExecutionRequestPopulator.class); + this.artifactResolver = container.lookup(ArtifactResolver.class); + this.defaultSessionFactory = container.lookup(DefaultSessionFactory.class); + this.repoSession = container.lookup(RepositorySystemSessionFactory.class); + this.sessionScope = container.lookup(SessionScope.class); + this.repositorySystem = container.lookup(MavenRepositorySystem.class); + } + + public List verify(final MavenExecutionRequest cliRequest) throws MavenExecutionRequestPopulationException { + final MavenExecutionRequest mavenExecutionRequest = mavenExecutionRequestPopulator.populateDefaults(cliRequest); + + final ArtifactRepository localRepository = cliRequest.getLocalRepository(); + + final List localRepositoryIssues = + verifyLocalRepository(Paths.get(URI.create(localRepository.getUrl()))); + + // We overwrite the local repository with a temporary directory to avoid using a cached version of the artifact. + setTemporaryLocalRepositoryPathOnRequest(cliRequest); + + final List remoteRepositoryIssues = + verifyRemoteRepositoryConnections(cliRequest.getRemoteRepositories(), mavenExecutionRequest); + final List artifactResolutionIssues = verifyArtifactResolution(mavenExecutionRequest); + + cleanupTempFiles(); + + // Collect all issues into a single list + return Stream.of(localRepositoryIssues, remoteRepositoryIssues, artifactResolutionIssues) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private void cleanupTempFiles() { + if (tempLocalRepository != null) { + try (Stream files = Files.walk(tempLocalRepository)) { + files.sorted(Comparator.reverseOrder()) // Sort in reverse order so that directories are deleted last + .map(Path::toFile) + .forEach(File::delete); + } catch (IOException ioe) { + LOGGER.debug("Failed to delete temporary local repository", ioe); + } + } + } + + private void setTemporaryLocalRepositoryPathOnRequest(final MavenExecutionRequest request) { + try { + tempLocalRepository = Files.createTempDirectory("mvn-status").toAbsolutePath(); + request.setLocalRepositoryPath(tempLocalRepository.toString()); + request.setLocalRepository(repositorySystem.createLocalRepository(request, tempLocalRepository.toFile())); + } catch (Exception ex) { + LOGGER.debug("Could not create temporary local repository", ex); + LOGGER.warn("Artifact resolution test is less accurate as it may use earlier resolution results."); + } + } + + private List verifyRemoteRepositoryConnections( + final List remoteRepositories, final MavenExecutionRequest mavenExecutionRequest) { + final List issues = new ArrayList<>(); + + for (ArtifactRepository remoteRepository : remoteRepositories) { + try (RepositorySystemSession.CloseableSession repositorySession = repoSession + .newRepositorySessionBuilder(mavenExecutionRequest) + .build()) { + remoteRepositoryConnectionVerifier + .verifyConnectionToRemoteRepository(repositorySession, remoteRepository) + .ifPresent(issues::add); + } + } + + return issues; + } + + private List verifyArtifactResolution(final MavenExecutionRequest mavenExecutionRequest) { + this.sessionScope.enter(); + try (RepositorySystemSession.CloseableSession repoSession = this.repoSession + .newRepositorySessionBuilder(mavenExecutionRequest) + .build()) { + final Session session = this.defaultSessionFactory.newSession( + new MavenSession(repoSession, mavenExecutionRequest, new DefaultMavenExecutionResult())); + InternalMavenSession internalSession = InternalMavenSession.from(session); + sessionScope.seed(InternalMavenSession.class, internalSession); + ArtifactCoordinate artifactCoordinate = + new DefaultArtifactCoordinate(InternalSession.from(session), APACHE_MAVEN_ARTIFACT); + ArtifactResolverResult resolverResult = + artifactResolver.resolve(session, Collections.singleton(artifactCoordinate)); + resolverResult + .getArtifacts() + .forEach((key, value) -> LOGGER.debug("Successfully resolved {} to {}", key, value)); + + return Collections.emptyList(); + } catch (ArtifactResolverException are) { + return extractIssuesFromArtifactResolverException(are); + } finally { + this.sessionScope.exit(); + LOGGER.info("Artifact resolution check completed"); + } + } + + private List extractIssuesFromArtifactResolverException(final Exception exception) { + final boolean isArtifactResolutionException = exception.getCause() instanceof ArtifactResolutionException; + if (isArtifactResolutionException) { + final ArtifactResolutionException are = (ArtifactResolutionException) exception.getCause(); + return are.getResults().stream() + .map(ArtifactResult::getExceptions) + .flatMap(List::stream) + .map(Throwable::getMessage) + .collect(Collectors.toList()); + } else { + return Collections.singletonList(exception.getMessage()); + } + } + + private List verifyLocalRepository(final Path localRepositoryPath) { + final List issues = new ArrayList<>(); + + if (!Files.isDirectory(localRepositoryPath)) { + issues.add(String.format("Local repository path '%s' is not a directory.", localRepositoryPath)); + } + + if (!Files.isReadable(localRepositoryPath)) { + issues.add(String.format("No read permissions on local repository '%s'.", localRepositoryPath)); + } + + if (!Files.isWritable(localRepositoryPath)) { + issues.add(String.format("No write permissions on local repository '%s'.", localRepositoryPath)); + } + + LOGGER.info("Local repository setup check completed"); + return issues; + } +} diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/RemoteRepositoryConnectionVerifier.java b/maven-embedder/src/main/java/org/apache/maven/cli/RemoteRepositoryConnectionVerifier.java new file mode 100644 index 000000000000..9935b08de1a6 --- /dev/null +++ b/maven-embedder/src/main/java/org/apache/maven/cli/RemoteRepositoryConnectionVerifier.java @@ -0,0 +1,94 @@ +/* + * 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.cli; + +import java.net.URI; +import java.util.Optional; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.GetTask; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterProvider; +import org.eclipse.aether.transfer.NoTransporterException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class to verify connection to a remote repository. + */ +public class RemoteRepositoryConnectionVerifier { + private final Logger logger; + private final TransporterProvider transporterProvider; + + public RemoteRepositoryConnectionVerifier(final PlexusContainer container) throws ComponentLookupException { + this.logger = LoggerFactory.getILoggerFactory().getLogger(RemoteRepositoryConnectionVerifier.class.getName()); + this.transporterProvider = container.lookup(TransporterProvider.class); + } + + public Optional verifyConnectionToRemoteRepository( + final RepositorySystemSession session, final ArtifactRepository artifactRepository) { + final RemoteRepository repository = RepositoryUtils.toRepo(artifactRepository); + + try { + final Transporter transporter = transporterProvider.newTransporter(session, repository); + return verifyConnectionUsingTransport(transporter, repository); + } catch (final NoTransporterException nte) { + final String message = String.format( + "There is no compatible transport for remote repository '%s' with location '%s'", + repository.getId(), repository.getUrl()); + return Optional.of(message); + } + } + + private Optional verifyConnectionUsingTransport( + final Transporter transporter, final RemoteRepository remoteRepository) { + try { + final GetTask task = new GetTask(URI.create("")); + transporter.get(task); + // We could connect, but uncertain to what. Could be the repository, could be a valid web page. + logger.info( + "Connection check for repository '{}' at '{}' completed", + remoteRepository.getId(), + remoteRepository.getUrl()); + return Optional.empty(); + } catch (final Exception e) { + final int errorOrArtifactNotFound = transporter.classify(e); + if (Transporter.ERROR_NOT_FOUND == errorOrArtifactNotFound) { + // No-op since we could connect to the repository + // However we do not know what should or shouldn't be present + return Optional.empty(); + } + // In this case it is Transporter.ERROR_OTHER + return Optional.of(formatException(remoteRepository, e)); + } + } + + private String formatException(final RemoteRepository remoteRepository, final Exception e) { + final String repositoryId = remoteRepository.getId(); + final String repositoryUrl = remoteRepository.getUrl(); + final String repository = String.format("%s [%s]", repositoryId, repositoryUrl); + + return String.format("Connection to %s not possible. Cause: %s", repository, e.getMessage()); + } +}