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