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.eclipse.aether.util.repository;
20  
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.function.Function;
29  import java.util.function.Predicate;
30  
31  import org.eclipse.aether.RepositorySystemSession;
32  import org.eclipse.aether.repository.ArtifactRepository;
33  import org.eclipse.aether.repository.RemoteRepository;
34  import org.eclipse.aether.util.StringDigestUtil;
35  
36  import static java.util.Objects.requireNonNull;
37  
38  /**
39   * Helper class for {@link ArtifactRepository#getId()} handling. This class provides  helper function (cached or uncached)
40   * to get id of repository as it was originally envisioned: as path safe. While POMs are validated by Maven, there are
41   * POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly
42   * {@link RemoteRepository} instances, as all other implementations have fixed ids that are path safe.
43   *
44   * @since 2.0.11
45   */
46  public final class RepositoryIdHelper {
47      private RepositoryIdHelper() {}
48  
49      private static final Map<String, String> ILLEGAL_REPO_ID_REPLACEMENTS;
50  
51      static {
52          HashMap<String, String> illegalRepoIdReplacements = new HashMap<>();
53          illegalRepoIdReplacements.put("\\", "-BACKSLASH-");
54          illegalRepoIdReplacements.put("/", "-SLASH-");
55          illegalRepoIdReplacements.put(":", "-COLON-");
56          illegalRepoIdReplacements.put("\"", "-QUOTE-");
57          illegalRepoIdReplacements.put("<", "-LT-");
58          illegalRepoIdReplacements.put(">", "-GT-");
59          illegalRepoIdReplacements.put("|", "-PIPE-");
60          illegalRepoIdReplacements.put("?", "-QMARK-");
61          illegalRepoIdReplacements.put("*", "-ASTERISK-");
62          ILLEGAL_REPO_ID_REPLACEMENTS = Collections.unmodifiableMap(illegalRepoIdReplacements);
63      }
64  
65      private static final String CENTRAL_REPOSITORY_ID = "central";
66      private static final Collection<String> CENTRAL_URLS = Collections.unmodifiableList(Arrays.asList(
67              "https://repo.maven.apache.org/maven2",
68              "https://repo1.maven.org/maven2",
69              "https://maven-central.storage-download.googleapis.com/maven2"));
70      private static final Predicate<RemoteRepository> CENTRAL_DIRECT_ONLY =
71              remoteRepository -> CENTRAL_REPOSITORY_ID.equals(remoteRepository.getId())
72                      && "https".equals(remoteRepository.getProtocol().toLowerCase(Locale.ENGLISH))
73                      && CENTRAL_URLS.stream().anyMatch(remoteUrl -> {
74                          String rurl = remoteRepository.getUrl().toLowerCase(Locale.ENGLISH);
75                          if (rurl.endsWith("/")) {
76                              rurl = rurl.substring(0, rurl.length() - 1);
77                          }
78                          return rurl.equals(remoteUrl);
79                      })
80                      && remoteRepository.getPolicy(false).isEnabled()
81                      && !remoteRepository.getPolicy(true).isEnabled()
82                      && remoteRepository.getMirroredRepositories().isEmpty()
83                      && !remoteRepository.isRepositoryManager()
84                      && !remoteRepository.isBlocked();
85  
86      /**
87       * Creates unique repository id for given {@link RemoteRepository}. For Maven Central this method will return
88       * string "central", while for any other remote repository it will return string created as
89       * {@code $(repository.id)-sha1(repository-aspects)}. The key material contains all relevant aspects
90       * of remote repository, so repository with same ID even if just policy changes (enabled/disabled), will map to
91       * different string id. The checksum and update policies are not participating in key creation.
92       * <p>
93       * This method is costly, so should be invoked sparingly, or cache results if needed.
94       */
95      public static String remoteRepositoryUniqueId(RemoteRepository repository) {
96          if (CENTRAL_DIRECT_ONLY.test(repository)) {
97              return CENTRAL_REPOSITORY_ID;
98          } else {
99              StringBuilder buffer = new StringBuilder(256);
100             buffer.append(repository.getId());
101             buffer.append(" (").append(repository.getUrl());
102             buffer.append(", ").append(repository.getContentType());
103             boolean r = repository.getPolicy(false).isEnabled(),
104                     s = repository.getPolicy(true).isEnabled();
105             if (r && s) {
106                 buffer.append(", releases+snapshots");
107             } else if (r) {
108                 buffer.append(", releases");
109             } else if (s) {
110                 buffer.append(", snapshots");
111             } else {
112                 buffer.append(", disabled");
113             }
114             if (repository.isRepositoryManager()) {
115                 buffer.append(", managed(");
116                 for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) {
117                     buffer.append(remoteRepositoryUniqueId(mirroredRepo));
118                 }
119                 buffer.append(")");
120             }
121             if (repository.isBlocked()) {
122                 buffer.append(", blocked");
123             }
124             buffer.append(")");
125             return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(buffer.toString());
126         }
127     }
128 
129     /**
130      * Returns same instance of (session cached) function for session.
131      */
132     @SuppressWarnings("unchecked")
133     public static Function<ArtifactRepository, String> cachedIdToPathSegment(RepositorySystemSession session) {
134         requireNonNull(session, "session");
135         return (Function<ArtifactRepository, String>) session.getData()
136                 .computeIfAbsent(
137                         RepositoryIdHelper.class.getSimpleName() + "-idToPathSegmentFunction",
138                         () -> cachedIdToPathSegmentFunction(session));
139     }
140 
141     /**
142      * Returns new instance of function backed by cached or uncached (if session has no cache set)
143      * {@link #idToPathSegment(ArtifactRepository)} method call.
144      */
145     @SuppressWarnings("unchecked")
146     private static Function<ArtifactRepository, String> cachedIdToPathSegmentFunction(RepositorySystemSession session) {
147         if (session.getCache() != null) {
148             return repository -> ((ConcurrentHashMap<String, String>) session.getCache()
149                             .computeIfAbsent(
150                                     session,
151                                     RepositoryIdHelper.class.getSimpleName() + "-idToPathSegmentCache",
152                                     ConcurrentHashMap::new))
153                     .computeIfAbsent(repository.getId(), id -> idToPathSegment(repository));
154         } else {
155             return RepositoryIdHelper::idToPathSegment; // uncached
156         }
157     }
158 
159     /**
160      * This method returns the passed in {@link ArtifactRepository#getId()} value, modifying it if needed, making sure that
161      * returned repository ID is "path segment" safe. Ideally, this method should never modify repository ID, as
162      * Maven validation prevents use of illegal FS characters in them, but we found in Maven Central several POMs that
163      * define remote repositories with illegal FS characters in their ID.
164      * <p>
165      * This method is simplistic on purpose, and if frequently used, best if results are cached (per session),
166      * see {@link #cachedIdToPathSegment(RepositorySystemSession)} method.
167      * <p>
168      * This method is visible for testing only, should not be used in any other scenarios.
169      *
170      * @see #cachedIdToPathSegment(RepositorySystemSession)
171      */
172     static String idToPathSegment(ArtifactRepository repository) {
173         if (repository instanceof RemoteRepository) {
174             StringBuilder result = new StringBuilder(repository.getId());
175             for (Map.Entry<String, String> entry : ILLEGAL_REPO_ID_REPLACEMENTS.entrySet()) {
176                 String illegal = entry.getKey();
177                 int pos = result.indexOf(illegal);
178                 while (pos >= 0) {
179                     result.replace(pos, pos + illegal.length(), entry.getValue());
180                     pos = result.indexOf(illegal);
181                 }
182             }
183             return result.toString();
184         } else {
185             return repository.getId();
186         }
187     }
188 }