001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.util.repository; 020 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.Locale; 026import java.util.Map; 027import java.util.concurrent.ConcurrentHashMap; 028import java.util.function.Function; 029import java.util.function.Predicate; 030 031import org.eclipse.aether.RepositorySystemSession; 032import org.eclipse.aether.repository.ArtifactRepository; 033import org.eclipse.aether.repository.RemoteRepository; 034import org.eclipse.aether.util.StringDigestUtil; 035 036import static java.util.Objects.requireNonNull; 037 038/** 039 * Helper class for {@link ArtifactRepository#getId()} handling. This class provides helper function (cached or uncached) 040 * to get id of repository as it was originally envisioned: as path safe. While POMs are validated by Maven, there are 041 * POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly 042 * {@link RemoteRepository} instances, as all other implementations have fixed ids that are path safe. 043 * 044 * @since 2.0.11 045 */ 046public final class RepositoryIdHelper { 047 private RepositoryIdHelper() {} 048 049 private static final Map<String, String> ILLEGAL_REPO_ID_REPLACEMENTS; 050 051 static { 052 HashMap<String, String> illegalRepoIdReplacements = new HashMap<>(); 053 illegalRepoIdReplacements.put("\\", "-BACKSLASH-"); 054 illegalRepoIdReplacements.put("/", "-SLASH-"); 055 illegalRepoIdReplacements.put(":", "-COLON-"); 056 illegalRepoIdReplacements.put("\"", "-QUOTE-"); 057 illegalRepoIdReplacements.put("<", "-LT-"); 058 illegalRepoIdReplacements.put(">", "-GT-"); 059 illegalRepoIdReplacements.put("|", "-PIPE-"); 060 illegalRepoIdReplacements.put("?", "-QMARK-"); 061 illegalRepoIdReplacements.put("*", "-ASTERISK-"); 062 ILLEGAL_REPO_ID_REPLACEMENTS = Collections.unmodifiableMap(illegalRepoIdReplacements); 063 } 064 065 private static final String CENTRAL_REPOSITORY_ID = "central"; 066 private static final Collection<String> CENTRAL_URLS = Collections.unmodifiableList(Arrays.asList( 067 "https://repo.maven.apache.org/maven2", 068 "https://repo1.maven.org/maven2", 069 "https://maven-central.storage-download.googleapis.com/maven2")); 070 private static final Predicate<RemoteRepository> CENTRAL_DIRECT_ONLY = 071 remoteRepository -> CENTRAL_REPOSITORY_ID.equals(remoteRepository.getId()) 072 && "https".equals(remoteRepository.getProtocol().toLowerCase(Locale.ENGLISH)) 073 && CENTRAL_URLS.stream().anyMatch(remoteUrl -> { 074 String rurl = remoteRepository.getUrl().toLowerCase(Locale.ENGLISH); 075 if (rurl.endsWith("/")) { 076 rurl = rurl.substring(0, rurl.length() - 1); 077 } 078 return rurl.equals(remoteUrl); 079 }) 080 && remoteRepository.getPolicy(false).isEnabled() 081 && !remoteRepository.getPolicy(true).isEnabled() 082 && remoteRepository.getMirroredRepositories().isEmpty() 083 && !remoteRepository.isRepositoryManager() 084 && !remoteRepository.isBlocked(); 085 086 /** 087 * Creates unique repository id for given {@link RemoteRepository}. For Maven Central this method will return 088 * string "central", while for any other remote repository it will return string created as 089 * {@code $(repository.id)-sha1(repository-aspects)}. The key material contains all relevant aspects 090 * of remote repository, so repository with same ID even if just policy changes (enabled/disabled), will map to 091 * different string id. The checksum and update policies are not participating in key creation. 092 * <p> 093 * This method is costly, so should be invoked sparingly, or cache results if needed. 094 */ 095 public static String remoteRepositoryUniqueId(RemoteRepository repository) { 096 if (CENTRAL_DIRECT_ONLY.test(repository)) { 097 return CENTRAL_REPOSITORY_ID; 098 } else { 099 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}