From bec442bd0babfd6cc185fa10ec361d383132ad80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 24 Sep 2026 12:37:45 +0300 Subject: [PATCH] feat: resolve local repository paths without Maven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Efe Gökdemir --- README.md | 9 +- .../support/LocalRepositoryExecutorTool.java | 162 ++++++++++++++++++ .../LocalRepositoryExecutorToolTest.java | 97 +++++++++++ 3 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 maven-executor/src/main/java/org/apache/maven/executor/support/LocalRepositoryExecutorTool.java create mode 100644 maven-executor/src/test/java/org/apache/maven/executor/LocalRepositoryExecutorToolTest.java diff --git a/README.md b/README.md index 25a24c2..94c947f 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,19 @@ The Executor suite provides means to execute Maven 3 and Maven 4 programmaticall usually as part of some "integration builds" suite for some plugin or extensions, or Maven itself. It is meant to replace [maven-invoker](https://github.com/apache/maven-invoker/) and [maven-verifier](https://github.com/apache/maven-verifier/). -The dependency-less artifact `maven-executor` provides API and two executor -implementations out of the box: "forked" and "embedded". There are more providers in +The artifact `maven-executor` has no compile-time dependencies and provides API +and two executor implementations out of the box: "forked" and "embedded". There are more providers in `providers/` sub-projects. In total, below is list of supported executors: * `forked` OOTB with `maven-executor` * `embedded` OOTB with `maven-executor` * `docker-exe` uses Docker CLI and runs [Apache Maven Docker Image](https://hub.docker.com/_/maven) * `testcontainers` uses Docker via TestContainers and runs [Apache Maven Docker Image](https://hub.docker.com/_/maven) +The `LocalRepositoryExecutorTool` calculates local-repository, artifact, and metadata paths +without running Maven or downloading additional plugins. The `ToolboxExecutorTool` remains +available for diagnostic dumps and other callers that explicitly need Maveniverse Toolbox; +using it may download and execute that third-party plugin. + Versions and Branches --------------------- diff --git a/maven-executor/src/main/java/org/apache/maven/executor/support/LocalRepositoryExecutorTool.java b/maven-executor/src/main/java/org/apache/maven/executor/support/LocalRepositoryExecutorTool.java new file mode 100644 index 0000000..a53825a --- /dev/null +++ b/maven-executor/src/main/java/org/apache/maven/executor/support/LocalRepositoryExecutorTool.java @@ -0,0 +1,162 @@ +/* + * 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.executor.support; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.maven.executor.Executor; +import org.apache.maven.executor.ExecutorException; +import org.apache.maven.executor.ExecutorRequest; +import org.apache.maven.executor.ExecutorTool; +import org.w3c.dom.Document; + +/** + * An {@link ExecutorTool} which calculates local-repository paths without invoking a Maven plugin. + * Diagnostic dumps are delegated to {@link ToolboxExecutorTool}, because they require Maven itself. + */ +public final class LocalRepositoryExecutorTool implements ExecutorTool { + private final ToolboxExecutorTool toolbox; + + public LocalRepositoryExecutorTool(Executor executor) { + toolbox = new ToolboxExecutorTool(Objects.requireNonNull(executor)); + } + + @Override + public Map dump(ExecutorRequest.Builder request) throws ExecutorException { + return toolbox.dump(request); + } + + @Override + public String localRepository(ExecutorRequest.Builder request) throws ExecutorException { + ExecutorRequest built = request.build(); + String configured = property(built, "maven.repo.local"); + if (configured != null) { + return configured; + } + Path settings = settingsFile(built); + if (settings != null && Files.isRegularFile(settings)) { + String localRepository = localRepositoryFromSettings(settings); + if (localRepository != null) { + return expand(localRepository, built).toString(); + } + } + return built.userHomeDirectory().resolve(".m2").resolve("repository").toString(); + } + + @Override + public String artifactPath(ExecutorRequest.Builder request, String gav, String repositoryId) + throws ExecutorException { + String[] parts = coordinates(gav, 3, 5); + String group = parts[0]; + String artifact = parts[1]; + String version = parts[2]; + String type = parts.length > 3 && !parts[3].isEmpty() ? parts[3] : "jar"; + String classifier = parts.length > 4 ? parts[4] : ""; + String fileName = artifact + "-" + version + + (classifier.isEmpty() ? "" : "-" + classifier) + "." + type; + return Paths.get(localRepository(request)).resolve(group.replace('.', '/')) + .resolve(artifact).resolve(version).resolve(fileName).toString(); + } + + @Override + public String metadataPath(ExecutorRequest.Builder request, String gav, String repositoryId) + throws ExecutorException { + String[] parts = coordinates(gav, 1, 4); + String fileName = parts.length == 4 && !parts[3].isEmpty() ? parts[3] : "maven-metadata.xml"; + Path path = Paths.get(localRepository(request)); + if (!parts[0].isEmpty()) { + path = path.resolve(parts[0].replace('.', '/')); + } + if (parts.length > 1 && !parts[1].isEmpty()) { + path = path.resolve(parts[1]); + } + if (parts.length > 2 && !parts[2].isEmpty()) { + path = path.resolve(parts[2]); + } + if (repositoryId != null && !repositoryId.isEmpty() && "maven-metadata.xml".equals(fileName)) { + fileName = "maven-metadata-" + repositoryId + ".xml"; + } + return path.resolve(fileName).toString(); + } + + private static String[] coordinates(String value, int min, int max) { + Objects.requireNonNull(value, "coordinates"); + String[] parts = value.split(":", -1); + if (parts.length < min || parts.length > max) { + throw new IllegalArgumentException("Invalid Maven coordinates: " + value); + } + return parts; + } + + private static String property(ExecutorRequest request, String name) { + Optional> properties = request.jvmSystemProperties(); + if (properties.isPresent() && properties.get().containsKey(name)) { + return properties.get().get(name); + } + for (String argument : request.arguments()) { + String prefix = "-D" + name + "="; + if (argument.startsWith(prefix)) { + return argument.substring(prefix.length()); + } + } + return null; + } + + private static Path settingsFile(ExecutorRequest request) { + String[] arguments = request.arguments().toArray(new String[0]); + for (int i = 0; i < arguments.length; i++) { + if (("-s".equals(arguments[i]) || "--settings".equals(arguments[i])) && i + 1 < arguments.length) { + return request.cwd().resolve(arguments[i + 1]).normalize(); + } + } + return request.userHomeDirectory().resolve(".m2/settings.xml"); + } + + private static String localRepositoryFromSettings(Path settings) throws ExecutorException { + try (InputStream input = Files.newInputStream(settings)) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + Document document = factory.newDocumentBuilder().parse(input); + if (document.getElementsByTagName("localRepository").getLength() == 0) { + return null; + } + String value = document.getElementsByTagName("localRepository").item(0).getTextContent().trim(); + return value.isEmpty() ? null : value; + } catch (Exception e) { + throw new ExecutorException("Unable to read Maven settings: " + settings, e); + } + } + + private static Path expand(String value, ExecutorRequest request) { + return Paths.get(value.replace("${user.home}", request.userHomeDirectory().toString()) + .replace("${maven.multiModuleProjectDirectory}", request.cwd().toString())); + } +} diff --git a/maven-executor/src/test/java/org/apache/maven/executor/LocalRepositoryExecutorToolTest.java b/maven-executor/src/test/java/org/apache/maven/executor/LocalRepositoryExecutorToolTest.java new file mode 100644 index 0000000..2419f91 --- /dev/null +++ b/maven-executor/src/test/java/org/apache/maven/executor/LocalRepositoryExecutorToolTest.java @@ -0,0 +1,97 @@ +/* + * 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.executor; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.apache.maven.executor.support.LocalRepositoryExecutorTool; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class LocalRepositoryExecutorToolTest { + @TempDir + Path tempDir; + + @Test + void calculatesDefaultArtifactAndMetadataPaths() throws Exception { + Path home = tempDir.resolve("home"); + Path cwd = tempDir.resolve("project"); + ExecutorRequest.Builder request = ExecutorRequest.mavenBuilder() + .userHomeDirectory(home) + .cwd(cwd); + LocalRepositoryExecutorTool tool = new LocalRepositoryExecutorTool(noopExecutor()); + + assertEquals( + home.resolve(".m2/repository/org/example/library/1.2/library-1.2-tests.jar").toString(), + tool.artifactPath(request, "org.example:library:1.2:jar:tests", null)); + assertEquals( + home.resolve(".m2/repository/org/example/library/maven-metadata-central.xml").toString(), + tool.metadataPath(request, "org.example:library", "central")); + } + + @Test + void honoursConfiguredRepositoryAndSettings() throws Exception { + Path home = tempDir.resolve("home"); + Path cwd = tempDir.resolve("project"); + Files.createDirectories(cwd); + Path settings = cwd.resolve("settings.xml"); + Files.write(settings, Collections.singletonList( + "${user.home}/custom-repository")); + ExecutorRequest.Builder request = ExecutorRequest.mavenBuilder() + .userHomeDirectory(home) + .cwd(cwd) + .argument("--settings") + .argument("settings.xml"); + LocalRepositoryExecutorTool tool = new LocalRepositoryExecutorTool(noopExecutor()); + + assertEquals(home.resolve("custom-repository").toString(), tool.localRepository(request)); + } + + @Test + void explicitRepositoryPropertyTakesPrecedence() throws Exception { + Path repository = tempDir.resolve("repository"); + ExecutorRequest.Builder request = ExecutorRequest.mavenBuilder() + .userHomeDirectory(tempDir.resolve("home")) + .cwd(tempDir) + .argument("-Dmaven.repo.local=" + repository); + + assertEquals(repository.toString(), new LocalRepositoryExecutorTool(noopExecutor()).localRepository(request)); + } + + private static Executor noopExecutor() { + return new Executor() { + @Override + public ExecutorResult execute(ExecutorRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public String mavenVersion() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() {} + }; + } +}