View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.repository.internal.relocation;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.Objects;
27  import java.util.function.Predicate;
28  import java.util.stream.Collectors;
29  import java.util.stream.Stream;
30  
31  import org.apache.maven.model.Model;
32  import org.apache.maven.repository.internal.MavenArtifactRelocationSource;
33  import org.apache.maven.repository.internal.RelocatedArtifact;
34  import org.eclipse.aether.RepositorySystemSession;
35  import org.eclipse.aether.artifact.Artifact;
36  import org.eclipse.aether.artifact.DefaultArtifact;
37  import org.eclipse.aether.resolution.ArtifactDescriptorException;
38  import org.eclipse.aether.resolution.ArtifactDescriptorResult;
39  import org.eclipse.sisu.Priority;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * User controlled relocations.
45   * This property is a comma separated list of entries with the syntax <code>GAV&gt;GAV</code>.
46   * The first <code>GAV</code> can contain <code>*</code> for any elem (so <code>*:*:*</code> would mean ALL, something
47   * you don't want). The second <code>GAV</code> is either fully specified, or also can contain <code>*</code>,
48   * then it behaves as "ordinary relocation": the coordinate is preserved from relocated artifact.
49   * Finally, if right hand <code>GAV</code> is absent (line looks like <code>GAV&gt;</code>), the left hand matching
50   * <code>GAV</code> is banned fully (from resolving).
51   * <br/>
52   * Note: the <code>&gt;</code> means project level, while <code>&gt;&gt;</code> means global (whole session level,
53   * so even plugins will get relocated artifacts) relocation.
54   * <br/>
55   * For example,
56   * <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>
57   * means: 3 entries, ban <code>org.foo group</code> (exactly, so <code>org.foo.bar</code> is allowed),
58   * relocate <code>org.here</code> to <code>org.there</code> and finally globally relocate (see <code>&gt;&gt;</code> above)
59   * <code>javax.inject:javax.inject:1</code> to <code>jakarta.inject:jakarta.inject:1.0.5</code>.
60   *
61   * @since 3.10.0
62   */
63  @Singleton
64  @Named(UserPropertiesArtifactRelocationSource.NAME)
65  @Priority(50)
66  public final class UserPropertiesArtifactRelocationSource implements MavenArtifactRelocationSource {
67      public static final String NAME = "userProperties";
68      private static final Logger LOGGER = LoggerFactory.getLogger(UserPropertiesArtifactRelocationSource.class);
69  
70      private static final String CONFIG_PROP_RELOCATIONS_ENTRIES = "maven.relocations.entries";
71  
72      private static final Artifact SENTINEL = new DefaultArtifact("org.apache.maven.banned:user-relocation:1.0");
73  
74      @Override
75      public Artifact relocatedTarget(
76              RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model)
77              throws ArtifactDescriptorException {
78          Relocations relocations = (Relocations) session.getData()
79                  .computeIfAbsent(getClass().getName() + ".relocations", () -> parseRelocations(session));
80          if (relocations != null) {
81              Artifact original = artifactDescriptorResult.getRequest().getArtifact();
82              Relocation relocation = relocations.getRelocation(original);
83              if (relocation != null
84                      && (isProjectContext(artifactDescriptorResult.getRequest().getRequestContext())
85                              || relocation.global)) {
86                  if (relocation.target == SENTINEL) {
87                      String message = "The artifact " + original + " has been banned from resolution: "
88                              + (relocation.global ? "User global ban" : "User project ban");
89                      LOGGER.debug(message);
90                      throw new ArtifactDescriptorException(artifactDescriptorResult, message);
91                  }
92                  Artifact result = new RelocatedArtifact(
93                          original,
94                          isAny(relocation.target.getGroupId()) ? null : relocation.target.getGroupId(),
95                          isAny(relocation.target.getArtifactId()) ? null : relocation.target.getArtifactId(),
96                          isAny(relocation.target.getClassifier()) ? null : relocation.target.getClassifier(),
97                          isAny(relocation.target.getExtension()) ? null : relocation.target.getExtension(),
98                          isAny(relocation.target.getVersion()) ? null : relocation.target.getVersion(),
99                          relocation.global ? "User global relocation" : "User project relocation");
100                 LOGGER.debug(
101                         "The artifact {} has been relocated to {}: {}",
102                         original,
103                         result,
104                         relocation.global ? "User global relocation" : "User project relocation");
105                 return result;
106             }
107         }
108         return null;
109     }
110 
111     private boolean isProjectContext(String context) {
112         return context != null && context.startsWith("project");
113     }
114 
115     private static boolean isAny(String str) {
116         return "*".equals(str);
117     }
118 
119     private static boolean matches(String pattern, String str) {
120         if (isAny(pattern)) {
121             return true;
122         } else if (pattern.endsWith("*")) {
123             return str.startsWith(pattern.substring(0, pattern.length() - 1));
124         } else {
125             return Objects.equals(pattern, str);
126         }
127     }
128 
129     private static Predicate<Artifact> artifactPredicate(Artifact artifact) {
130         return a -> matches(artifact.getGroupId(), a.getGroupId())
131                 && matches(artifact.getArtifactId(), a.getArtifactId())
132                 && matches(artifact.getBaseVersion(), a.getBaseVersion())
133                 && matches(artifact.getExtension(), a.getExtension())
134                 && matches(artifact.getClassifier(), a.getClassifier());
135     }
136 
137     private static class Relocation {
138         private final Predicate<Artifact> predicate;
139         private final boolean global;
140         private final Artifact source;
141         private final Artifact target;
142 
143         private Relocation(boolean global, Artifact source, Artifact target) {
144             this.predicate = artifactPredicate(source);
145             this.global = global;
146             this.source = source;
147             this.target = target;
148         }
149 
150         @Override
151         public String toString() {
152             return source + (global ? " >> " : " > ") + target;
153         }
154     }
155 
156     private static class Relocations {
157         private final List<Relocation> relocations;
158 
159         private Relocations(List<Relocation> relocations) {
160             this.relocations = relocations;
161         }
162 
163         private Relocation getRelocation(Artifact artifact) {
164             return relocations.stream()
165                     .filter(r -> r.predicate.test(artifact))
166                     .findFirst()
167                     .orElse(null);
168         }
169     }
170 
171     private Relocations parseRelocations(RepositorySystemSession session) {
172         String relocationsEntries = (String) session.getConfigProperties().get(CONFIG_PROP_RELOCATIONS_ENTRIES);
173         if (relocationsEntries == null) {
174             return null;
175         }
176         String[] entries = relocationsEntries.split(",");
177         try (Stream<String> lines = Arrays.stream(entries)) {
178             List<Relocation> relocationList = lines.filter(
179                             l -> l != null && !l.trim().isEmpty())
180                     .map(l -> {
181                         boolean global;
182                         String splitExpr;
183                         if (l.contains(">>")) {
184                             global = true;
185                             splitExpr = ">>";
186                         } else if (l.contains(">")) {
187                             global = false;
188                             splitExpr = ">";
189                         } else {
190                             throw new IllegalArgumentException("Unrecognized entry: " + l);
191                         }
192                         String[] parts = l.split(splitExpr);
193                         if (parts.length < 1) {
194                             throw new IllegalArgumentException("Unrecognized entry: " + l);
195                         }
196                         Artifact s = parseArtifact(parts[0]);
197                         Artifact t;
198                         if (parts.length > 1) {
199                             t = parseArtifact(parts[1]);
200                         } else {
201                             t = SENTINEL;
202                         }
203                         return new Relocation(global, s, t);
204                     })
205                     .collect(Collectors.toList());
206             LOGGER.info("Parsed {} user relocations", relocationList.size());
207             return new Relocations(relocationList);
208         }
209     }
210 
211     private static Artifact parseArtifact(String coords) {
212         String[] parts = coords.split(":");
213         switch (parts.length) {
214             case 3:
215                 return new DefaultArtifact(parts[0], parts[1], "*", "*", parts[2]);
216             case 4:
217                 return new DefaultArtifact(parts[0], parts[1], "*", parts[2], parts[3]);
218             case 5:
219                 return new DefaultArtifact(parts[0], parts[1], parts[2], parts[3], parts[4]);
220             default:
221                 throw new IllegalArgumentException("Bad artifact coordinates " + coords
222                         + ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>");
223         }
224     }
225 }