[MNG-7959] User controlled relocations (#1339)

With some improvements. It accepts user property with CSV entries for relocations.

To toy with it, use `-Dmaven.relocations.entries` user property, for example create `.mvn/maven.config` file with contents:
```
-Dmaven.relocations.entries=entry1,entry2,...
```
It accepts CSV (comma delimited) of entries, while entry form is as:
```
GAV>GAV
```
Where left GAV can contain `*` for any elem (so `*:*:*` would mean ALL, something you don't want). Right GAV is either fully specified, or also can contain `*`, then it behaves as "ordinary relocation": the coordinate is preserved from relocated artifact. Finally, if right hand GAV is absent (line looks like "GAV>"). the left hand matching GAV is banned fully (from resolving).

Note: the ">" means project level, while ">>" means global (whole session level, so even plugins will get relocated artifacts) relocation.

Examples:
```
-Dmaven.relocations.entries=org.foo:*:*>,org.here:*:*>org.there:*:*,javax.inject:javax.inject:1>>jakarta.inject:jakarta.inject:1.0.5
```

Meaning: 3 entries, ban `org.foo` group (exactly, so `org.foo.bar` is allowed), relocate `org.here` to `org.there` and finally **globally relocate** (see ">>") `javax.inject:javax.inject:1` to `jakarta.inject:jakarta.inject:1.0.5`

---

https://issues.apache.org/jira/browse/MNG-7959
This commit is contained in:
Tamas Cservenak 2023-12-18 11:34:30 +01:00 committed by GitHub
parent 6e192fbb54
commit e34afc897a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 393 additions and 56 deletions

View File

@ -78,6 +78,21 @@ under the License.
<artifactId>javax.inject</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.sisu</groupId>
<artifactId>org.eclipse.sisu.plexus</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.sisu</groupId>
<artifactId>org.eclipse.sisu.inject</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
@ -110,21 +125,6 @@ under the License.
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.sisu</groupId>
<artifactId>org.eclipse.sisu.plexus</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.sisu</groupId>
<artifactId>org.eclipse.sisu.inject</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -28,9 +28,7 @@ import java.util.Objects;
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.ArtifactModelSource;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.ModelBuilder;
@ -64,16 +62,13 @@ import org.eclipse.aether.resolution.VersionRequest;
import org.eclipse.aether.resolution.VersionResolutionException;
import org.eclipse.aether.resolution.VersionResult;
import org.eclipse.aether.transfer.ArtifactNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default artifact descriptor reader.
*/
@Named
@Singleton
public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultArtifactDescriptorReader.class);
private final RemoteRepositoryManager remoteRepositoryManager;
private final VersionResolver versionResolver;
private final VersionRangeResolver versionRangeResolver;
@ -81,6 +76,8 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
private final RepositoryEventDispatcher repositoryEventDispatcher;
private final ModelBuilder modelBuilder;
private final ModelCacheFactory modelCacheFactory;
private final Map<String, MavenArtifactRelocationSource> artifactRelocationSources;
private final ArtifactDescriptorReaderDelegate delegate;
@Inject
public DefaultArtifactDescriptorReader(
@ -90,7 +87,8 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
ArtifactResolver artifactResolver,
ModelBuilder modelBuilder,
RepositoryEventDispatcher repositoryEventDispatcher,
ModelCacheFactory modelCacheFactory) {
ModelCacheFactory modelCacheFactory,
Map<String, MavenArtifactRelocationSource> artifactRelocationSources) {
this.remoteRepositoryManager =
Objects.requireNonNull(remoteRepositoryManager, "remoteRepositoryManager cannot be null");
this.versionResolver = Objects.requireNonNull(versionResolver, "versionResolver cannot be null");
@ -100,6 +98,9 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
this.repositoryEventDispatcher =
Objects.requireNonNull(repositoryEventDispatcher, "repositoryEventDispatcher cannot be null");
this.modelCacheFactory = Objects.requireNonNull(modelCacheFactory, "modelCacheFactory cannot be null");
this.artifactRelocationSources =
Objects.requireNonNull(artifactRelocationSources, "artifactRelocationSources cannot be null");
this.delegate = new ArtifactDescriptorReaderDelegate();
}
@Override
@ -114,7 +115,7 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
(ArtifactDescriptorReaderDelegate) config.get(ArtifactDescriptorReaderDelegate.class.getName());
if (delegate == null) {
delegate = new ArtifactDescriptorReaderDelegate();
delegate = this.delegate;
}
delegate.populateResult(session, result, model);
@ -236,16 +237,10 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
throw new ArtifactDescriptorException(result);
}
Relocation relocation = getRelocation(model);
if (relocation != null) {
Artifact relocatedArtifact = getRelocation(session, request, model);
if (relocatedArtifact != null) {
result.addRelocation(a);
a = new RelocatedArtifact(
a,
relocation.getGroupId(),
relocation.getArtifactId(),
relocation.getVersion(),
relocation.getMessage());
a = relocatedArtifact;
result.setArtifact(a);
} else {
return model;
@ -264,13 +259,15 @@ public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader
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, ArtifactDescriptorRequest request, Model model) {
Artifact result = null;
for (MavenArtifactRelocationSource source : artifactRelocationSources.values()) {
result = source.relocatedTarget(session, request, model);
if (result != null) {
break;
}
}
return relocation;
return result;
}
private void missingDescriptor(

View File

@ -0,0 +1,41 @@
/*
* 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.ArtifactDescriptorRequest;
/**
* Maven relocation source.
*
* @since 4.0.0
*/
public interface MavenArtifactRelocationSource {
/**
* Returns {@link Artifact} instance where to relocate to, or {@code null}.
*
* @param session The session, never {@code null}.
* @param request The artifact descriptor request, 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, ArtifactDescriptorRequest request, Model model);
}

View File

@ -35,16 +35,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");
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
@ -65,6 +78,24 @@ public final class RelocatedArtifact extends AbstractArtifact {
}
}
@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) {
@ -81,7 +112,7 @@ public final class RelocatedArtifact extends AbstractArtifact {
if (current.equals(version) || (version == null && current.length() <= 0)) {
return this;
}
return new RelocatedArtifact(artifact, groupId, artifactId, version, message);
return new RelocatedArtifact(artifact, groupId, artifactId, classifier, extension, version, message);
}
@Override
@ -90,7 +121,8 @@ public final class RelocatedArtifact extends AbstractArtifact {
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
@ -99,17 +131,8 @@ public final class RelocatedArtifact extends AbstractArtifact {
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

View File

@ -0,0 +1,75 @@
/*
* 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.ArtifactDescriptorRequest;
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 4.0.0
*/
@Singleton
@Named
@Priority(5)
@SuppressWarnings("checkstyle:MagicNumber")
public final class DistributionManagementArtifactRelocationSource implements MavenArtifactRelocationSource {
private static final Logger LOGGER = LoggerFactory.getLogger(DistributionManagementArtifactRelocationSource.class);
@Override
public Artifact relocatedTarget(RepositorySystemSession session, ArtifactDescriptorRequest request, Model model) {
DistributionManagement distMgmt = model.getDistributionManagement();
if (distMgmt != null) {
Relocation relocation = distMgmt.getRelocation();
if (relocation != null) {
Artifact result = new RelocatedArtifact(
request.getArtifact(),
relocation.getGroupId(),
relocation.getArtifactId(),
null,
null,
relocation.getVersion(),
relocation.getMessage());
LOGGER.debug(
"The artifact {} has been relocated to {}: {}",
request.getArtifact(),
result,
relocation.getMessage());
return result;
}
}
return null;
}
}

View File

@ -0,0 +1,201 @@
/*
* 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.ArtifactDescriptorRequest;
import org.eclipse.sisu.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Relocation source from user properties.
*
* @since 4.0.0
*/
@Singleton
@Named
@Priority(50)
@SuppressWarnings("checkstyle:MagicNumber")
public final class UserPropertiesArtifactRelocationSource implements MavenArtifactRelocationSource {
private static final Logger LOGGER = LoggerFactory.getLogger(UserPropertiesArtifactRelocationSource.class);
private static final String CONFIG_PROP_RELOCATIONS_ENTRIES = "maven.relocations.entries";
@Override
public Artifact relocatedTarget(RepositorySystemSession session, ArtifactDescriptorRequest request, Model model) {
Relocations relocations = (Relocations) session.getData()
.computeIfAbsent(getClass().getName() + ".relocations", () -> parseRelocations(session));
if (relocations != null) {
Relocation relocation = relocations.getRelocation(request.getArtifact());
if (relocation != null && (isProjectContext(request.getRequestContext()) || relocation.global)) {
Artifact result = new RelocatedArtifact(
request.getArtifact(),
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 {}: {}",
request.getArtifact(),
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 = new DefaultArtifact("org.apache.maven.banned:user-relocation:1.0");
}
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 coord) {
Artifact s;
String[] parts = coord.split(":");
switch (parts.length) {
case 3:
s = new DefaultArtifact(parts[0], parts[1], "*", "*", parts[2]);
break;
case 4:
s = new DefaultArtifact(parts[0], parts[1], "*", parts[2], parts[3]);
break;
case 5:
s = new DefaultArtifact(parts[0], parts[1], parts[2], parts[3], parts[4]);
break;
default:
throw new IllegalArgumentException("Bad artifact coordinates " + coord
+ ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>");
}
return s;
}
}