diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java index 44ae964b0..6c586c572 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java @@ -31,19 +31,25 @@ * the repository. */ public final class LocalRepository implements ArtifactRepository { + public static final String ID = "local"; private final Path basePath; private final String type; + private final int hashCode; + /** * Creates a new local repository with the specified base directory and unknown type. * * @param basedir The base directory of the repository, may be {@code null}. + * @deprecated Use {@link LocalRepository(Path)} instead. */ + @Deprecated public LocalRepository(String basedir) { this.basePath = Paths.get(RepositoryUriUtils.toUri(basedir)).toAbsolutePath(); this.type = ""; + this.hashCode = Objects.hash(this.basePath, this.type); } /** @@ -99,6 +105,7 @@ public LocalRepository(File basedir, String type) { public LocalRepository(Path basePath, String type) { this.basePath = basePath; this.type = (type != null) ? type : ""; + this.hashCode = Objects.hash(this.basePath, this.type); } @Override @@ -108,7 +115,7 @@ public String getContentType() { @Override public String getId() { - return "local"; + return ID; } /** @@ -153,13 +160,6 @@ public boolean equals(Object obj) { @Override public int hashCode() { - int hash = 17; - hash = hash * 31 + hash(basePath); - hash = hash * 31 + hash(type); - return hash; - } - - private static int hash(Object obj) { - return obj != null ? obj.hashCode() : 0; + return hashCode; } } diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java index e128af209..9b0b44021 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java @@ -18,6 +18,7 @@ */ package org.eclipse.aether.repository; +import java.util.Objects; import java.util.UUID; /** @@ -27,11 +28,14 @@ * the contained artifacts is handled by a {@link WorkspaceReader}. */ public final class WorkspaceRepository implements ArtifactRepository { + public static final String ID = "workspace"; private final String type; private final Object key; + private final int hashCode; + /** * Creates a new workspace repository of type {@code "workspace"} and a random key. */ @@ -58,6 +62,7 @@ public WorkspaceRepository(String type) { public WorkspaceRepository(String type, Object key) { this.type = (type != null) ? type : ""; this.key = (key != null) ? key : UUID.randomUUID().toString().replace("-", ""); + this.hashCode = Objects.hash(type, key); } public String getContentType() { @@ -65,7 +70,7 @@ public String getContentType() { } public String getId() { - return "workspace"; + return ID; } /** @@ -99,9 +104,6 @@ public boolean equals(Object obj) { @Override public int hashCode() { - int hash = 17; - hash = hash * 31 + getKey().hashCode(); - hash = hash * 31 + getContentType().hashCode(); - return hash; + return hashCode; } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java index b44f4011b..8b5e308f7 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java @@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import org.apache.maven.resolver.examples.util.Booter; import org.eclipse.aether.RepositorySystem; @@ -55,7 +56,7 @@ public class Resolver { private final LocalRepository localRepository; - public Resolver(String[] args, String remoteRepository, String localRepository) { + public Resolver(String[] args, String remoteRepository, Path localRepository) { this.args = args; this.remoteRepository = remoteRepository; this.repositorySystem = Booter.newRepositorySystem(Booter.selectFactory(args)); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java index 7c0e70a46..eb8193bbd 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java @@ -19,6 +19,7 @@ package org.apache.maven.resolver.examples.resolver; import java.io.File; +import java.nio.file.Paths; import java.util.List; import org.eclipse.aether.artifact.Artifact; @@ -37,7 +38,8 @@ public static void main(String[] args) throws Exception { System.out.println("------------------------------------------------------------"); System.out.println(ResolverDemo.class.getSimpleName()); - Resolver resolver = new Resolver(args, "https://repo.maven.apache.org/maven2/", "target/resolver-demo-repo"); + Resolver resolver = + new Resolver(args, "https://repo.maven.apache.org/maven2/", Paths.get("target/resolver-demo-repo")); ResolverResult result = resolver.resolve("junit", "junit", "4.13.2"); System.out.println("Result:"); @@ -47,8 +49,8 @@ public static void main(String[] args) throws Exception { } public void resolve(String[] args) throws DependencyResolutionException { - Resolver resolver = - new Resolver(args, "http://localhost:8081/nexus/content/groups/public", "target/aether-repo"); + Resolver resolver = new Resolver( + args, "http://localhost:8081/nexus/content/groups/public", Paths.get("target/aether-repo")); ResolverResult result = resolver.resolve("com.mycompany.app", "super-app", "1.0"); @@ -66,8 +68,8 @@ public void resolve(String[] args) throws DependencyResolutionException { } public void installAndDeploy(String[] args) throws InstallationException, DeploymentException { - Resolver resolver = - new Resolver(args, "http://localhost:8081/nexus/content/groups/public", "target/aether-repo"); + Resolver resolver = new Resolver( + args, "http://localhost:8081/nexus/content/groups/public", Paths.get("target/aether-repo")); Artifact artifact = new DefaultArtifact("com.mycompany.super", "super-core", "jar", "0.1-SNAPSHOT"); artifact = artifact.setFile(new File("jar-from-whatever-process.jar")); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java index 2a18ce049..74c488dda 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java @@ -21,11 +21,12 @@ import javax.inject.Named; import javax.inject.Singleton; -import java.util.function.Function; +import java.util.function.BiFunction; import org.eclipse.aether.RepositorySystemSession; -import org.eclipse.aether.repository.ArtifactRepository; -import org.eclipse.aether.util.repository.RepositoryIdHelper; +import org.eclipse.aether.repository.RemoteRepository; + +import static org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManagerFactory.repositoryKeyFunction; /** * Default local path prefix composer factory: it fully reuses {@link LocalPathPrefixComposerFactorySupport} class @@ -48,7 +49,7 @@ public LocalPathPrefixComposer createComposer(RepositorySystemSession session) { isSplitRemoteRepositoryLast(session), getReleasesPrefix(session), getSnapshotsPrefix(session), - RepositoryIdHelper.cachedIdToPathSegment(session)); + repositoryKeyFunction(session)); } /** @@ -66,7 +67,7 @@ private DefaultLocalPathPrefixComposer( boolean splitRemoteRepositoryLast, String releasesPrefix, String snapshotsPrefix, - Function idToPathSegmentFunction) { + BiFunction repositoryKeyFunction) { super( split, localPrefix, @@ -77,7 +78,7 @@ private DefaultLocalPathPrefixComposer( splitRemoteRepositoryLast, releasesPrefix, snapshotsPrefix, - idToPathSegmentFunction); + repositoryKeyFunction); } } } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java index 3ad72aade..e2521962e 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java @@ -26,8 +26,12 @@ import java.util.Arrays; import java.util.List; import java.util.ListIterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; +import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.RepositoryCache; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.impl.RemoteRepositoryManager; @@ -40,6 +44,8 @@ import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +56,51 @@ @Singleton @Named public class DefaultRemoteRepositoryManager implements RemoteRepositoryManager { + private static final String CONFIG_PROPS_PREFIX = + ConfigurationProperties.PREFIX_AETHER + "remoteRepositoryManager."; + + /** + * Experimental: Configuration for "repository key" function. + * Note: repository key functions other than "nid" produce repository keys will be way different + * that those produced with previous versions or without this option enabled. Manager uses this key to + * detect "same" remote repositories, and in case of mirrors, to merge them. + * + * @since 2.0.14 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.String} + * @configurationDefaultValue {@link #DEFAULT_REPOSITORY_KEY_FUNCTION} + */ + public static final String CONFIG_PROP_REPOSITORY_KEY_FUNCTION = CONFIG_PROPS_PREFIX + "repositoryKeyFunction"; + + public static final String DEFAULT_REPOSITORY_KEY_FUNCTION = "nid"; + + /** + * Method that based on configuration returns the "repository key function". Used by {@link EnhancedLocalRepositoryManagerFactory} + * and {@link LocalPathPrefixComposerFactory}. + * + * @since 2.0.14 + */ + @SuppressWarnings("unchecked") + private static BiFunction repositoryKeyFunction(RepositorySystemSession session) { + final RepositoryIdHelper.RepositoryKeyFunction repositoryKeyFunction = + RepositoryIdHelper.getRepositoryKeyFunction(ConfigUtils.getString( + session, DEFAULT_REPOSITORY_KEY_FUNCTION, CONFIG_PROP_REPOSITORY_KEY_FUNCTION)); + if (session.getCache() != null) { + // both are expensive methods; cache it in session (repo -> context -> ID) + return (repository, context) -> ((ConcurrentMap>) + session.getCache() + .computeIfAbsent( + session, + EnhancedLocalRepositoryManagerFactory.class.getName() + + ".repositoryKeyFunction", + ConcurrentHashMap::new)) + .computeIfAbsent(repository, k1 -> new ConcurrentHashMap<>()) + .computeIfAbsent( + context == null ? "" : context, k2 -> repositoryKeyFunction.apply(repository, context)); + } else { + return repositoryKeyFunction; + } + } private static final class LoggedMirror { @@ -102,6 +153,7 @@ public List aggregateRepositories( return dominantRepositories; } + BiFunction repositoryKeyFunction = repositoryKeyFunction(session); MirrorSelector mirrorSelector = session.getMirrorSelector(); AuthenticationSelector authSelector = session.getAuthenticationSelector(); ProxySelector proxySelector = session.getProxySelector(); @@ -121,15 +173,16 @@ public List aggregateRepositories( } } - String key = getKey(repository); + String key = repositoryKeyFunction.apply(repository, null); for (ListIterator it = result.listIterator(); it.hasNext(); ) { RemoteRepository dominantRepository = it.next(); - if (key.equals(getKey(dominantRepository))) { + if (key.equals(repositoryKeyFunction.apply(dominantRepository, null))) { if (!dominantRepository.getMirroredRepositories().isEmpty() && !repository.getMirroredRepositories().isEmpty()) { - RemoteRepository mergedRepository = mergeMirrors(session, dominantRepository, repository); + RemoteRepository mergedRepository = + mergeMirrors(session, repositoryKeyFunction, dominantRepository, repository); if (mergedRepository != dominantRepository) { it.set(mergedRepository); } @@ -188,21 +241,20 @@ private void logMirror(RepositorySystemSession session, RemoteRepository origina original.getUrl()); } - private String getKey(RemoteRepository repository) { - return repository.getId(); - } - private RemoteRepository mergeMirrors( - RepositorySystemSession session, RemoteRepository dominant, RemoteRepository recessive) { + RepositorySystemSession session, + BiFunction repositoryKeyFunction, + RemoteRepository dominant, + RemoteRepository recessive) { RemoteRepository.Builder merged = null; RepositoryPolicy releases = null, snapshots = null; next: for (RemoteRepository rec : recessive.getMirroredRepositories()) { - String recKey = getKey(rec); + String recKey = repositoryKeyFunction.apply(rec, null); for (RemoteRepository dom : dominant.getMirroredRepositories()) { - if (recKey.equals(getKey(dom))) { + if (recKey.equals(repositoryKeyFunction.apply(dom, null))) { continue next; } } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java index e55150f3f..c1b80999f 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java @@ -27,12 +27,11 @@ import java.util.Map; import java.util.Objects; import java.util.Properties; -import java.util.function.Function; +import java.util.function.BiFunction; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.metadata.Metadata; -import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.LocalArtifactRegistration; import org.eclipse.aether.repository.LocalArtifactRequest; import org.eclipse.aether.repository.LocalArtifactResult; @@ -72,11 +71,11 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager { EnhancedLocalRepositoryManager( Path basedir, LocalPathComposer localPathComposer, - Function idToPathSegmentFunction, + BiFunction repositoryKeyFunction, String trackingFilename, TrackingFileManager trackingFileManager, LocalPathPrefixComposer localPathPrefixComposer) { - super(basedir, "enhanced", localPathComposer, idToPathSegmentFunction); + super(basedir, "enhanced", localPathComposer, repositoryKeyFunction); this.trackingFilename = requireNonNull(trackingFilename); this.trackingFileManager = requireNonNull(trackingFileManager); this.localPathPrefixComposer = requireNonNull(localPathPrefixComposer); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java index b00f1dff9..92f81ba0c 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java @@ -22,11 +22,16 @@ import javax.inject.Named; import javax.inject.Singleton; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; + import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; import org.eclipse.aether.repository.NoLocalRepositoryManagerException; +import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; import org.eclipse.aether.util.ConfigUtils; import org.eclipse.aether.util.repository.RepositoryIdHelper; @@ -58,6 +63,21 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan public static final String DEFAULT_TRACKING_FILENAME = "_remote.repositories"; + /** + * Experimental: Configuration for "repository key" function. + * Note: repository key functions other than "simple" produce repository keys will be way different + * that those produced with previous versions or without this option enabled. Ideally, you may want to + * use empty local repository to populate with new repository key contained metadata. + * + * @since 2.0.14 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.String} + * @configurationDefaultValue {@link #DEFAULT_REPOSITORY_KEY_FUNCTION} + */ + public static final String CONFIG_PROP_REPOSITORY_KEY_FUNCTION = CONFIG_PROPS_PREFIX + "repositoryKeyFunction"; + + public static final String DEFAULT_REPOSITORY_KEY_FUNCTION = "simple"; + private float priority = 10.0f; private final LocalPathComposer localPathComposer; @@ -66,6 +86,34 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan private final LocalPathPrefixComposerFactory localPathPrefixComposerFactory; + /** + * Method that based on configuration returns the "repository key function". Used by {@link EnhancedLocalRepositoryManagerFactory} + * and {@link LocalPathPrefixComposerFactory}. + * + * @since 2.0.14 + */ + @SuppressWarnings("unchecked") + public static BiFunction repositoryKeyFunction(RepositorySystemSession session) { + final RepositoryIdHelper.RepositoryKeyFunction repositoryKeyFunction = + RepositoryIdHelper.getRepositoryKeyFunction(ConfigUtils.getString( + session, DEFAULT_REPOSITORY_KEY_FUNCTION, CONFIG_PROP_REPOSITORY_KEY_FUNCTION)); + if (session.getCache() != null) { + // both are expensive methods; cache it in session (repo -> context -> ID) + return (repository, context) -> ((ConcurrentMap>) + session.getCache() + .computeIfAbsent( + session, + EnhancedLocalRepositoryManagerFactory.class.getName() + + ".repositoryKeyFunction", + ConcurrentHashMap::new)) + .computeIfAbsent(repository, k1 -> new ConcurrentHashMap<>()) + .computeIfAbsent( + context == null ? "" : context, k2 -> repositoryKeyFunction.apply(repository, context)); + } else { + return repositoryKeyFunction; + } + } + @Inject public EnhancedLocalRepositoryManagerFactory( final LocalPathComposer localPathComposer, @@ -94,7 +142,7 @@ public LocalRepositoryManager newInstance(RepositorySystemSession session, Local return new EnhancedLocalRepositoryManager( repository.getBasePath(), localPathComposer, - RepositoryIdHelper.cachedIdToPathSegment(session), + repositoryKeyFunction(session), trackingFilename, trackingFileManager, localPathPrefixComposerFactory.createComposer(session)); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java index b86223b53..f26c329aa 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java @@ -18,12 +18,11 @@ */ package org.eclipse.aether.internal.impl; -import java.util.function.Function; +import java.util.function.BiFunction; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.metadata.Metadata; -import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.util.ConfigUtils; @@ -244,7 +243,7 @@ protected abstract static class LocalPathPrefixComposerSupport implements LocalP protected final String snapshotsPrefix; - protected final Function idToPathSegmentFunction; + protected final BiFunction repositoryKeyFunction; protected LocalPathPrefixComposerSupport( boolean split, @@ -256,7 +255,7 @@ protected LocalPathPrefixComposerSupport( boolean splitRemoteRepositoryLast, String releasesPrefix, String snapshotsPrefix, - Function idToPathSegmentFunction) { + BiFunction repositoryKeyFunction) { this.split = split; this.localPrefix = localPrefix; this.splitLocal = splitLocal; @@ -266,7 +265,7 @@ protected LocalPathPrefixComposerSupport( this.splitRemoteRepositoryLast = splitRemoteRepositoryLast; this.releasesPrefix = releasesPrefix; this.snapshotsPrefix = snapshotsPrefix; - this.idToPathSegmentFunction = idToPathSegmentFunction; + this.repositoryKeyFunction = repositoryKeyFunction; } @Override @@ -288,13 +287,13 @@ public String getPathPrefixForRemoteArtifact(Artifact artifact, RemoteRepository } String result = remotePrefix; if (!splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + idToPathSegmentFunction.apply(repository); + result += "/" + repositoryKeyFunction.apply(repository, null); } if (splitRemote) { result += "/" + (artifact.isSnapshot() ? snapshotsPrefix : releasesPrefix); } if (splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + idToPathSegmentFunction.apply(repository); + result += "/" + repositoryKeyFunction.apply(repository, null); } return result; } @@ -318,13 +317,13 @@ public String getPathPrefixForRemoteMetadata(Metadata metadata, RemoteRepository } String result = remotePrefix; if (!splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + idToPathSegmentFunction.apply(repository); + result += "/" + repositoryKeyFunction.apply(repository, null); } if (splitRemote) { result += "/" + (isSnapshot(metadata) ? snapshotsPrefix : releasesPrefix); } if (splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + idToPathSegmentFunction.apply(repository); + result += "/" + repositoryKeyFunction.apply(repository, null); } return result; } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java index ea751d41a..f1da03db9 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java @@ -21,14 +21,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.function.Function; +import java.util.function.BiFunction; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.metadata.Metadata; -import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.LocalArtifactRegistration; import org.eclipse.aether.repository.LocalArtifactRequest; import org.eclipse.aether.repository.LocalArtifactResult; @@ -38,7 +35,6 @@ import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.util.StringDigestUtil; import static java.util.Objects.requireNonNull; @@ -51,17 +47,17 @@ class SimpleLocalRepositoryManager implements LocalRepositoryManager { private final LocalPathComposer localPathComposer; - private final Function idToPathSegmentFunction; + private final BiFunction repositoryKeyFunction; SimpleLocalRepositoryManager( Path basePath, String type, LocalPathComposer localPathComposer, - Function idToPathSegmentFunction) { + BiFunction repositoryKeyFunction) { requireNonNull(basePath, "base directory cannot be null"); repository = new LocalRepository(basePath.toAbsolutePath(), type); this.localPathComposer = requireNonNull(localPathComposer); - this.idToPathSegmentFunction = requireNonNull(idToPathSegmentFunction); + this.repositoryKeyFunction = requireNonNull(repositoryKeyFunction); } @Override @@ -85,7 +81,7 @@ public String getPathForRemoteArtifact(Artifact artifact, RemoteRepository repos @Override public String getPathForLocalMetadata(Metadata metadata) { requireNonNull(metadata, "metadata cannot be null"); - return localPathComposer.getPathForMetadata(metadata, "local"); + return localPathComposer.getPathForMetadata(metadata, LocalRepository.ID); } @Override @@ -101,35 +97,7 @@ public String getPathForRemoteMetadata(Metadata metadata, RemoteRepository repos * of the remote repository (as it may change). */ protected String getRepositoryKey(RemoteRepository repository, String context) { - String key; - - if (repository.isRepositoryManager()) { - // repository serves dynamic contents, take request parameters into account for key - - StringBuilder buffer = new StringBuilder(128); - - buffer.append(idToPathSegmentFunction.apply(repository)); - - buffer.append('-'); - - SortedSet subKeys = new TreeSet<>(); - for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) { - subKeys.add(mirroredRepo.getId()); - } - - StringDigestUtil sha1 = StringDigestUtil.sha1(); - sha1.update(context); - for (String subKey : subKeys) { - sha1.update(subKey); - } - buffer.append(sha1.digest()); - - key = buffer.toString(); - } else { - key = idToPathSegmentFunction.apply(repository); - } - - return key; + return repositoryKeyFunction.apply(repository, context); } @Override diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java index c1e4ccd21..a5975138a 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java @@ -22,10 +22,15 @@ import javax.inject.Named; import javax.inject.Singleton; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; + import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; import org.eclipse.aether.repository.NoLocalRepositoryManagerException; +import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; import org.eclipse.aether.util.repository.RepositoryIdHelper; @@ -54,6 +59,7 @@ public SimpleLocalRepositoryManagerFactory(final LocalPathComposer localPathComp this.localPathComposer = requireNonNull(localPathComposer); } + @SuppressWarnings("unchecked") @Override public LocalRepositoryManager newInstance(RepositorySystemSession session, LocalRepository repository) throws NoLocalRepositoryManagerException { @@ -61,11 +67,21 @@ public LocalRepositoryManager newInstance(RepositorySystemSession session, Local requireNonNull(repository, "repository cannot be null"); if ("".equals(repository.getContentType()) || "simple".equals(repository.getContentType())) { + BiFunction repositoryKeyFunction = + RepositoryIdHelper::simpleRepositoryKey; + if (session.getCache() != null) { + repositoryKeyFunction = (r, c) -> ((ConcurrentMap>) + session.getCache() + .computeIfAbsent( + session, + EnhancedLocalRepositoryManagerFactory.class.getName() + + ".repositoryKeyFunction", + ConcurrentHashMap::new)) + .computeIfAbsent(r, k1 -> new ConcurrentHashMap<>()) + .computeIfAbsent(c == null ? "" : c, k2 -> RepositoryIdHelper.simpleRepositoryKey(r, c)); + } return new SimpleLocalRepositoryManager( - repository.getBasePath(), - "simple", - localPathComposer, - RepositoryIdHelper.cachedIdToPathSegment(session)); + repository.getBasePath(), "simple", localPathComposer, repositoryKeyFunction); } else { throw new NoLocalRepositoryManagerException(repository); } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java index 87cb48ae1..7b5d6f203 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java @@ -22,6 +22,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -31,6 +33,7 @@ import org.eclipse.aether.impl.OfflineController; import org.eclipse.aether.installation.InstallRequest; import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ResolutionErrorPolicy; import org.eclipse.aether.resolution.ResolutionErrorPolicyRequest; @@ -39,6 +42,7 @@ import org.eclipse.aether.spi.artifact.generator.ArtifactGenerator; import org.eclipse.aether.spi.artifact.generator.ArtifactGeneratorFactory; import org.eclipse.aether.transfer.RepositoryOfflineException; +import org.eclipse.aether.util.repository.RepositoryIdHelper; /** * Internal utility methods. @@ -207,4 +211,19 @@ public static void checkOffline( offlineController.checkOffline(session, repository); } } + + /** + * Shared and cached {@link RepositoryIdHelper#idToPathSegment(ArtifactRepository)} method, + */ + @SuppressWarnings("unchecked") + public static String cachedIdToPathSegment(RepositorySystemSession session, ArtifactRepository artifactRepository) { + if (session.getCache() != null) { + return ((ConcurrentMap) session.getCache() + .computeIfAbsent( + session, Utils.class.getName() + ".cachedIdToPathSegment", ConcurrentHashMap::new)) + .computeIfAbsent(artifactRepository, RepositoryIdHelper::idToPathSegment); + } else { + return RepositoryIdHelper.idToPathSegment(artifactRepository); + } + } } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java index fc417884c..5657a585b 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java @@ -53,7 +53,7 @@ * * @since 1.9.0 */ -abstract class FileTrustedChecksumsSourceSupport implements TrustedChecksumsSource { +public abstract class FileTrustedChecksumsSourceSupport implements TrustedChecksumsSource { protected static final String CONFIG_PROPS_PREFIX = ConfigurationProperties.PREFIX_AETHER + "trustedChecksumsSource."; diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java index 1d6af287b..6162b6c95 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java @@ -34,11 +34,11 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.internal.impl.LocalPathComposer; +import org.eclipse.aether.internal.impl.Utils; import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory; import org.eclipse.aether.spi.io.ChecksumProcessor; import org.eclipse.aether.util.ConfigUtils; -import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -134,7 +134,7 @@ protected Map doGetTrustedArtifactChecksums( Path checksumPath = basedir.resolve(calculateArtifactPath( originAware, artifact, - RepositoryIdHelper.cachedIdToPathSegment(session).apply(artifactRepository), + Utils.cachedIdToPathSegment(session, artifactRepository), checksumAlgorithmFactory)); if (!Files.isRegularFile(checksumPath)) { @@ -167,7 +167,7 @@ protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession ses return new SparseDirectoryWriter( getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, true), isOriginAware(session), - RepositoryIdHelper.cachedIdToPathSegment(session)); + r -> Utils.cachedIdToPathSegment(session, r)); } private String calculateArtifactPath( diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java index 85c9be19f..b6827c5c9 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java @@ -43,11 +43,11 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.impl.RepositorySystemLifecycle; import org.eclipse.aether.internal.impl.LocalPathComposer; +import org.eclipse.aether.internal.impl.Utils; import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory; import org.eclipse.aether.spi.io.PathProcessor; import org.eclipse.aether.util.ConfigUtils; -import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -177,7 +177,7 @@ protected Map doGetTrustedArtifactChecksums( Path summaryFile = summaryFile( basedir, originAware, - RepositoryIdHelper.cachedIdToPathSegment(session).apply(artifactRepository), + Utils.cachedIdToPathSegment(session, artifactRepository), checksumAlgorithmFactory.getFileExtension()); ConcurrentHashMap algorithmChecksums = checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile)); @@ -199,7 +199,7 @@ protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession ses checksums, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, true), isOriginAware(session), - RepositoryIdHelper.cachedIdToPathSegment(session)); + r -> Utils.cachedIdToPathSegment(session, r)); } /** diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java index 3d6643876..dd4699331 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java @@ -42,6 +42,7 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.impl.RepositorySystemLifecycle; +import org.eclipse.aether.internal.impl.Utils; import org.eclipse.aether.internal.impl.filter.ruletree.GroupTree; import org.eclipse.aether.metadata.Metadata; import org.eclipse.aether.repository.RemoteRepository; @@ -50,7 +51,6 @@ import org.eclipse.aether.spi.io.PathProcessor; import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor; import org.eclipse.aether.util.ConfigUtils; -import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -249,7 +249,7 @@ private Path ruleFile(RepositorySystemSession session, RemoteRepository remoteRe return ruleFiles(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> getBasedir( session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false) .resolve(GROUP_ID_FILE_PREFIX - + RepositoryIdHelper.cachedIdToPathSegment(session).apply(remoteRepository) + + Utils.cachedIdToPathSegment(session, remoteRepository) + GROUP_ID_FILE_SUFFIX)); } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index 9a4b2c301..117e621c3 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -36,6 +36,7 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.impl.MetadataResolver; import org.eclipse.aether.impl.RemoteRepositoryManager; +import org.eclipse.aether.internal.impl.Utils; import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource; import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree; import org.eclipse.aether.metadata.DefaultMetadata; @@ -49,7 +50,6 @@ import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider; import org.eclipse.aether.transfer.NoRepositoryLayoutException; import org.eclipse.aether.util.ConfigUtils; -import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -318,9 +318,8 @@ private PrefixTree loadPrefixTree( private Path resolvePrefixesFromLocalConfiguration( RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) { - Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX - + RepositoryIdHelper.cachedIdToPathSegment(session).apply(remoteRepository) - + PREFIXES_FILE_SUFFIX); + Path filePath = baseDir.resolve( + PREFIXES_FILE_PREFIX + Utils.cachedIdToPathSegment(session, remoteRepository) + PREFIXES_FILE_SUFFIX); if (Files.isReadable(filePath)) { return filePath; } else { diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java index 7d8a296cc..25c6af747 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java @@ -33,13 +33,13 @@ import org.eclipse.aether.metadata.DefaultMetadata; import org.eclipse.aether.metadata.Metadata; import org.eclipse.aether.metadata.Metadata.Nature; -import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.LocalArtifactRegistration; import org.eclipse.aether.repository.LocalArtifactRequest; import org.eclipse.aether.repository.LocalArtifactResult; import org.eclipse.aether.repository.LocalMetadataRequest; import org.eclipse.aether.repository.LocalMetadataResult; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,7 +108,7 @@ protected EnhancedLocalRepositoryManager getManager() { return new EnhancedLocalRepositoryManager( basedir.toPath(), new DefaultLocalPathComposer(), - ArtifactRepository::getId, + RepositoryIdHelper::simpleRepositoryKey, "_remote.repositories", trackingFileManager, new DefaultLocalPathPrefixComposerFactory().createComposer(session)); diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java index 4a78ea302..dbf37e80c 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java @@ -20,8 +20,8 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; -import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -34,7 +34,7 @@ protected EnhancedLocalRepositoryManager getManager() { return new EnhancedLocalRepositoryManager( basedir.toPath(), new DefaultLocalPathComposer(), - ArtifactRepository::getId, + RepositoryIdHelper::simpleRepositoryKey, "_remote.repositories", trackingFileManager, new DefaultLocalPathPrefixComposerFactory().createComposer(session)); diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java index fbe240992..5bd674136 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java @@ -26,10 +26,10 @@ import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.internal.test.util.TestFileUtils; import org.eclipse.aether.internal.test.util.TestUtils; -import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.LocalArtifactRequest; import org.eclipse.aether.repository.LocalArtifactResult; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.repository.RepositoryIdHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -50,7 +50,7 @@ public class SimpleLocalRepositoryManagerTest { @BeforeEach void setup() throws IOException { manager = new SimpleLocalRepositoryManager( - basedir.toPath(), "simple", new DefaultLocalPathComposer(), ArtifactRepository::getId); + basedir.toPath(), "simple", new DefaultLocalPathComposer(), RepositoryIdHelper::simpleRepositoryKey); session = TestUtils.newSession(); } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java index 5c8bbbbcd..05c827717 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java @@ -18,15 +18,13 @@ */ package org.eclipse.aether.util.repository; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Comparator; import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.BiFunction; -import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.util.PathUtils; @@ -35,10 +33,13 @@ import static java.util.Objects.requireNonNull; /** - * Helper class for {@link ArtifactRepository#getId()} handling. This class provides helper function (cached or uncached) - * to get id of repository as it was originally envisioned: as path safe. While POMs are validated by Maven, there are - * POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly + * Helper class for {@link ArtifactRepository#getId()} handling. This class provides helper methods + * to get id of repository as it was originally envisioned: as path safe, unique, etc. While POMs are validated by Maven, + * there are POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly * {@link RemoteRepository} instances, as all other implementations have fixed ids that are path safe. + *

+ * Important: multiple of these provided methods are not trivial processing-wise, and some sort of + * caching is warmly recommended. * * @see PathUtils * @since 2.0.11 @@ -46,103 +47,168 @@ public final class RepositoryIdHelper { private RepositoryIdHelper() {} - private static final String CENTRAL_REPOSITORY_ID = "central"; - private static final Collection CENTRAL_URLS = Collections.unmodifiableList(Arrays.asList( - "https://repo.maven.apache.org/maven2", - "https://repo1.maven.org/maven2", - "https://maven-central.storage-download.googleapis.com/maven2")); - private static final Predicate CENTRAL_DIRECT_ONLY = - remoteRepository -> CENTRAL_REPOSITORY_ID.equals(remoteRepository.getId()) - && "https".equals(remoteRepository.getProtocol().toLowerCase(Locale.ENGLISH)) - && CENTRAL_URLS.stream().anyMatch(remoteUrl -> { - String rurl = remoteRepository.getUrl().toLowerCase(Locale.ENGLISH); - if (rurl.endsWith("/")) { - rurl = rurl.substring(0, rurl.length() - 1); - } - return rurl.equals(remoteUrl); - }) - && remoteRepository.getPolicy(false).isEnabled() - && !remoteRepository.getPolicy(true).isEnabled() - && remoteRepository.getMirroredRepositories().isEmpty() - && !remoteRepository.isRepositoryManager() - && !remoteRepository.isBlocked(); + /** + * Supported {@code repositoryKey} types. + * + * @since 2.0.14 + */ + public enum RepositoryKeyType { + /** + * The "simple" repository key, was default in Maven 3. + */ + SIMPLE, + /** + * Crafts repository key using normalized {@link RemoteRepository#getId()}. + */ + NID, + /** + * Crafts repository key using hashed {@link RemoteRepository#getUrl()}. + */ + HURL, + /** + * Crafts unique repository key using normalized {@link RemoteRepository#getId()} and hashed {@link RemoteRepository#getUrl()}. + */ + NID_HURL, + /** + * Crafts normalized unique repository key using {@link RemoteRepository#getId()} and all the remaining properties of + * {@link RemoteRepository} ignoring actual list of mirrors, if any (but mirrors are split). + */ + NGURK, + /** + * Crafts unique repository key using {@link RemoteRepository#getId()} and all the remaining properties of + * {@link RemoteRepository}. + */ + GURK + } /** - * Creates unique repository id for given {@link RemoteRepository}. For Maven Central this method will return - * string "central", while for any other remote repository it will return string created as - * {@code $(repository.id)-sha1(repository-aspects)}. The key material contains all relevant aspects - * of remote repository, so repository with same ID even if just policy changes (enabled/disabled), will map to - * different string id. The checksum and update policies are not participating in key creation. - *

- * This method is costly, so should be invoked sparingly, or cache results if needed. - *

- * Important:Do not use this method, or at least do consider when do you want to use it, as it - * totally disconnects repositories used in session. This method may be used under some special circumstances - * (ie reporting), but must not be used within Resolver (and Maven) session for "usual" resolution and - * deployment use cases. + * The repository key function. */ - public static String remoteRepositoryUniqueId(RemoteRepository repository) { - if (CENTRAL_DIRECT_ONLY.test(repository)) { - return CENTRAL_REPOSITORY_ID; - } else { - StringBuilder buffer = new StringBuilder(256); - buffer.append(repository.getId()); - buffer.append(" (").append(repository.getUrl()); - buffer.append(", ").append(repository.getContentType()); - boolean r = repository.getPolicy(false).isEnabled(), - s = repository.getPolicy(true).isEnabled(); - if (r && s) { - buffer.append(", releases+snapshots"); - } else if (r) { - buffer.append(", releases"); - } else if (s) { - buffer.append(", snapshots"); - } else { - buffer.append(", disabled"); - } - if (repository.isRepositoryManager()) { - buffer.append(", managed("); - for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) { - buffer.append(remoteRepositoryUniqueId(mirroredRepo)); - } - buffer.append(")"); + @FunctionalInterface + public interface RepositoryKeyFunction extends BiFunction { + @Override + String apply(RemoteRepository repository, String context); + } + + /** + * Selector method for {@link RepositoryKeyFunction}. + */ + public static RepositoryKeyFunction getRepositoryKeyFunction(String keyTypeString) { + requireNonNull(keyTypeString); + RepositoryKeyType keyType = RepositoryKeyType.valueOf(keyTypeString.toUpperCase(Locale.ENGLISH)); + switch (keyType) { + case SIMPLE: + return RepositoryIdHelper::simpleRepositoryKey; + case NID: + return RepositoryIdHelper::nidRepositoryKey; + case HURL: + return RepositoryIdHelper::hurlRepositoryKey; + case NID_HURL: + return RepositoryIdHelper::nidAndHurlRepositoryKey; + case NGURK: + return RepositoryIdHelper::normalizedGloballyUniqueRepositoryKey; + case GURK: + return RepositoryIdHelper::globallyUniqueRepositoryKey; + default: + throw new IllegalArgumentException("Unknown repository key type: " + keyType.name()); + } + } + + /** + * Simple {@code repositoryKey} function (classic). Returns {@link RemoteRepository#getId()}, unless + * {@link RemoteRepository#isRepositoryManager()} returns {@code true}, in which case this method creates + * unique identifier based on ID and current configuration of the remote repository and context. + *

+ * This was the default {@code repositoryKey} method in Maven 3. Is exposed (others key methods are private) as + * it is directly used by "simple" LRM. + * + * @since 2.0.14 + **/ + public static String simpleRepositoryKey(RemoteRepository repository, String context) { + if (repository.isRepositoryManager()) { + StringBuilder buffer = new StringBuilder(128); + buffer.append(idToPathSegment(repository)); + buffer.append('-'); + SortedSet subKeys = new TreeSet<>(); + for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) { + subKeys.add(mirroredRepo.getId()); } - if (repository.isBlocked()) { - buffer.append(", blocked"); + StringDigestUtil sha1 = StringDigestUtil.sha1(); + sha1.update(context); + for (String subKey : subKeys) { + sha1.update(subKey); } - buffer.append(")"); - return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(buffer.toString()); + buffer.append(sha1.digest()); + return buffer.toString(); + } else { + return idToPathSegment(repository); } } /** - * Returns same instance of (session cached) function for session. - */ - @SuppressWarnings("unchecked") - public static Function cachedIdToPathSegment(RepositorySystemSession session) { - requireNonNull(session, "session"); - return (Function) session.getData() - .computeIfAbsent( - RepositoryIdHelper.class.getSimpleName() + "-idToPathSegmentFunction", - () -> cachedIdToPathSegmentFunction(session)); + * The ID {@code repositoryKey} function that uses only the {@link RemoteRepository#getId()} value to derive a key. + * + * @since 2.0.14 + **/ + private static String nidRepositoryKey(RemoteRepository repository, String context) { + return idToPathSegment(repository); } /** - * Returns new instance of function backed by cached or uncached (if session has no cache set) - * {@link #idToPathSegment(ArtifactRepository)} method call. - */ - @SuppressWarnings("unchecked") - private static Function cachedIdToPathSegmentFunction(RepositorySystemSession session) { - if (session.getCache() != null) { - return repository -> ((ConcurrentHashMap) session.getCache() - .computeIfAbsent( - session, - RepositoryIdHelper.class.getSimpleName() + "-idToPathSegmentCache", - ConcurrentHashMap::new)) - .computeIfAbsent(repository.getId(), id -> idToPathSegment(repository)); - } else { - return RepositoryIdHelper::idToPathSegment; // uncached + * The URL {@code repositoryKey} function that uses only the {@link RemoteRepository#getUrl()} hash to derive a key. + * + * @since 2.0.14 + **/ + private static String hurlRepositoryKey(RemoteRepository repository, String context) { + return StringDigestUtil.sha1(repository.getUrl()); + } + + /** + * The ID and URL {@code repositoryKey} function. This method creates unique identifier based on ID and URL + * of the remote repository. + * + * @since 2.0.14 + **/ + private static String nidAndHurlRepositoryKey(RemoteRepository repository, String context) { + String seed = repository.getUrl(); + if (repository.isRepositoryManager() && context != null && !context.isEmpty()) { + seed += context; + } + return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed); + } + + /** + * Normalized globally unique {@code repositoryKey} function. This method creates unique identifier based on ID and current + * configuration of the remote repository ignoring mirrors (it records the fact repository is a mirror, but ignores + * mirrored repositories). If {@link RemoteRepository#isRepositoryManager()} returns {@code true}, the passed in + * {@code context} string is factored in as well. + * + * @since 2.0.14 + **/ + private static String normalizedGloballyUniqueRepositoryKey(RemoteRepository repository, String context) { + String seed = remoteRepositoryDescription(repository, false); + if (repository.isRepositoryManager() && context != null && !context.isEmpty()) { + seed += context; } + return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed); + } + + /** + * Globally unique {@code repositoryKey} function. This method creates unique identifier based on ID and current + * configuration of the remote repository. If {@link RemoteRepository#isRepositoryManager()} returns {@code true}, + * the passed in {@code context} string is factored in as well. + *

+ * Important: this repository key can be considered "stable" for normal remote repositories (where only + * ID and URL matters). But, for mirror repositories, the key will change if mirror members change. + * + * @since 2.0.14 + **/ + private static String globallyUniqueRepositoryKey(RemoteRepository repository, String context) { + String seed = remoteRepositoryDescription(repository, true); + if (repository.isRepositoryManager() && context != null && !context.isEmpty()) { + seed += context; + } + return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed); } /** @@ -150,17 +216,61 @@ private static Function cachedIdToPathSegmentFunctio * returned repository ID is "path segment" safe. Ideally, this method should never modify repository ID, as * Maven validation prevents use of illegal FS characters in them, but we found in Maven Central several POMs that * define remote repositories with illegal FS characters in their ID. - *

- * This method is simplistic on purpose, and if frequently used, best if results are cached (per session), - * see {@link #cachedIdToPathSegment(RepositorySystemSession)} method. - * - * @see #cachedIdToPathSegment(RepositorySystemSession) */ - private static String idToPathSegment(ArtifactRepository repository) { + public static String idToPathSegment(ArtifactRepository repository) { if (repository instanceof RemoteRepository) { return PathUtils.stringToPathSegment(repository.getId()); } else { return repository.getId(); } } + + /** + * Creates unique string for given {@link RemoteRepository}. Ignores following properties: + *

    + *
  • {@link RemoteRepository#getAuthentication()}
  • + *
  • {@link RemoteRepository#getProxy()}
  • + *
  • {@link RemoteRepository#getIntent()}
  • + *
+ */ + private static String remoteRepositoryDescription(RemoteRepository repository, boolean mirrorDetails) { + StringBuilder buffer = new StringBuilder(256); + buffer.append(repository.getId()); + buffer.append(" (").append(repository.getUrl()); + buffer.append(", ").append(repository.getContentType()); + boolean r = repository.getPolicy(false).isEnabled(), + s = repository.getPolicy(true).isEnabled(); + if (r && s) { + buffer.append(", releases+snapshots"); + } else if (r) { + buffer.append(", releases"); + } else if (s) { + buffer.append(", snapshots"); + } else { + buffer.append(", disabled"); + } + if (repository.isRepositoryManager()) { + buffer.append(", managed"); + } + if (!repository.getMirroredRepositories().isEmpty()) { + if (mirrorDetails) { + // sort them to make it stable ordering + ArrayList mirroredRepositories = + new ArrayList<>(repository.getMirroredRepositories()); + mirroredRepositories.sort(Comparator.comparing(RemoteRepository::getId)); + buffer.append(", mirrorOf("); + for (RemoteRepository mirroredRepo : mirroredRepositories) { + buffer.append(remoteRepositoryDescription(mirroredRepo, true)); + } + buffer.append(")"); + } else { + buffer.append(", isMirror"); + } + } + if (repository.isBlocked()) { + buffer.append(", blocked"); + } + buffer.append(")"); + return buffer.toString(); + } } diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java index bdbc15c1e..f9c10ee72 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java @@ -28,15 +28,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; public class RepositoryIdHelperTest { @Test - void caching() { + void idToPathSegment() { DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(s -> false); session.setCache(new DefaultRepositoryCache()); // session has cache set - Function safeId = RepositoryIdHelper.cachedIdToPathSegment(session); + Function safeId = RepositoryIdHelper::idToPathSegment; RemoteRepository good = new RemoteRepository.Builder("good", "default", "https://somewhere.com").build(); RemoteRepository bad = new RemoteRepository.Builder("bad/id", "default", "https://somewhere.com").build(); @@ -44,33 +42,10 @@ void caching() { String goodId = good.getId(); String goodFixedId = safeId.apply(good); assertEquals(goodId, goodFixedId); - assertSame(goodFixedId, safeId.apply(good)); String badId = bad.getId(); String badFixedId = safeId.apply(bad); assertNotEquals(badId, badFixedId); assertEquals("bad-SLASH-id", badFixedId); - assertSame(badFixedId, safeId.apply(bad)); - } - - @Test - void nonCaching() { - DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(s -> false); - session.setCache(null); // session has no cache set - Function safeId = RepositoryIdHelper.cachedIdToPathSegment(session); - - RemoteRepository good = new RemoteRepository.Builder("good", "default", "https://somewhere.com").build(); - RemoteRepository bad = new RemoteRepository.Builder("bad/id", "default", "https://somewhere.com").build(); - - String goodId = good.getId(); - String goodFixedId = safeId.apply(good); - assertEquals(goodId, goodFixedId); - assertNotSame(goodFixedId, safeId.apply(good)); - - String badId = bad.getId(); - String badFixedId = safeId.apply(bad); - assertNotEquals(badId, badFixedId); - assertEquals("bad-SLASH-id", badFixedId); - assertNotSame(badFixedId, safeId.apply(bad)); } } diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index 4735a16ed..aa4bc61bc 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -74,6 +74,7 @@ To modify this file, edit the template and regenerate. | `"aether.lrm.enhanced.localPrefix"` | `String` | The prefix to use for locally installed artifacts. | `"installed"` | 1.8.1 | No | Session Configuration | | `"aether.lrm.enhanced.releasesPrefix"` | `String` | The prefix to use for release artifacts. | `"releases"` | 1.8.1 | No | Session Configuration | | `"aether.lrm.enhanced.remotePrefix"` | `String` | The prefix to use for remotely cached artifacts. | `"cached"` | 1.8.1 | No | Session Configuration | +| `"aether.lrm.enhanced.repositoryKeyFunction"` | `String` | Experimental: Configuration for "repository key" function. Note: repository key functions other than "simple" produce repository keys will be way different that those produced with previous versions or without this option enabled. Ideally, you may want to use empty local repository to populate with new repository key contained metadata. | `"simple"` | 2.0.14 | No | Session Configuration | | `"aether.lrm.enhanced.snapshotsPrefix"` | `String` | The prefix to use for snapshot artifacts. | `"snapshots"` | 1.8.1 | No | Session Configuration | | `"aether.lrm.enhanced.split"` | `Boolean` | Whether LRM should split local and remote artifacts. | `false` | 1.8.1 | No | Session Configuration | | `"aether.lrm.enhanced.splitLocal"` | `Boolean` | Whether locally installed artifacts should be split by version (release/snapshot). | `false` | 1.8.1 | No | Session Configuration | @@ -107,6 +108,7 @@ To modify this file, edit the template and regenerate. | `"aether.remoteRepositoryFilter.prefixes.skipped"` | `Boolean` | Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if true the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while leaving configuration like #CONFIG_PROP_ENABLED still show the "real state". | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.useMirroredRepositories"` | `Boolean` | Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to work Maven should be aware that given remote repository is mirror and is usually backed by MRM. Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use #CONFIG_PROP_ENABLED with repository ID suffix. | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.useRepositoryManagers"` | `Boolean` | Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to work Maven should be aware that given remote repository is backed by repository manager. Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use #CONFIG_PROP_ENABLED with repository ID suffix. Note: as of today, nothing sets this on remote repositories, but is added for future. | `false` | 2.0.14 | Yes | Session Configuration | +| `"aether.remoteRepositoryManager.repositoryKeyFunction"` | `String` | Experimental: Configuration for "repository key" function. Note: repository key functions other than "nid" produce repository keys will be way different that those produced with previous versions or without this option enabled. Manager uses this key to detect "same" remote repositories, and in case of mirrors, to merge them. | `"nid"` | 2.0.14 | No | Session Configuration | | `"aether.snapshotFilter"` | `Boolean` | The key in the repository session's RepositorySystemSession#getConfigProperties() configurationproperties used to store a Boolean flag whether this filter should be forced to ban snapshots. By default, snapshots are only filtered if the root artifact is not a snapshot. | `false` | | No | Session Configuration | | `"aether.syncContext.named.basedir.locksDir"` | `String` | The location of the directory toi use for locks. If relative path, it is resolved from the local repository root. | `".locks"` | 1.9.0 | No | Session Configuration | | `"aether.syncContext.named.discriminating.discriminator"` | `String` | Configuration property to pass in discriminator, if needed. If not present, it is auto-calculated. | - | 1.7.0 | No | Session Configuration |