You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2026/04/13 18:41:18 UTC

(maven) branch maven-3.10.x updated: Feat: user relocations backport (#11937)

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch maven-3.10.x
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/maven-3.10.x by this push:
     new f8f5335ac5 Feat: user relocations backport (#11937)
f8f5335ac5 is described below

commit f8f5335ac510454016fd0b27604efca6f91ceed6
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Mon Apr 13 20:41:12 2026 +0200

    Feat: user relocations backport (#11937)
    
    Backport of user relocations.
    
    Fixes #11917
---
 .../internal/DefaultArtifactDescriptorReader.java  |  90 +++++++--
 .../internal/MavenArtifactRelocationSource.java    |  45 +++++
 .../repository/internal/RelocatedArtifact.java     |  62 ++++--
 ...ributionManagementArtifactRelocationSource.java |  76 +++++++
 .../UserPropertiesArtifactRelocationSource.java    | 225 +++++++++++++++++++++
 5 files changed, 458 insertions(+), 40 deletions(-)

diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultArtifactDescriptorReader.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultArtifactDescriptorReader.java
index bfea91cb6c..761c4f30ab 100644
--- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultArtifactDescriptorReader.java
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultArtifactDescriptorReader.java
@@ -22,6 +22,7 @@
 import javax.inject.Named;
 import javax.inject.Singleton;
 
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -29,9 +30,7 @@
 import java.util.Properties;
 import java.util.Set;
 
-import org.apache.maven.model.DistributionManagement;
 import org.apache.maven.model.Model;
-import org.apache.maven.model.Relocation;
 import org.apache.maven.model.building.DefaultModelBuilderFactory;
 import org.apache.maven.model.building.DefaultModelBuildingRequest;
 import org.apache.maven.model.building.FileModelSource;
@@ -42,6 +41,8 @@
 import org.apache.maven.model.building.ModelProblem;
 import org.apache.maven.model.building.ModelProblemUtils;
 import org.apache.maven.model.resolution.UnresolvableModelException;
+import org.apache.maven.repository.internal.relocation.DistributionManagementArtifactRelocationSource;
+import org.apache.maven.repository.internal.relocation.UserPropertiesArtifactRelocationSource;
 import org.codehaus.plexus.util.StringUtils;
 import org.eclipse.aether.RepositoryEvent;
 import org.eclipse.aether.RepositoryEvent.EventType;
@@ -94,6 +95,8 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
 
     private ModelCacheFactory modelCacheFactory;
 
+    private Map<String, MavenArtifactRelocationSource> artifactRelocationSources;
+
     private final ArtifactDescriptorReaderDelegate artifactDescriptorReaderDelegate =
             new ArtifactDescriptorReaderDelegate();
 
@@ -104,7 +107,10 @@ public DefaultArtifactDescriptorReader() {
         // enable no-arg constructor
     }
 
-    @Inject
+    /**
+     * Just here for "static" resolver providers.
+     */
+    @Deprecated
     public DefaultArtifactDescriptorReader(
             RemoteRepositoryManager remoteRepositoryManager,
             VersionResolver versionResolver,
@@ -120,6 +126,34 @@ public DefaultArtifactDescriptorReader(
         setModelBuilder(modelBuilder);
         setRepositoryEventDispatcher(repositoryEventDispatcher);
         setModelCacheFactory(modelCacheFactory);
+        Map<String, MavenArtifactRelocationSource> artifactRelocationSources = new LinkedHashMap<>(); // order!
+        artifactRelocationSources.put(
+                UserPropertiesArtifactRelocationSource.NAME, new UserPropertiesArtifactRelocationSource());
+        artifactRelocationSources.put(
+                DistributionManagementArtifactRelocationSource.NAME,
+                new DistributionManagementArtifactRelocationSource());
+        setArtifactRelocationSources(artifactRelocationSources);
+    }
+
+    @SuppressWarnings("checkstyle:parameternumber")
+    @Inject
+    public DefaultArtifactDescriptorReader(
+            RemoteRepositoryManager remoteRepositoryManager,
+            VersionResolver versionResolver,
+            VersionRangeResolver versionRangeResolver,
+            ArtifactResolver artifactResolver,
+            ModelBuilder modelBuilder,
+            RepositoryEventDispatcher repositoryEventDispatcher,
+            ModelCacheFactory modelCacheFactory,
+            Map<String, MavenArtifactRelocationSource> artifactRelocationSources) {
+        setRemoteRepositoryManager(remoteRepositoryManager);
+        setVersionResolver(versionResolver);
+        setVersionRangeResolver(versionRangeResolver);
+        setArtifactResolver(artifactResolver);
+        setModelBuilder(modelBuilder);
+        setRepositoryEventDispatcher(repositoryEventDispatcher);
+        setModelCacheFactory(modelCacheFactory);
+        setArtifactRelocationSources(artifactRelocationSources);
     }
 
     @Deprecated
@@ -175,6 +209,13 @@ public DefaultArtifactDescriptorReader setModelCacheFactory(ModelCacheFactory mo
         return this;
     }
 
+    public DefaultArtifactDescriptorReader setArtifactRelocationSources(
+            Map<String, MavenArtifactRelocationSource> artifactRelocationSources) {
+        this.artifactRelocationSources =
+                Objects.requireNonNull(artifactRelocationSources, "artifactRelocationSources cannot be null");
+        return this;
+    }
+
     @Override
     public ArtifactDescriptorResult readArtifactDescriptor(
             RepositorySystemSession session, ArtifactDescriptorRequest request) throws ArtifactDescriptorException {
@@ -336,23 +377,28 @@ private Model loadPom(
                 throw new ArtifactDescriptorException(result);
             }
 
-            Relocation relocation = getRelocation(model);
-
-            if (relocation != null) {
-                result.addRelocation(a);
-                a = new RelocatedArtifact(
-                        a,
-                        relocation.getGroupId(),
-                        relocation.getArtifactId(),
-                        relocation.getVersion(),
-                        relocation.getMessage());
-                result.setArtifact(a);
+            Artifact relocatedArtifact = getRelocation(session, result, model);
+            if (relocatedArtifact != null) {
+                if (withinSameGav(relocatedArtifact, a)) {
+                    result.setArtifact(relocatedArtifact);
+                    return model; // they share same model
+                } else {
+                    result.addRelocation(a);
+                    a = relocatedArtifact;
+                    result.setArtifact(a);
+                }
             } else {
                 return model;
             }
         }
     }
 
+    private boolean withinSameGav(Artifact a1, Artifact a2) {
+        return Objects.equals(a1.getGroupId(), a2.getGroupId())
+                && Objects.equals(a1.getArtifactId(), a2.getArtifactId())
+                && Objects.equals(a1.getVersion(), a2.getVersion());
+    }
+
     private Properties toProperties(Map<String, String> dominant, Map<String, String> recessive) {
         Properties props = new Properties();
         if (recessive != null) {
@@ -364,13 +410,17 @@ private Properties toProperties(Map<String, String> dominant, Map<String, String
         return props;
     }
 
-    private Relocation getRelocation(Model model) {
-        Relocation relocation = null;
-        DistributionManagement distMgmt = model.getDistributionManagement();
-        if (distMgmt != null) {
-            relocation = distMgmt.getRelocation();
+    private Artifact getRelocation(
+            RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model)
+            throws ArtifactDescriptorException {
+        Artifact result = null;
+        for (MavenArtifactRelocationSource source : artifactRelocationSources.values()) {
+            result = source.relocatedTarget(session, artifactDescriptorResult, model);
+            if (result != null) {
+                break;
+            }
         }
-        return relocation;
+        return result;
     }
 
     private void missingDescriptor(
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenArtifactRelocationSource.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenArtifactRelocationSource.java
new file mode 100644
index 0000000000..13d4880c61
--- /dev/null
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenArtifactRelocationSource.java
@@ -0,0 +1,45 @@
+/*
+ * 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.repository.internal;
+
+import org.apache.maven.model.Model;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+
+/**
+ * Maven relocation source.
+ * Note: implementations of this component should avoid the "default" name (has special meaning in Eclipse Sisu) and
+ * explicitly order implementations using Sisu priorities.
+ *
+ * @since 3.10.0
+ */
+public interface MavenArtifactRelocationSource {
+    /**
+     * Returns {@link Artifact} instance where to relocate to, or {@code null}.
+     *
+     * @param session The session, never {@code null}.
+     * @param result The artifact descriptor result, never {@code null}.
+     * @param model The artifact model, never {@code null}.
+     * @return The {@link Artifact} to relocate to, or {@code null} if no relocation wanted.
+     */
+    Artifact relocatedTarget(RepositorySystemSession session, ArtifactDescriptorResult result, Model model)
+            throws ArtifactDescriptorException;
+}
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/RelocatedArtifact.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/RelocatedArtifact.java
index 1053ef983d..44d48b07fc 100644
--- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/RelocatedArtifact.java
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/RelocatedArtifact.java
@@ -36,17 +36,29 @@ public final class RelocatedArtifact extends AbstractArtifact {
 
     private final String artifactId;
 
+    private final String classifier;
+
+    private final String extension;
+
     private final String version;
 
     private final String message;
 
-    RelocatedArtifact(Artifact artifact, String groupId, String artifactId, String version, String message) {
+    public RelocatedArtifact(
+            Artifact artifact,
+            String groupId,
+            String artifactId,
+            String classifier,
+            String extension,
+            String version,
+            String message) {
         this.artifact = Objects.requireNonNull(artifact, "artifact cannot be null");
-        // TODO Use StringUtils here
-        this.groupId = (groupId != null && groupId.length() > 0) ? groupId : null;
-        this.artifactId = (artifactId != null && artifactId.length() > 0) ? artifactId : null;
-        this.version = (version != null && version.length() > 0) ? version : null;
-        this.message = (message != null && message.length() > 0) ? message : null;
+        this.groupId = (groupId != null && !groupId.isEmpty()) ? groupId : null;
+        this.artifactId = (artifactId != null && !artifactId.isEmpty()) ? artifactId : null;
+        this.classifier = (classifier != null && !classifier.isEmpty()) ? classifier : null;
+        this.extension = (extension != null && !extension.isEmpty()) ? extension : null;
+        this.version = (version != null && !version.isEmpty()) ? version : null;
+        this.message = (message != null && !message.isEmpty()) ? message : null;
     }
 
     @Override
@@ -67,6 +79,24 @@ public String getArtifactId() {
         }
     }
 
+    @Override
+    public String getClassifier() {
+        if (classifier != null) {
+            return classifier;
+        } else {
+            return artifact.getClassifier();
+        }
+    }
+
+    @Override
+    public String getExtension() {
+        if (extension != null) {
+            return extension;
+        } else {
+            return artifact.getExtension();
+        }
+    }
+
     @Override
     public String getVersion() {
         if (version != null) {
@@ -80,10 +110,10 @@ public String getVersion() {
     @Override
     public Artifact setVersion(String version) {
         String current = getVersion();
-        if (current.equals(version) || (version == null && current.length() <= 0)) {
+        if (current.equals(version) || (version == null && current.isEmpty())) {
             return this;
         }
-        return new RelocatedArtifact(artifact, groupId, artifactId, version, message);
+        return new RelocatedArtifact(artifact, groupId, artifactId, classifier, extension, version, message);
     }
 
     @Override
@@ -92,7 +122,8 @@ public Artifact setFile(File file) {
         if (Objects.equals(current, file)) {
             return this;
         }
-        return new RelocatedArtifact(artifact.setFile(file), groupId, artifactId, version, message);
+        return new RelocatedArtifact(
+                artifact.setFile(file), groupId, artifactId, classifier, extension, version, message);
     }
 
     @Override
@@ -101,17 +132,8 @@ public Artifact setProperties(Map<String, String> properties) {
         if (current.equals(properties) || (properties == null && current.isEmpty())) {
             return this;
         }
-        return new RelocatedArtifact(artifact.setProperties(properties), groupId, artifactId, version, message);
-    }
-
-    @Override
-    public String getClassifier() {
-        return artifact.getClassifier();
-    }
-
-    @Override
-    public String getExtension() {
-        return artifact.getExtension();
+        return new RelocatedArtifact(
+                artifact.setProperties(properties), groupId, artifactId, classifier, extension, version, message);
     }
 
     @Override
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java
new file mode 100644
index 0000000000..790e85280a
--- /dev/null
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java
@@ -0,0 +1,76 @@
+/*
+ * 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.repository.internal.relocation;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.maven.model.DistributionManagement;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.Relocation;
+import org.apache.maven.repository.internal.MavenArtifactRelocationSource;
+import org.apache.maven.repository.internal.RelocatedArtifact;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.sisu.Priority;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Relocation source from standard distribution management. This is the "one and only" relocation implementation that
+ * existed in Maven 3 land, uses POM distributionManagement/relocation.
+ * <p>
+ * Note: this component should kick-in last regarding relocations.
+ *
+ * @since 3.10.0
+ */
+@Singleton
+@Named(DistributionManagementArtifactRelocationSource.NAME)
+@Priority(5)
+public final class DistributionManagementArtifactRelocationSource implements MavenArtifactRelocationSource {
+    public static final String NAME = "distributionManagement";
+    private static final Logger LOGGER = LoggerFactory.getLogger(DistributionManagementArtifactRelocationSource.class);
+
+    @Override
+    public Artifact relocatedTarget(
+            RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) {
+        DistributionManagement distMgmt = model.getDistributionManagement();
+        if (distMgmt != null) {
+            Relocation relocation = distMgmt.getRelocation();
+            if (relocation != null) {
+                Artifact result = new RelocatedArtifact(
+                        artifactDescriptorResult.getRequest().getArtifact(),
+                        relocation.getGroupId(),
+                        relocation.getArtifactId(),
+                        null,
+                        null,
+                        relocation.getVersion(),
+                        relocation.getMessage());
+                LOGGER.debug(
+                        "The artifact {} has been relocated to {}: {}",
+                        artifactDescriptorResult.getRequest().getArtifact(),
+                        result,
+                        relocation.getMessage());
+                return result;
+            }
+        }
+        return null;
+    }
+}
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/UserPropertiesArtifactRelocationSource.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/UserPropertiesArtifactRelocationSource.java
new file mode 100644
index 0000000000..cb4cc0ac17
--- /dev/null
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/UserPropertiesArtifactRelocationSource.java
@@ -0,0 +1,225 @@
+/*
+ * 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.repository.internal.relocation;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.model.Model;
+import org.apache.maven.repository.internal.MavenArtifactRelocationSource;
+import org.apache.maven.repository.internal.RelocatedArtifact;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.sisu.Priority;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * User controlled relocations.
+ * This property is a comma separated list of entries with the syntax <code>GAV&gt;GAV</code>.
+ * The first <code>GAV</code> can contain <code>*</code> for any elem (so <code>*:*:*</code> would mean ALL, something
+ * you don't want). The second <code>GAV</code> is either fully specified, or also can contain <code>*</code>,
+ * then it behaves as "ordinary relocation": the coordinate is preserved from relocated artifact.
+ * Finally, if right hand <code>GAV</code> is absent (line looks like <code>GAV&gt;</code>), the left hand matching
+ * <code>GAV</code> is banned fully (from resolving).
+ * <br/>
+ * Note: the <code>&gt;</code> means project level, while <code>&gt;&gt;</code> means global (whole session level,
+ * so even plugins will get relocated artifacts) relocation.
+ * <br/>
+ * For example,
+ * <pre>maven.relocations.entries = org.foo:*:*>, \\<br/>    org.here:*:*>org.there:*:*, \\<br/>    javax.inject:javax.inject:1>>jakarta.inject:jakarta.inject:1.0.5</pre>
+ * means: 3 entries, ban <code>org.foo group</code> (exactly, so <code>org.foo.bar</code> is allowed),
+ * relocate <code>org.here</code> to <code>org.there</code> and finally globally relocate (see <code>&gt;&gt;</code> above)
+ * <code>javax.inject:javax.inject:1</code> to <code>jakarta.inject:jakarta.inject:1.0.5</code>.
+ *
+ * @since 3.10.0
+ */
+@Singleton
+@Named(UserPropertiesArtifactRelocationSource.NAME)
+@Priority(50)
+public final class UserPropertiesArtifactRelocationSource implements MavenArtifactRelocationSource {
+    public static final String NAME = "userProperties";
+    private static final Logger LOGGER = LoggerFactory.getLogger(UserPropertiesArtifactRelocationSource.class);
+
+    private static final String CONFIG_PROP_RELOCATIONS_ENTRIES = "maven.relocations.entries";
+
+    private static final Artifact SENTINEL = new DefaultArtifact("org.apache.maven.banned:user-relocation:1.0");
+
+    @Override
+    public Artifact relocatedTarget(
+            RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model)
+            throws ArtifactDescriptorException {
+        Relocations relocations = (Relocations) session.getData()
+                .computeIfAbsent(getClass().getName() + ".relocations", () -> parseRelocations(session));
+        if (relocations != null) {
+            Artifact original = artifactDescriptorResult.getRequest().getArtifact();
+            Relocation relocation = relocations.getRelocation(original);
+            if (relocation != null
+                    && (isProjectContext(artifactDescriptorResult.getRequest().getRequestContext())
+                            || relocation.global)) {
+                if (relocation.target == SENTINEL) {
+                    String message = "The artifact " + original + " has been banned from resolution: "
+                            + (relocation.global ? "User global ban" : "User project ban");
+                    LOGGER.debug(message);
+                    throw new ArtifactDescriptorException(artifactDescriptorResult, message);
+                }
+                Artifact result = new RelocatedArtifact(
+                        original,
+                        isAny(relocation.target.getGroupId()) ? null : relocation.target.getGroupId(),
+                        isAny(relocation.target.getArtifactId()) ? null : relocation.target.getArtifactId(),
+                        isAny(relocation.target.getClassifier()) ? null : relocation.target.getClassifier(),
+                        isAny(relocation.target.getExtension()) ? null : relocation.target.getExtension(),
+                        isAny(relocation.target.getVersion()) ? null : relocation.target.getVersion(),
+                        relocation.global ? "User global relocation" : "User project relocation");
+                LOGGER.debug(
+                        "The artifact {} has been relocated to {}: {}",
+                        original,
+                        result,
+                        relocation.global ? "User global relocation" : "User project relocation");
+                return result;
+            }
+        }
+        return null;
+    }
+
+    private boolean isProjectContext(String context) {
+        return context != null && context.startsWith("project");
+    }
+
+    private static boolean isAny(String str) {
+        return "*".equals(str);
+    }
+
+    private static boolean matches(String pattern, String str) {
+        if (isAny(pattern)) {
+            return true;
+        } else if (pattern.endsWith("*")) {
+            return str.startsWith(pattern.substring(0, pattern.length() - 1));
+        } else {
+            return Objects.equals(pattern, str);
+        }
+    }
+
+    private static Predicate<Artifact> artifactPredicate(Artifact artifact) {
+        return a -> matches(artifact.getGroupId(), a.getGroupId())
+                && matches(artifact.getArtifactId(), a.getArtifactId())
+                && matches(artifact.getBaseVersion(), a.getBaseVersion())
+                && matches(artifact.getExtension(), a.getExtension())
+                && matches(artifact.getClassifier(), a.getClassifier());
+    }
+
+    private static class Relocation {
+        private final Predicate<Artifact> predicate;
+        private final boolean global;
+        private final Artifact source;
+        private final Artifact target;
+
+        private Relocation(boolean global, Artifact source, Artifact target) {
+            this.predicate = artifactPredicate(source);
+            this.global = global;
+            this.source = source;
+            this.target = target;
+        }
+
+        @Override
+        public String toString() {
+            return source + (global ? " >> " : " > ") + target;
+        }
+    }
+
+    private static class Relocations {
+        private final List<Relocation> relocations;
+
+        private Relocations(List<Relocation> relocations) {
+            this.relocations = relocations;
+        }
+
+        private Relocation getRelocation(Artifact artifact) {
+            return relocations.stream()
+                    .filter(r -> r.predicate.test(artifact))
+                    .findFirst()
+                    .orElse(null);
+        }
+    }
+
+    private Relocations parseRelocations(RepositorySystemSession session) {
+        String relocationsEntries = (String) session.getConfigProperties().get(CONFIG_PROP_RELOCATIONS_ENTRIES);
+        if (relocationsEntries == null) {
+            return null;
+        }
+        String[] entries = relocationsEntries.split(",");
+        try (Stream<String> lines = Arrays.stream(entries)) {
+            List<Relocation> relocationList = lines.filter(
+                            l -> l != null && !l.trim().isEmpty())
+                    .map(l -> {
+                        boolean global;
+                        String splitExpr;
+                        if (l.contains(">>")) {
+                            global = true;
+                            splitExpr = ">>";
+                        } else if (l.contains(">")) {
+                            global = false;
+                            splitExpr = ">";
+                        } else {
+                            throw new IllegalArgumentException("Unrecognized entry: " + l);
+                        }
+                        String[] parts = l.split(splitExpr);
+                        if (parts.length < 1) {
+                            throw new IllegalArgumentException("Unrecognized entry: " + l);
+                        }
+                        Artifact s = parseArtifact(parts[0]);
+                        Artifact t;
+                        if (parts.length > 1) {
+                            t = parseArtifact(parts[1]);
+                        } else {
+                            t = SENTINEL;
+                        }
+                        return new Relocation(global, s, t);
+                    })
+                    .collect(Collectors.toList());
+            LOGGER.info("Parsed {} user relocations", relocationList.size());
+            return new Relocations(relocationList);
+        }
+    }
+
+    private static Artifact parseArtifact(String coords) {
+        String[] parts = coords.split(":");
+        switch (parts.length) {
+            case 3:
+                return new DefaultArtifact(parts[0], parts[1], "*", "*", parts[2]);
+            case 4:
+                return new DefaultArtifact(parts[0], parts[1], "*", parts[2], parts[3]);
+            case 5:
+                return new DefaultArtifact(parts[0], parts[1], parts[2], parts[3], parts[4]);
+            default:
+                throw new IllegalArgumentException("Bad artifact coordinates " + coords
+                        + ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>");
+        }
+    }
+}