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.Locale; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.function.Function; 027import java.util.function.Predicate; 028 029import org.eclipse.aether.RepositorySystemSession; 030import org.eclipse.aether.repository.ArtifactRepository; 031import org.eclipse.aether.repository.RemoteRepository; 032import org.eclipse.aether.util.PathUtils; 033import org.eclipse.aether.util.StringDigestUtil; 034 035import static java.util.Objects.requireNonNull; 036 037/** 038 * Helper class for {@link ArtifactRepository#getId()} handling. This class provides helper function (cached or uncached) 039 * to get id of repository as it was originally envisioned: as path safe. While POMs are validated by Maven, there are 040 * POMs out there that somehow define repositories with unsafe characters in their id. The problem affects mostly 041 * {@link RemoteRepository} instances, as all other implementations have fixed ids that are path safe. 042 * 043 * @see PathUtils 044 * @since 2.0.11 045 */ 046public final class RepositoryIdHelper { 047 private RepositoryIdHelper() {} 048 049 private static final String CENTRAL_REPOSITORY_ID = "central"; 050 private static final Collection<String> CENTRAL_URLS = Collections.unmodifiableList(Arrays.asList( 051 "https://repo.maven.apache.org/maven2", 052 "https://repo1.maven.org/maven2", 053 "https://maven-central.storage-download.googleapis.com/maven2")); 054 private static final Predicate<RemoteRepository> CENTRAL_DIRECT_ONLY = 055 remoteRepository -> CENTRAL_REPOSITORY_ID.equals(remoteRepository.getId()) 056 && "https".equals(remoteRepository.getProtocol().toLowerCase(Locale.ENGLISH)) 057 && CENTRAL_URLS.stream().anyMatch(remoteUrl -> { 058 String rurl = remoteRepository.getUrl().toLowerCase(Locale.ENGLISH); 059 if (rurl.endsWith("/")) { 060 rurl = rurl.substring(0, rurl.length() - 1); 061 } 062 return rurl.equals(remoteUrl); 063 }) 064 && remoteRepository.getPolicy(false).isEnabled() 065 && !remoteRepository.getPolicy(true).isEnabled() 066 && remoteRepository.getMirroredRepositories().isEmpty() 067 && !remoteRepository.isRepositoryManager() 068 && !remoteRepository.isBlocked(); 069 070 /** 071 * Creates unique repository id for given {@link RemoteRepository}. For Maven Central this method will return 072 * string "central", while for any other remote repository it will return string created as 073 * {@code $(repository.id)-sha1(repository-aspects)}. The key material contains all relevant aspects 074 * of remote repository, so repository with same ID even if just policy changes (enabled/disabled), will map to 075 * different string id. The checksum and update policies are not participating in key creation. 076 * <p> 077 * This method is costly, so should be invoked sparingly, or cache results if needed. 078 */ 079 public static String remoteRepositoryUniqueId(RemoteRepository repository) { 080 if (CENTRAL_DIRECT_ONLY.test(repository)) { 081 return CENTRAL_REPOSITORY_ID; 082 } else { 083 StringBuilder buffer = new StringBuilder(256); 084 buffer.append(repository.getId()); 085 buffer.append(" (").append(repository.getUrl()); 086 buffer.append(", ").append(repository.getContentType()); 087 boolean r = repository.getPolicy(false).isEnabled(), 088 s = repository.getPolicy(true).isEnabled(); 089 if (r && s) { 090 buffer.append(", releases+snapshots"); 091 } else if (r) { 092 buffer.append(", releases"); 093 } else if (s) { 094 buffer.append(", snapshots"); 095 } else { 096 buffer.append(", disabled"); 097 } 098 if (repository.isRepositoryManager()) { 099 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}