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.net.URI; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.util.Collections; 029import java.util.List; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.ConcurrentMap; 032import java.util.function.Supplier; 033 034import org.eclipse.aether.DefaultRepositorySystemSession; 035import org.eclipse.aether.Keys; 036import org.eclipse.aether.RepositorySystemSession; 037import org.eclipse.aether.artifact.Artifact; 038import org.eclipse.aether.impl.MetadataResolver; 039import org.eclipse.aether.impl.RemoteRepositoryManager; 040import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource; 041import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree; 042import org.eclipse.aether.metadata.DefaultMetadata; 043import org.eclipse.aether.metadata.Metadata; 044import org.eclipse.aether.repository.RemoteRepository; 045import org.eclipse.aether.resolution.MetadataRequest; 046import org.eclipse.aether.resolution.MetadataResult; 047import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory; 048import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; 049import org.eclipse.aether.spi.connector.layout.RepositoryLayout; 050import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider; 051import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory; 052import org.eclipse.aether.transfer.NoRepositoryLayoutException; 053import org.eclipse.aether.util.ConfigUtils; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057import static java.util.Objects.requireNonNull; 058 059/** 060 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path 061 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in 062 * path with no corresponding prefix present in this file is filtered out. 063 * <p> 064 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines 065 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and 066 * MRMs publish these kind of files, they can be downloaded from corresponding URLs. 067 * <p> 068 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt". 069 * <p> 070 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not 071 * noticed. 072 * <p> 073 * Examples of published prefix files: 074 * <ul> 075 * <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li> 076 * <li>Apache Releases: 077 * <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li> 078 * </ul> 079 * 080 * @since 1.9.0 081 */ 082@Singleton 083@Named(PrefixesRemoteRepositoryFilterSource.NAME) 084public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport { 085 public static final String NAME = "prefixes"; 086 087 static final String PREFIX_FILE_TYPE = ".meta/prefixes.txt"; 088 089 /** 090 * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using 091 * repository ID suffixes. 092 * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without 093 * configuration files, the enabled filter remains dormant and does not interfere with resolution. 094 * <strong>Configuration File Resolution:</strong> 095 * <ol> 096 * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR} 097 * (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li> 098 * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li> 099 * </ol> 100 * <strong>File Naming:</strong> {@code prefixes-$(repository.id).txt} 101 * <strong>Recommended Setup (Auto-Discovery with Override Capability):</strong> 102 * Start with auto-discovery, but prepare for project-specific overrides. Add to {@code .mvn/maven.config}: 103 * <pre> 104 * -Daether.remoteRepositoryFilter.prefixes=true 105 * -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/ 106 * </pre> 107 * <strong>Initial setup:</strong> Don't provide any files - rely on auto-discovery as repositories are accessed. 108 * <strong>Override when needed:</strong> Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and 109 * commit to version control. 110 * <strong>Caching:</strong> Auto-discovered prefix files are cached in the local repository. 111 * 112 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 113 * @configurationType {@link java.lang.Boolean} 114 * @configurationRepoIdSuffix Yes 115 * @configurationDefaultValue {@link #DEFAULT_ENABLED} 116 */ 117 public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME; 118 119 public static final boolean DEFAULT_ENABLED = true; 120 121 /** 122 * Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if {@code true} 123 * the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during 124 * discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while 125 * leaving configuration like {@link #CONFIG_PROP_ENABLED} still show the "real state". 126 * 127 * @since 2.0.14 128 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 129 * @configurationType {@link java.lang.Boolean} 130 * @configurationRepoIdSuffix Yes 131 * @configurationDefaultValue {@link #DEFAULT_SKIPPED} 132 */ 133 public static final String CONFIG_PROP_SKIPPED = 134 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped"; 135 136 public static final boolean DEFAULT_SKIPPED = false; 137 138 /** 139 * Determines what happens when the filter is enabled, but has no prefixes available for given remote repository 140 * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote 141 * repository when no prefixes are available. When set to {@code false}, the filter blocks all requests toward 142 * given remote repository when no prefixes are available. This setting allows repoId suffix, hence, can 143 * determine "global" or "repository targeted" behaviors. 144 * 145 * @since 2.0.14 146 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 147 * @configurationType {@link java.lang.Boolean} 148 * @configurationRepoIdSuffix Yes 149 * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME} 150 */ 151 public static final String CONFIG_PROP_NO_INPUT_OUTCOME = 152 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome"; 153 154 public static final boolean DEFAULT_NO_INPUT_OUTCOME = true; 155 156 /** 157 * Configuration to allow Prefixes file resolution attempt from remote repository as "auto discovery". If this 158 * configuration set to {@code false} only user-provided prefixes will be used. 159 * 160 * @since 2.0.14 161 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 162 * @configurationType {@link java.lang.Boolean} 163 * @configurationRepoIdSuffix Yes 164 * @configurationDefaultValue {@link #DEFAULT_RESOLVE_PREFIX_FILES} 165 */ 166 public static final String CONFIG_PROP_RESOLVE_PREFIX_FILES = 167 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".resolvePrefixFiles"; 168 169 public static final boolean DEFAULT_RESOLVE_PREFIX_FILES = true; 170 171 /** 172 * Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to 173 * work <em>Maven should be aware</em> that given remote repository is mirror and is usually backed by MRM. Given 174 * multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use 175 * {@link #CONFIG_PROP_ENABLED} with repository ID suffix. 176 * 177 * @since 2.0.14 178 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 179 * @configurationType {@link java.lang.Boolean} 180 * @configurationRepoIdSuffix Yes 181 * @configurationDefaultValue {@link #DEFAULT_USE_MIRRORED_REPOSITORIES} 182 */ 183 public static final String CONFIG_PROP_USE_MIRRORED_REPOSITORIES = 184 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useMirroredRepositories"; 185 186 public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false; 187 188 /** 189 * Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to 190 * work <em>Maven should be aware</em> that given remote repository is backed by repository manager. 191 * Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use 192 * {@link #CONFIG_PROP_ENABLED} with repository ID suffix. 193 * <em>Note: as of today, nothing sets this on remote repositories, but is added for future.</em> 194 * 195 * @since 2.0.14 196 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 197 * @configurationType {@link java.lang.Boolean} 198 * @configurationRepoIdSuffix Yes 199 * @configurationDefaultValue {@link #DEFAULT_USE_REPOSITORY_MANAGERS} 200 */ 201 public static final String CONFIG_PROP_USE_REPOSITORY_MANAGERS = 202 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useRepositoryManagers"; 203 204 public static final boolean DEFAULT_USE_REPOSITORY_MANAGERS = false; 205 206 /** 207 * The basedir where to store filter files. If path is relative, it is resolved from local repository root. 208 * 209 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 210 * @configurationType {@link java.lang.String} 211 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 212 */ 213 public static final String CONFIG_PROP_BASEDIR = 214 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir"; 215 216 public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters"; 217 218 static final String PREFIXES_FILE_PREFIX = "prefixes-"; 219 220 static final String PREFIXES_FILE_SUFFIX = ".txt"; 221 222 private final Logger logger = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class); 223 224 private final Supplier<MetadataResolver> metadataResolver; 225 226 private final Supplier<RemoteRepositoryManager> remoteRepositoryManager; 227 228 private final RepositoryLayoutProvider repositoryLayoutProvider; 229 230 @Inject 231 public PrefixesRemoteRepositoryFilterSource( 232 RepositoryKeyFunctionFactory repositoryKeyFunctionFactory, 233 Supplier<MetadataResolver> metadataResolver, 234 Supplier<RemoteRepositoryManager> remoteRepositoryManager, 235 RepositoryLayoutProvider repositoryLayoutProvider) { 236 super(repositoryKeyFunctionFactory); 237 this.metadataResolver = requireNonNull(metadataResolver); 238 this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager); 239 this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider); 240 } 241 242 private static final Object PREFIXES_KEY = Keys.of(PrefixesRemoteRepositoryFilterSource.class, "prefixes"); 243 244 @SuppressWarnings("unchecked") 245 private ConcurrentMap<RemoteRepository, PrefixTree> prefixes(RepositorySystemSession session) { 246 return (ConcurrentMap<RemoteRepository, PrefixTree>) 247 session.getData().computeIfAbsent(PREFIXES_KEY, ConcurrentHashMap::new); 248 } 249 250 private static final Object LAYOUTS_KEY = Keys.of(PrefixesRemoteRepositoryFilterSource.class, "layouts"); 251 252 @SuppressWarnings("unchecked") 253 private ConcurrentMap<RemoteRepository, RepositoryLayout> layouts(RepositorySystemSession session) { 254 return (ConcurrentMap<RemoteRepository, RepositoryLayout>) 255 session.getData().computeIfAbsent(LAYOUTS_KEY, ConcurrentHashMap::new); 256 } 257 258 @Override 259 protected boolean isEnabled(RepositorySystemSession session) { 260 return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED) 261 && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED); 262 } 263 264 private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) { 265 if (isEnabled(session)) { 266 return ConfigUtils.getBoolean( 267 session, 268 DEFAULT_ENABLED, 269 CONFIG_PROP_ENABLED + "." + remoteRepository.getId(), 270 CONFIG_PROP_ENABLED + ".*") 271 && !ConfigUtils.getBoolean( 272 session, 273 DEFAULT_SKIPPED, 274 CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(), 275 CONFIG_PROP_SKIPPED + ".*"); 276 } 277 return false; 278 } 279 280 @Override 281 public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { 282 if (isEnabled(session)) { 283 return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false)); 284 } 285 return null; 286 } 287 288 /** 289 * Caches layout instances for remote repository. In case of unknown layout it returns {@link #NOT_SUPPORTED}. 290 * 291 * @return the layout instance or {@link #NOT_SUPPORTED} if layout not supported. 292 */ 293 private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) { 294 return layouts(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> { 295 try { 296 return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository); 297 } catch (NoRepositoryLayoutException e) { 298 return NOT_SUPPORTED; 299 } 300 }); 301 } 302 303 private PrefixTree cachePrefixTree( 304 RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) { 305 return prefixes(session) 306 .computeIfAbsent( 307 normalizeRemoteRepository(session, remoteRepository), 308 r -> loadPrefixTree(session, basedir, remoteRepository)); 309 } 310 311 private static final PrefixTree DISABLED = new PrefixTree("disabled"); 312 private static final PrefixTree ENABLED_NO_INPUT = new PrefixTree("enabled-no-input"); 313 314 private PrefixTree loadPrefixTree( 315 RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) { 316 if (isRepositoryFilteringEnabled(session, remoteRepository)) { 317 String origin = "user-provided"; 318 Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository); 319 if (filePath == null) { 320 if (!supportedResolvePrefixesForRemoteRepository(session, remoteRepository)) { 321 origin = "unsupported"; 322 } else { 323 origin = "auto-discovered"; 324 filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository); 325 } 326 } 327 if (filePath != null) { 328 PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath); 329 if (prefixesSource.valid()) { 330 logger.debug( 331 "Loaded prefixes for remote repository {} from {} file '{}'", 332 prefixesSource.origin().getId(), 333 origin, 334 prefixesSource.path()); 335 PrefixTree prefixTree = new PrefixTree(""); 336 int rules = prefixTree.loadNodes(prefixesSource.entries().stream()); 337 logger.info( 338 "Loaded {} {} prefixes for remote repository {} ({})", 339 rules, 340 origin, 341 prefixesSource.origin().getId(), 342 prefixesSource.path().getFileName()); 343 return prefixTree; 344 } else { 345 logger.info( 346 "Rejected {} prefixes for remote repository {} ({}): {}", 347 origin, 348 prefixesSource.origin().getId(), 349 prefixesSource.path().getFileName(), 350 prefixesSource.message()); 351 } 352 } 353 logger.debug("Prefix file for remote repository {} not available", remoteRepository); 354 return ENABLED_NO_INPUT; 355 } 356 logger.debug("Prefix file for remote repository {} disabled", remoteRepository); 357 return DISABLED; 358 } 359 360 private Path resolvePrefixesFromLocalConfiguration( 361 RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) { 362 Path filePath = 363 baseDir.resolve(PREFIXES_FILE_PREFIX + repositoryKey(session, remoteRepository) + PREFIXES_FILE_SUFFIX); 364 if (Files.isReadable(filePath)) { 365 return filePath; 366 } else { 367 return null; 368 } 369 } 370 371 private boolean supportedResolvePrefixesForRemoteRepository( 372 RepositorySystemSession session, RemoteRepository remoteRepository) { 373 if (!ConfigUtils.getBoolean( 374 session, 375 DEFAULT_RESOLVE_PREFIX_FILES, 376 CONFIG_PROP_RESOLVE_PREFIX_FILES + "." + remoteRepository.getId(), 377 CONFIG_PROP_RESOLVE_PREFIX_FILES)) { 378 return false; 379 } 380 if (remoteRepository.isRepositoryManager()) { 381 return ConfigUtils.getBoolean( 382 session, DEFAULT_USE_REPOSITORY_MANAGERS, CONFIG_PROP_USE_REPOSITORY_MANAGERS); 383 } else { 384 return remoteRepository.getMirroredRepositories().isEmpty() 385 || ConfigUtils.getBoolean( 386 session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES); 387 } 388 } 389 390 private Path resolvePrefixesFromRemoteRepository( 391 RepositorySystemSession session, RemoteRepository remoteRepository) { 392 MetadataResolver mr = metadataResolver.get(); 393 RemoteRepositoryManager rm = remoteRepositoryManager.get(); 394 if (mr != null && rm != null) { 395 // retrieve prefix as metadata from repository 396 MetadataResult result = mr.resolveMetadata( 397 new DefaultRepositorySystemSession(session) 398 .setTransferListener(null) 399 .setConfigProperty(CONFIG_PROP_SKIPPED, Boolean.TRUE.toString()), 400 Collections.singleton(new MetadataRequest( 401 new DefaultMetadata(PREFIX_FILE_TYPE, Metadata.Nature.RELEASE_OR_SNAPSHOT)) 402 .setRepository(remoteRepository) 403 .setDeleteLocalCopyIfMissing(true) 404 .setFavorLocalRepository(true))) 405 .get(0); 406 if (result.isResolved()) { 407 return result.getMetadata().getPath(); 408 } else { 409 return null; 410 } 411 } 412 return null; 413 } 414 415 private class PrefixesFilter implements RemoteRepositoryFilter { 416 private final RepositorySystemSession session; 417 private final Path basedir; 418 419 private PrefixesFilter(RepositorySystemSession session, Path basedir) { 420 this.session = session; 421 this.basedir = basedir; 422 } 423 424 @Override 425 public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { 426 RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); 427 if (repositoryLayout == NOT_SUPPORTED) { 428 return result(true, NAME, "Unsupported layout: " + remoteRepository); 429 } 430 return acceptPrefix( 431 remoteRepository, 432 repositoryLayout.getLocation(artifact, false).getPath()); 433 } 434 435 @Override 436 public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { 437 RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); 438 if (repositoryLayout == NOT_SUPPORTED) { 439 return result(true, NAME, "Unsupported layout: " + remoteRepository); 440 } 441 return acceptPrefix( 442 remoteRepository, 443 repositoryLayout.getLocation(metadata, false).getPath()); 444 } 445 446 private Result acceptPrefix(RemoteRepository repository, String path) { 447 PrefixTree prefixTree = cachePrefixTree(session, basedir, repository); 448 if (prefixTree == DISABLED) { 449 return result(true, NAME, "Disabled"); 450 } else if (prefixTree == ENABLED_NO_INPUT) { 451 return result( 452 ConfigUtils.getBoolean( 453 session, 454 DEFAULT_NO_INPUT_OUTCOME, 455 CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(), 456 CONFIG_PROP_NO_INPUT_OUTCOME), 457 NAME, 458 "No input available"); 459 } 460 boolean accepted = prefixTree.acceptedPath(path); 461 return result( 462 accepted, 463 NAME, 464 accepted 465 ? "Path " + path + " allowed from " + repository.getId() 466 : "Path " + path + " NOT allowed from " + repository.getId()); 467 } 468 } 469 470 private static final RepositoryLayout NOT_SUPPORTED = new RepositoryLayout() { 471 @Override 472 public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() { 473 throw new UnsupportedOperationException(); 474 } 475 476 @Override 477 public boolean hasChecksums(Artifact artifact) { 478 throw new UnsupportedOperationException(); 479 } 480 481 @Override 482 public URI getLocation(Artifact artifact, boolean upload) { 483 throw new UnsupportedOperationException(); 484 } 485 486 @Override 487 public URI getLocation(Metadata metadata, boolean upload) { 488 throw new UnsupportedOperationException(); 489 } 490 491 @Override 492 public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) { 493 throw new UnsupportedOperationException(); 494 } 495 496 @Override 497 public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) { 498 throw new UnsupportedOperationException(); 499 } 500 }; 501}