diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java index 73b351458d..2764766160 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java +++ b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java @@ -72,6 +72,7 @@ import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; import org.eclipse.aether.transform.FileTransformer; import org.eclipse.aether.transform.TransformException; import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.listener.ChainedRepositoryListener; import org.eclipse.aether.util.repository.AuthenticationBuilder; import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager; import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; @@ -104,6 +105,16 @@ public class DefaultRepositorySystemSessionFactory { */ private static final String MAVEN_REPO_LOCAL_TAIL_IGNORE_AVAILABILITY = "maven.repo.local.tail.ignoreAvailability"; + /** + * User property for reverse dependency tree. If enabled, Maven will record ".tracking" directory into local + * repository with "reverse dependency tree", essentially explaining WHY given artifact is present in local + * repository. + * Default: {@code false}, will not record anything. + * + * @since 3.9.0 + */ + private static final String MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE = "maven.repo.local.recordReverseTree"; + private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport"; private static final String MAVEN_RESOLVER_TRANSPORT_DEFAULT = "default"; @@ -348,6 +359,12 @@ public class DefaultRepositorySystemSessionFactory { session.setRepositoryListener(eventSpyDispatcher.chainListener(new LoggingRepositoryListener(logger))); + boolean recordReverseTree = ConfigUtils.getBoolean(session, false, MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE); + if (recordReverseTree) { + session.setRepositoryListener(new ChainedRepositoryListener( + session.getRepositoryListener(), new ReverseTreeRepositoryListener())); + } + mavenRepositorySystem.injectMirror(request.getRemoteRepositories(), request.getMirrors()); mavenRepositorySystem.injectProxy(session, request.getRemoteRepositories()); mavenRepositorySystem.injectAuthentication(session, request.getRemoteRepositories()); diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListener.java b/maven-core/src/main/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListener.java new file mode 100644 index 0000000000..773d2e16fd --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListener.java @@ -0,0 +1,136 @@ +/* + * 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.internal.aether; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.ListIterator; +import java.util.Objects; +import org.eclipse.aether.AbstractRepositoryListener; +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RequestTrace; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectStepData; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; + +/** + * A class building reverse tree using {@link CollectStepData} trace data provided in {@link RepositoryEvent} + * events fired during collection. + * + * @since 3.9.0 + */ +class ReverseTreeRepositoryListener extends AbstractRepositoryListener { + @Override + public void artifactResolved(RepositoryEvent event) { + requireNonNull(event, "event cannot be null"); + + if (!isLocalRepositoryArtifact(event.getSession(), event.getArtifact())) { + return; + } + + CollectStepData collectStepTrace = lookupCollectStepData(event.getTrace()); + if (collectStepTrace == null) { + return; + } + + Artifact resolvedArtifact = event.getArtifact(); + Artifact nodeArtifact = collectStepTrace.getNode().getArtifact(); + + if (isInScope(resolvedArtifact, nodeArtifact)) { + Dependency node = collectStepTrace.getNode(); + ArrayList trackingData = new ArrayList<>(); + trackingData.add(node + " (" + collectStepTrace.getContext() + ")"); + String indent = ""; + ListIterator iter = collectStepTrace + .getPath() + .listIterator(collectStepTrace.getPath().size()); + while (iter.hasPrevious()) { + DependencyNode curr = iter.previous(); + indent += " "; + trackingData.add(indent + curr + " (" + collectStepTrace.getContext() + ")"); + } + try { + Path trackingDir = + resolvedArtifact.getFile().getParentFile().toPath().resolve(".tracking"); + Files.createDirectories(trackingDir); + Path trackingFile = trackingDir.resolve(collectStepTrace + .getPath() + .get(0) + .getArtifact() + .toString() + .replace(":", "_")); + Files.write(trackingFile, trackingData, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + /** + * Returns {@code true} if passed in artifact is originating from local repository. In other words, we want + * to process and store tracking information ONLY into local repository, not to any other place. This method + * filters out currently built artifacts, as events are fired for them as well, but their resolved artifact + * file would point to checked out source-tree, not the local repository. + *

+ * Visible for testing. + */ + static boolean isLocalRepositoryArtifact(RepositorySystemSession session, Artifact artifact) { + return artifact.getFile() + .getPath() + .startsWith(session.getLocalRepository().getBasedir().getPath()); + } + + /** + * Unravels trace tree (going upwards from current node), looking for {@link CollectStepData} trace data. + * This method may return {@code null} if no collect step data found in passed trace data or it's parents. + *

+ * Visible for testing. + */ + static CollectStepData lookupCollectStepData(RequestTrace trace) { + CollectStepData collectStepTrace = null; + while (trace != null) { + if (trace.getData() instanceof CollectStepData) { + collectStepTrace = (CollectStepData) trace.getData(); + break; + } + trace = trace.getParent(); + } + return collectStepTrace; + } + + /** + * The event "artifact resolved" if fired WHENEVER an artifact is resolved, BUT it happens also when an artifact + * descriptor (model, the POM) is being built, and parent (and parent of parent...) is being asked for. Hence, this + * method "filters" out in WHICH artifact are we interested in, but it intentionally neglects extension as + * ArtifactDescriptorReader modifies extension to "pom" during collect. So all we have to rely on is GAV only. + */ + static boolean isInScope(Artifact artifact, Artifact nodeArtifact) { + return Objects.equals(artifact.getGroupId(), nodeArtifact.getGroupId()) + && Objects.equals(artifact.getArtifactId(), nodeArtifact.getArtifactId()) + && Objects.equals(artifact.getVersion(), nodeArtifact.getVersion()); + } +} diff --git a/maven-core/src/test/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListenerTest.java b/maven-core/src/test/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListenerTest.java new file mode 100644 index 0000000000..f7e68beba6 --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListenerTest.java @@ -0,0 +1,87 @@ +/* + * 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.internal.aether; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RequestTrace; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectStepData; +import org.eclipse.aether.repository.LocalRepository; +import org.junit.jupiter.api.Test; + +/** + * UT for {@link ReverseTreeRepositoryListener}. + */ +public class ReverseTreeRepositoryListenerTest { + @Test + public void isLocalRepositoryArtifactTest() { + File baseDir = new File("local/repository"); + LocalRepository localRepository = new LocalRepository(baseDir); + RepositorySystemSession session = mock(RepositorySystemSession.class); + when(session.getLocalRepository()).thenReturn(localRepository); + + Artifact localRepositoryArtifact = mock(Artifact.class); + when(localRepositoryArtifact.getFile()).thenReturn(new File(baseDir, "some/path/within")); + + Artifact nonLocalReposioryArtifact = mock(Artifact.class); + when(nonLocalReposioryArtifact.getFile()).thenReturn(new File("something/completely/different")); + + assertThat( + ReverseTreeRepositoryListener.isLocalRepositoryArtifact(session, localRepositoryArtifact), + equalTo(true)); + assertThat( + ReverseTreeRepositoryListener.isLocalRepositoryArtifact(session, nonLocalReposioryArtifact), + equalTo(false)); + } + + @Test + public void lookupCollectStepDataTest() { + RequestTrace doesNotHaveIt = + RequestTrace.newChild(null, "foo").newChild("bar").newChild("baz"); + assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(doesNotHaveIt), nullValue()); + + final CollectStepData data = mock(CollectStepData.class); + + RequestTrace haveItFirst = RequestTrace.newChild(null, data) + .newChild("foo") + .newChild("bar") + .newChild("baz"); + assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveItFirst), sameInstance(data)); + + RequestTrace haveItLast = RequestTrace.newChild(null, "foo") + .newChild("bar") + .newChild("baz") + .newChild(data); + assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveItLast), sameInstance(data)); + + RequestTrace haveIt = RequestTrace.newChild(null, "foo") + .newChild("bar") + .newChild(data) + .newChild("baz"); + assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveIt), sameInstance(data)); + } +}