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.internal.impl.filter; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023import javax.inject.Singleton; 024 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.util.Collections; 028import java.util.Objects; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.function.Supplier; 031 032import org.eclipse.aether.DefaultRepositorySystemSession; 033import org.eclipse.aether.RepositorySystemSession; 034import org.eclipse.aether.artifact.Artifact; 035import org.eclipse.aether.impl.MetadataResolver; 036import org.eclipse.aether.impl.RemoteRepositoryManager; 037import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource; 038import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree; 039import org.eclipse.aether.metadata.DefaultMetadata; 040import org.eclipse.aether.metadata.Metadata; 041import org.eclipse.aether.repository.RemoteRepository; 042import org.eclipse.aether.resolution.MetadataRequest; 043import org.eclipse.aether.resolution.MetadataResult; 044import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; 045import org.eclipse.aether.spi.connector.layout.RepositoryLayout; 046import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider; 047import org.eclipse.aether.transfer.NoRepositoryLayoutException; 048import org.eclipse.aether.util.ConfigUtils; 049import org.eclipse.aether.util.repository.RepositoryIdHelper; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052 053import static java.util.Objects.requireNonNull; 054 055/** 056 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path 057 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in 058 * path with no corresponding prefix present in this file is filtered out. 059 * <p> 060 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines 061 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and 062 * MRMs publish these kind of files, they can be downloaded from corresponding URLs. 063 * <p> 064 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt". 065 * <p> 066 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not 067 * noticed. 068 * <p> 069 * Examples of published prefix files: 070 * <ul> 071 * <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li> 072 * <li>Apache Releases: 073 * <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li> 074 * </ul> 075 * 076 * @since 1.9.0 077 */ 078@Singleton 079@Named(PrefixesRemoteRepositoryFilterSource.NAME) 080public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport { 081 public static final String NAME = "prefixes"; 082 083 private static final String CONFIG_PROPS_PREFIX = 084 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + "."; 085 086 private static final String PREFIX_FILE_PATH = ".meta/prefixes.txt"; 087 088 /** 089 * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using 090 * repository ID suffixes. 091 * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without 092 * configuration files, the enabled filter remains dormant and does not interfere with resolution. 093 * <strong>Configuration File Resolution:</strong> 094 * <ol> 095 * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR} 096 * (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li> 097 * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li> 098 * </ol> 099 * <strong>File Naming:</strong> {@code prefixes-$(repository.id).txt} 100 * <strong>Recommended Setup (Auto-Discovery with Override Capability):</strong> 101 * Start with auto-discovery, but prepare for project-specific overrides. Add to {@code .mvn/maven.config}: 102 * <pre> 103 * -Daether.remoteRepositoryFilter.prefixes=true 104 * -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/ 105 * </pre> 106 * <strong>Initial setup:</strong> Don't provide any files - rely on auto-discovery as repositories are accessed. 107 * <strong>Override when needed:</strong> Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and 108 * commit to version control. 109 * <strong>Caching:</strong> Auto-discovered prefix files are cached in the local repository with unique IDs 110 * (using {@link RepositoryIdHelper#remoteRepositoryUniqueId(RemoteRepository)}) to prevent conflicts that 111 * could cause build failures. 112 * 113 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 114 * @configurationType {@link java.lang.Boolean} 115 * @configurationRepoIdSuffix Yes 116 * @configurationDefaultValue {@link #DEFAULT_ENABLED} 117 */ 118 public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME; 119 120 public static final boolean DEFAULT_ENABLED = true; 121 122 /** 123 * The basedir where to store filter files. If path is relative, it is resolved from local repository root. 124 * 125 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 126 * @configurationType {@link java.lang.String} 127 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 128 */ 129 public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir"; 130 131 public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters"; 132 133 static final String PREFIXES_FILE_PREFIX = "prefixes-"; 134 135 static final String PREFIXES_FILE_SUFFIX = ".txt"; 136 137 private final Logger logger = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class); 138 139 private final Supplier<MetadataResolver> metadataResolver; 140 141 private final Supplier<RemoteRepositoryManager> remoteRepositoryManager; 142 143 private final RepositoryLayoutProvider repositoryLayoutProvider; 144 145 private final ConcurrentHashMap<RemoteRepository, PrefixTree> prefixes; 146 147 private final ConcurrentHashMap<RemoteRepository, RepositoryLayout> layouts; 148 149 private final ConcurrentHashMap<RemoteRepository, Boolean> ongoingUpdates; 150 151 @Inject 152 public PrefixesRemoteRepositoryFilterSource( 153 Supplier<MetadataResolver> metadataResolver, 154 Supplier<RemoteRepositoryManager> remoteRepositoryManager, 155 RepositoryLayoutProvider repositoryLayoutProvider) { 156 this.metadataResolver = requireNonNull(metadataResolver); 157 this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager); 158 this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider); 159 this.prefixes = new ConcurrentHashMap<>(); 160 this.layouts = new ConcurrentHashMap<>(); 161 this.ongoingUpdates = new ConcurrentHashMap<>(); 162 } 163 164 @Override 165 protected boolean isEnabled(RepositorySystemSession session) { 166 return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED); 167 } 168 169 private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) { 170 if (isEnabled(session)) { 171 return ConfigUtils.getBoolean( 172 session, 173 ConfigUtils.getBoolean(session, true, CONFIG_PROP_ENABLED + ".*"), 174 CONFIG_PROP_ENABLED + "." + remoteRepository.getId()); 175 } 176 return false; 177 } 178 179 @Override 180 public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { 181 if (isEnabled(session)) { 182 return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false)); 183 } 184 return null; 185 } 186 187 /** 188 * Caches layout instances for remote repository. In case of unknown layout it returns {@code null}. 189 * 190 * @return the layout instance of {@code null} if layout not supported. 191 */ 192 private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) { 193 return layouts.computeIfAbsent(remoteRepository, r -> { 194 try { 195 return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository); 196 } catch (NoRepositoryLayoutException e) { 197 return null; 198 } 199 }); 200 } 201 202 private PrefixTree cachePrefixTree( 203 RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) { 204 return ongoingUpdatesGuard( 205 remoteRepository, 206 () -> prefixes.computeIfAbsent( 207 remoteRepository, r -> loadPrefixTree(session, basedir, remoteRepository)), 208 () -> PrefixTree.SENTINEL); 209 } 210 211 private <T> T ongoingUpdatesGuard(RemoteRepository remoteRepository, Supplier<T> unblocked, Supplier<T> blocked) { 212 if (!remoteRepository.isBlocked() && null == ongoingUpdates.putIfAbsent(remoteRepository, Boolean.TRUE)) { 213 try { 214 return unblocked.get(); 215 } finally { 216 ongoingUpdates.remove(remoteRepository); 217 } 218 } 219 return blocked.get(); 220 } 221 222 private PrefixTree loadPrefixTree( 223 RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) { 224 if (isRepositoryFilteringEnabled(session, remoteRepository)) { 225 String origin = "user-provided"; 226 Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository); 227 if (filePath == null) { 228 origin = "auto-discovered"; 229 filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository); 230 } 231 if (filePath != null) { 232 PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath); 233 if (prefixesSource.valid()) { 234 logger.debug( 235 "Loaded prefixes for remote repository {} from {} file '{}'", 236 prefixesSource.origin().getId(), 237 origin, 238 prefixesSource.path()); 239 PrefixTree prefixTree = new PrefixTree(""); 240 int rules = prefixTree.loadNodes(prefixesSource.entries().stream()); 241 logger.info( 242 "Loaded {} {} prefixes for remote repository {} ({})", 243 rules, 244 origin, 245 prefixesSource.origin().getId(), 246 prefixesSource.path().getFileName()); 247 return prefixTree; 248 } else { 249 logger.info( 250 "Rejected {} prefixes for remote repository {} ({}): {}", 251 origin, 252 prefixesSource.origin().getId(), 253 prefixesSource.path().getFileName(), 254 prefixesSource.message()); 255 } 256 } 257 logger.debug("Prefix file for remote repository {} not available", remoteRepository); 258 return PrefixTree.SENTINEL; 259 } 260 logger.debug("Prefix file for remote repository {} disabled", remoteRepository); 261 return PrefixTree.SENTINEL; 262 } 263 264 private Path resolvePrefixesFromLocalConfiguration( 265 RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) { 266 Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX 267 + RepositoryIdHelper.cachedIdToPathSegment(session).apply(remoteRepository) 268 + PREFIXES_FILE_SUFFIX); 269 if (Files.isReadable(filePath)) { 270 return filePath; 271 } else { 272 return null; 273 } 274 } 275 276 private Path resolvePrefixesFromRemoteRepository( 277 RepositorySystemSession session, RemoteRepository remoteRepository) { 278 MetadataResolver mr = metadataResolver.get(); 279 RemoteRepositoryManager rm = remoteRepositoryManager.get(); 280 if (mr != null && rm != null) { 281 // create "prepared" (auth, proxy and mirror equipped repo) 282 RemoteRepository prepared = rm.aggregateRepositories( 283 session, Collections.emptyList(), Collections.singletonList(remoteRepository), true) 284 .get(0); 285 // make it unique 286 RemoteRepository unique = new RemoteRepository.Builder(prepared) 287 .setId(RepositoryIdHelper.remoteRepositoryUniqueId(remoteRepository)) 288 .build(); 289 // supplier for path 290 Supplier<Path> supplier = () -> { 291 MetadataRequest request = 292 new MetadataRequest(new DefaultMetadata(PREFIX_FILE_PATH, Metadata.Nature.RELEASE_OR_SNAPSHOT)); 293 // use unique repository; this will result in prefix (repository metadata) cached under unique id 294 request.setRepository(unique); 295 request.setDeleteLocalCopyIfMissing(true); 296 request.setFavorLocalRepository(true); 297 MetadataResult result = mr.resolveMetadata( 298 new DefaultRepositorySystemSession(session).setTransferListener(null), 299 Collections.singleton(request)) 300 .get(0); 301 if (result.isResolved()) { 302 return result.getMetadata().getPath(); 303 } else { 304 return null; 305 } 306 }; 307 308 // prevent recursive calls; but we need extra work if not dealing with Central (as in that case outer call 309 // shields us) 310 if (Objects.equals(prepared.getId(), unique.getId())) { 311 return supplier.get(); 312 } else { 313 return ongoingUpdatesGuard(unique, supplier, () -> null); 314 } 315 } 316 return null; 317 } 318 319 private class PrefixesFilter implements RemoteRepositoryFilter { 320 private final RepositorySystemSession session; 321 private final Path basedir; 322 323 private PrefixesFilter(RepositorySystemSession session, Path basedir) { 324 this.session = session; 325 this.basedir = basedir; 326 } 327 328 @Override 329 public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { 330 RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); 331 if (repositoryLayout == null) { 332 return new SimpleResult(true, "Unsupported layout: " + remoteRepository); 333 } 334 return acceptPrefix( 335 remoteRepository, 336 repositoryLayout.getLocation(artifact, false).getPath()); 337 } 338 339 @Override 340 public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { 341 RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); 342 if (repositoryLayout == null) { 343 return new SimpleResult(true, "Unsupported layout: " + remoteRepository); 344 } 345 return acceptPrefix( 346 remoteRepository, 347 repositoryLayout.getLocation(metadata, false).getPath()); 348 } 349 350 private Result acceptPrefix(RemoteRepository remoteRepository, String path) { 351 PrefixTree prefixTree = cachePrefixTree(session, basedir, remoteRepository); 352 if (PrefixTree.SENTINEL == prefixTree) { 353 return NOT_PRESENT_RESULT; 354 } 355 if (prefixTree.acceptedPath(path)) { 356 return new SimpleResult(true, "Path " + path + " allowed from " + remoteRepository); 357 } else { 358 return new SimpleResult(false, "Prefix " + path + " NOT allowed from " + remoteRepository); 359 } 360 } 361 } 362 363 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = 364 new SimpleResult(true, "Prefix file not present"); 365}