diff --git a/maven-resolver/src/main/java/org/wildfly/channel/maven/RemoteArtifactVersionsFilter.java b/maven-resolver/src/main/java/org/wildfly/channel/maven/RemoteArtifactVersionsFilter.java new file mode 100644 index 00000000..246ac508 --- /dev/null +++ b/maven-resolver/src/main/java/org/wildfly/channel/maven/RemoteArtifactVersionsFilter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed 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.wildfly.channel.maven; + +import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.version.Version; +import org.jboss.logging.Logger; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +class RemoteArtifactVersionsFilter { + + private static final Logger LOG = Logger.getLogger(RemoteArtifactVersionsFilter.class); + + private final RepositorySystemSession session; + private final Artifact artifact; + private final List repositories; + private final Set remoteRepositoryIds; + private final VersionRangeResult versionRangeResult; + + RemoteArtifactVersionsFilter(RepositorySystemSession session, VersionRangeResult versionRangeResult) { + this.session = session; + this.artifact = versionRangeResult.getRequest().getArtifact(); + this.versionRangeResult = versionRangeResult; + this.repositories = versionRangeResult.getRequest().getRepositories(); + this.remoteRepositoryIds = repositories.stream().map(RemoteRepository::getId).collect(Collectors.toSet()); + } + + /** + * rejects versions available only in the local repository + * + * @param version + * @return + */ + boolean accept(Version version) { + if (repositories.isEmpty()) { + return false; + } + + // if the version was resolved from a remote repository, accept this version + if (versionRangeResult.getRepository(version) != null && remoteRepositoryIds.contains(versionRangeResult.getRepository(version).getId())) { + return true; + } + + /* + * If the version was resolved from a local repository, it might be because the local version masks the + * remote repository (see DefaultVersionRangeResolver#getVersions). + * + * During the resolution, the versions from remote repositories are cached in maven-metadata-*.xml. + * We are checking those to see if any of the remote repositories contain the same version as local. + */ + for (RemoteRepository repository : repositories) { + final DefaultMetadata artifactMetadata = new DefaultMetadata( + artifact.getGroupId(), + artifact.getArtifactId(), + "maven-metadata.xml", + Metadata.Nature.RELEASE); + final String pathForRemoteMetadata = session.getLocalRepositoryManager().getPathForRemoteMetadata(artifactMetadata, repository, null); + final File metadataFile = new File(session.getLocalRepository().getBasedir(), pathForRemoteMetadata); + + if (metadataFile.exists()) { + try { + final org.apache.maven.artifact.repository.metadata.Metadata metadata = new MetadataXpp3Reader().read(new FileReader(metadataFile)); + if (metadata.getVersioning().getVersions().contains(version.toString())) { + return true; + } + } catch (IOException | XmlPullParserException e) { + LOG.warn("Failed to parse version information in " + metadataFile + ", skipping.", e); + } + } + } + return false; + } +} diff --git a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java index 36ae37b1..cf47b257 100644 --- a/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java +++ b/maven-resolver/src/main/java/org/wildfly/channel/maven/VersionResolverFactory.java @@ -142,12 +142,8 @@ public Set getAllVersions(String groupId, String artifactId, String exte Artifact artifact = new DefaultArtifact(groupId, artifactId, classifier, extension, "[0,)"); VersionRangeRequest versionRangeRequest = new VersionRangeRequest(); versionRangeRequest.setArtifact(artifact); - final Set repos; if (repositories != null) { versionRangeRequest.setRepositories(repositories); - repos = new HashSet<>(repositories); - } else { - repos = null; } VersionRangeResult versionRangeResult = retryingResolver.attemptResolveMetadata(() -> { @@ -160,9 +156,19 @@ public Set getAllVersions(String groupId, String artifactId, String exte } }, attemptedRepositories()); - return versionRangeResult.getVersions().stream() - .filter(v -> repos != null && repos.contains(versionRangeResult.getRepository(v))) - .map(Version::toString).collect(Collectors.toSet()); + final RemoteArtifactVersionsFilter remoteOnlyFilter = new RemoteArtifactVersionsFilter(session, versionRangeResult); + final List remoteVersions = versionRangeResult.getVersions().stream() + .filter(remoteOnlyFilter::accept) + .collect(Collectors.toList()); + + if (!versionRangeResult.getVersions().isEmpty() && remoteVersions.isEmpty()) { + LOG.warnf("Error resolving artifact %s:%s versions: the only available version was resolved from local repository, discarding!", + artifact.getGroupId(), artifact.getArtifactId()); + } + + return remoteVersions.stream() + .map(Version::toString) + .collect(Collectors.toSet()); } @@ -180,7 +186,13 @@ public File resolveArtifact(String groupId, String artifactId, String extension, request.setRepositories(repositories); } - return retryingResolver.attemptResolve(()->List.of(system.resolveArtifact(session, request).getArtifact().getFile()), + return retryingResolver.attemptResolve(()->{ + final ArtifactResult artifactResult = system.resolveArtifact(session, request); + for (Exception exception : artifactResult.getExceptions()) { + LOG.info("Error resolving maven artifact: " + artifactResult.getRequest().getArtifact() + ": " + exception.getMessage(), exception); + } + return List.of(artifactResult.getArtifact().getFile()); + }, (ex)->singleton(new ArtifactCoordinate(groupId, artifactId, extension, classifier, version)), attemptedRepositories()).get(0); } diff --git a/maven-resolver/src/test/java/org/wildfly/channel/maven/RemoteArtifactVersionsFilterTest.java b/maven-resolver/src/test/java/org/wildfly/channel/maven/RemoteArtifactVersionsFilterTest.java new file mode 100644 index 00000000..9a87b1e6 --- /dev/null +++ b/maven-resolver/src/test/java/org/wildfly/channel/maven/RemoteArtifactVersionsFilterTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed 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.wildfly.channel.maven; + +import org.apache.maven.artifact.repository.metadata.Metadata; +import org.apache.maven.artifact.repository.metadata.Versioning; +import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.version.Version; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RemoteArtifactVersionsFilterTest { + + @TempDir + private Path tempDir; + + @Test + public void rejectIfNoRepositoriesAreUsed() { + final RepositorySystemSession session = mock(RepositorySystemSession.class);; + final VersionRangeResult result = new VersionRangeResult(mock(VersionRangeRequest.class)); + + final RemoteArtifactVersionsFilter filter = new RemoteArtifactVersionsFilter(session, result); + Assertions.assertFalse(filter.accept(mock(Version.class)), "A version requests without any remote repository should be rejected"); + } + + @Test + public void acceptIfTheVersionWasFoundInRemoteRepository() { + final RepositorySystemSession session = mock(RepositorySystemSession.class);; + final RemoteRepository repository = new RemoteRepository.Builder("test", "default", "http://foo.bar").build(); + final DefaultArtifact artifact = new DefaultArtifact("org.test", "test-one", "jar", "1.1.1"); + final VersionRangeResult result = new VersionRangeResult(new VersionRangeRequest(artifact, List.of(repository), null)); + result.setRepository(new TestVersion("1.1.1"), repository); + + final RemoteArtifactVersionsFilter filter = new RemoteArtifactVersionsFilter(session, result); + Assertions.assertTrue(filter.accept(new TestVersion("1.1.1")), "A version requests resolved from a remote repository should be accepted"); + } + + @Test + public void acceptIfTheVersionIsInRemoteCache() throws IOException { + final RepositorySystemSession session = mock(RepositorySystemSession.class);; + final LocalRepositoryManager lrm = mock(LocalRepositoryManager.class); + final LocalRepository lr = mock(LocalRepository.class); + final RemoteRepository repository = new RemoteRepository.Builder("test", "default", "http://foo.bar").build(); + final LocalRepository localRepository = new LocalRepository(tempDir.toFile()); + final String groupId = "org.test"; + final String artifactId = "test-one"; + final String version = "1.1.1"; + final DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId, "jar", version); + final VersionRangeResult result = new VersionRangeResult(new VersionRangeRequest(artifact, List.of(repository), null)); + result.setRepository(new TestVersion(version), localRepository); + + when(session.getLocalRepositoryManager()).thenReturn(lrm); + when(lrm.getPathForRemoteMetadata(any(), any(), any())).thenReturn(groupId.replace('.', File.separatorChar) + + File.separatorChar + artifactId + File.separatorChar + "maven-metadata-test.xml"); + when(session.getLocalRepository()).thenReturn(lr); + when(lr.getBasedir()).thenReturn(tempDir.toFile()); + + writeMavenCacheFile(groupId, artifactId, version); + + final RemoteArtifactVersionsFilter filter = new RemoteArtifactVersionsFilter(session, result); + Assertions.assertTrue(filter.accept(new TestVersion(version)), "A version requests with a matching version in cache file should be accepted"); + } + + @Test + public void rejectIfTheVersionIsNotInRemoteCache() throws IOException { + final RepositorySystemSession session = mock(RepositorySystemSession.class);; + final LocalRepositoryManager lrm = mock(LocalRepositoryManager.class); + final LocalRepository lr = mock(LocalRepository.class); + final RemoteRepository repository = new RemoteRepository.Builder("test", "default", "http://foo.bar").build(); + final LocalRepository localRepository = new LocalRepository(tempDir.toFile()); + final String groupId = "org.test"; + final String artifactId = "test-one"; + final String version = "1.1.1"; + final DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId, "jar", version); + final VersionRangeResult result = new VersionRangeResult(new VersionRangeRequest(artifact, List.of(repository), null)); + result.setRepository(new TestVersion(version), localRepository); + + when(session.getLocalRepositoryManager()).thenReturn(lrm); + when(lrm.getPathForRemoteMetadata(any(), any(), any())).thenReturn(groupId.replace('.', File.separatorChar) + + File.separatorChar + artifactId + File.separatorChar + "maven-metadata-test.xml"); + when(session.getLocalRepository()).thenReturn(lr); + when(lr.getBasedir()).thenReturn(tempDir.toFile()); + + writeMavenCacheFile(groupId, artifactId, "1.1.2"); + + final RemoteArtifactVersionsFilter filter = new RemoteArtifactVersionsFilter(session, result); + Assertions.assertFalse(filter.accept(new TestVersion(version)), "A version requests without matching version in cache should be rejected"); + } + + private void writeMavenCacheFile(String groupId, String artifactId, String version) throws IOException { + final Metadata metadata = new Metadata(); + final Versioning versioning = new Versioning(); + versioning.setVersions(List.of(version)); + metadata.setGroupId(groupId); + metadata.setArtifactId(artifactId); + metadata.setVersioning(versioning); + final Path manifestFile = tempDir.resolve(Path.of(groupId.replace('.', File.separatorChar), artifactId, "maven-metadata-test.xml")); + Files.createDirectories(manifestFile.getParent()); + new MetadataXpp3Writer().write(new FileWriter(manifestFile.toFile()), metadata); + } + + + static class TestVersion implements Version { + + private final String version; + + TestVersion(String version) { + this.version = version; + } + + @Override + public String toString() { + return this.version; + } + + @Override + public int compareTo(Version v) { + TestVersion other = (TestVersion) v; + return this.version.compareTo(other.version); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TestVersion that = (TestVersion) o; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(version); + } + } +} \ No newline at end of file diff --git a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java index 32e134f3..0793f9e5 100644 --- a/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java +++ b/maven-resolver/src/test/java/org/wildfly/channel/maven/VersionResolverFactoryTest.java @@ -81,6 +81,7 @@ public void testResolverGetAllVersions() throws VersionRangeResolutionException Version v111 = mock(Version.class); when(v111.toString()).thenReturn("1.1.1"); versionRangeResult.setVersions(asList(v100, v110, v111)); + versionRangeResult.getRequest().setRepositories(List.of(new RemoteRepository.Builder("test", "default", "file://test").build())); final Repository testRepository = new Repository("test", "file://test"); final ArtifactRepository testArtifactRepository = VersionResolverFactory.DEFAULT_REPOSITORY_MAPPER.apply(testRepository); for (Version v : versionRangeResult.getVersions()) versionRangeResult.setRepository(v, testArtifactRepository); @@ -221,6 +222,7 @@ public void testResolverResolveMetadataUsingGa() throws ArtifactResolutionExcept Version v111 = mock(Version.class); when(v111.toString()).thenReturn("1.1.1"); versionRangeResult.setVersions(asList(v100, v110, v111)); + versionRangeResult.getRequest().setRepositories(List.of(new RemoteRepository.Builder("test", "default", "file://test").build())); final Repository testRepository = new Repository("test", "file://test"); final ArtifactRepository testArtifactRepository = VersionResolverFactory.DEFAULT_REPOSITORY_MAPPER.apply(testRepository); for (Version v : versionRangeResult.getVersions()) versionRangeResult.setRepository(v, testArtifactRepository);