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.io.IOException; 026import java.io.UncheckedIOException; 027import java.nio.charset.StandardCharsets; 028import java.nio.file.Files; 029import java.nio.file.Path; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.TreeSet; 035import java.util.concurrent.ConcurrentHashMap; 036import java.util.concurrent.ConcurrentMap; 037import java.util.concurrent.atomic.AtomicBoolean; 038import java.util.stream.Collectors; 039import java.util.stream.Stream; 040 041import org.eclipse.aether.Keys; 042import org.eclipse.aether.MultiRuntimeException; 043import org.eclipse.aether.RepositorySystemSession; 044import org.eclipse.aether.artifact.Artifact; 045import org.eclipse.aether.impl.RepositorySystemLifecycle; 046import org.eclipse.aether.internal.impl.filter.ruletree.GroupTree; 047import org.eclipse.aether.metadata.Metadata; 048import org.eclipse.aether.repository.RemoteRepository; 049import org.eclipse.aether.resolution.ArtifactResult; 050import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; 051import org.eclipse.aether.spi.io.PathProcessor; 052import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory; 053import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor; 054import org.eclipse.aether.util.ConfigUtils; 055import org.slf4j.Logger; 056import org.slf4j.LoggerFactory; 057 058import static java.util.Objects.requireNonNull; 059 060/** 061 * Remote repository filter source filtering on G coordinate. It is backed by a file that is parsed into {@link GroupTree}. 062 * <p> 063 * The file can be authored manually. The file can also be pre-populated by "record" functionality of this filter. 064 * When "recording", this filter will not filter out anything, but will instead populate the file with all encountered 065 * groupIds recorded as {@code =groupId}. The recorded file should be authored afterward to fine tune it, as there is 066 * no optimization in place (ie to look for smallest common parent groupId and alike). 067 * <p> 068 * The groupId file is expected on path "${basedir}/groupId-${repository.id}.txt". 069 * <p> 070 * The groupId file once loaded are cached in component, so in-flight groupId file change during component existence 071 * are NOT noticed. 072 * 073 * @see GroupTree 074 * 075 * @since 1.9.0 076 */ 077@Singleton 078@Named(GroupIdRemoteRepositoryFilterSource.NAME) 079public final class GroupIdRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport 080 implements ArtifactResolverPostProcessor { 081 public static final String NAME = "groupId"; 082 083 /** 084 * Configuration to enable the GroupId filter (enabled by default). Can be fine-tuned per repository using 085 * repository ID suffixes. 086 * <strong>Important:</strong> For this filter to take effect, you must provide configuration files. Without 087 * configuration files, the enabled filter remains dormant and does not interfere with resolution. 088 * <strong>Configuration Files:</strong> 089 * <ul> 090 * <li>Location: Directory specified by {@link #CONFIG_PROP_BASEDIR} (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li> 091 * <li>Naming: {@code groupId-$(repository.id).txt}</li> 092 * <li>Content: One groupId per line to allow/block from the repository</li> 093 * </ul> 094 * <strong>Recommended Setup (Per-Project):</strong> 095 * Use project-specific configuration to avoid repository ID clashes. Add to {@code .mvn/maven.config}: 096 * <pre> 097 * -Daether.remoteRepositoryFilter.groupId=true 098 * -Daether.remoteRepositoryFilter.groupId.basedir=${session.rootDirectory}/.mvn/rrf/ 099 * </pre> 100 * Then create {@code groupId-myrepoId.txt} files in the {@code .mvn/rrf/} directory and commit them to version control. 101 * 102 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 103 * @configurationType {@link java.lang.Boolean} 104 * @configurationRepoIdSuffix Yes 105 * @configurationDefaultValue {@link #DEFAULT_ENABLED} 106 */ 107 public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME; 108 109 public static final boolean DEFAULT_ENABLED = true; 110 111 /** 112 * Configuration to skip the GroupId filter for given request. This configuration is evaluated and if {@code true} 113 * the GroupId remote filter will not kick in. 114 * 115 * @since 2.0.14 116 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 117 * @configurationType {@link java.lang.Boolean} 118 * @configurationRepoIdSuffix Yes 119 * @configurationDefaultValue {@link #DEFAULT_SKIPPED} 120 */ 121 public static final String CONFIG_PROP_SKIPPED = 122 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped"; 123 124 public static final boolean DEFAULT_SKIPPED = false; 125 126 /** 127 * Determines what happens when the filter is enabled, but has no groupId file available for given remote repository 128 * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote 129 * repository when no groupId file is available. When set to {@code false}, the filter blocks all requests toward 130 * given remote repository when no groupId file is available. This setting allows repoId suffix, hence, can 131 * determine "global" or "repository targeted" behaviors. 132 * 133 * @since 2.0.14 134 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 135 * @configurationType {@link java.lang.Boolean} 136 * @configurationRepoIdSuffix Yes 137 * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME} 138 */ 139 public static final String CONFIG_PROP_NO_INPUT_OUTCOME = 140 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome"; 141 142 public static final boolean DEFAULT_NO_INPUT_OUTCOME = true; 143 144 /** 145 * The basedir where to store filter files. If path is relative, it is resolved from local repository root. 146 * 147 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 148 * @configurationType {@link java.lang.String} 149 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 150 */ 151 public static final String CONFIG_PROP_BASEDIR = 152 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir"; 153 154 public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters"; 155 156 /** 157 * Should filter go into "record" mode (and collect encountered artifacts)? 158 * 159 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 160 * @configurationType {@link java.lang.Boolean} 161 * @configurationDefaultValue false 162 */ 163 public static final String CONFIG_PROP_RECORD = 164 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".record"; 165 166 static final String GROUP_ID_FILE_PREFIX = "groupId-"; 167 168 static final String GROUP_ID_FILE_SUFFIX = ".txt"; 169 170 private final Logger logger = LoggerFactory.getLogger(GroupIdRemoteRepositoryFilterSource.class); 171 172 private final RepositorySystemLifecycle repositorySystemLifecycle; 173 174 private final PathProcessor pathProcessor; 175 176 @Inject 177 public GroupIdRemoteRepositoryFilterSource( 178 RepositoryKeyFunctionFactory repositoryKeyFunctionFactory, 179 RepositorySystemLifecycle repositorySystemLifecycle, 180 PathProcessor pathProcessor) { 181 super(repositoryKeyFunctionFactory); 182 this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); 183 this.pathProcessor = requireNonNull(pathProcessor); 184 } 185 186 private static final Object RULES = Keys.of(GroupIdRemoteRepositoryFilterSource.class, "rules"); 187 188 @SuppressWarnings("unchecked") 189 private ConcurrentMap<RemoteRepository, GroupTree> rules(RepositorySystemSession session) { 190 return (ConcurrentMap<RemoteRepository, GroupTree>) 191 session.getData().computeIfAbsent(RULES, ConcurrentHashMap::new); 192 } 193 194 private static final Object RULE_FILES = Keys.of(GroupIdRemoteRepositoryFilterSource.class, "ruleFiles"); 195 196 @SuppressWarnings("unchecked") 197 private ConcurrentMap<RemoteRepository, Path> ruleFiles(RepositorySystemSession session) { 198 return (ConcurrentMap<RemoteRepository, Path>) 199 session.getData().computeIfAbsent(RULE_FILES, ConcurrentHashMap::new); 200 } 201 202 private static final Object RECORDED_RULES = Keys.of(GroupIdRemoteRepositoryFilterSource.class, "recordedRules"); 203 204 @SuppressWarnings("unchecked") 205 private ConcurrentMap<RemoteRepository, Set<String>> recordedRules(RepositorySystemSession session) { 206 return (ConcurrentMap<RemoteRepository, Set<String>>) 207 session.getData().computeIfAbsent(RECORDED_RULES, ConcurrentHashMap::new); 208 } 209 210 private static final Object SHUTDOWN_HANDLER_REGISTERED = 211 Keys.of(GroupIdRemoteRepositoryFilterSource.class, "onShutdownHandlerRegistered"); 212 213 private AtomicBoolean onShutdownHandlerRegistered(RepositorySystemSession session) { 214 return (AtomicBoolean) session.getData().computeIfAbsent(SHUTDOWN_HANDLER_REGISTERED, AtomicBoolean::new); 215 } 216 217 @Override 218 protected boolean isEnabled(RepositorySystemSession session) { 219 return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED) 220 && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED); 221 } 222 223 private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) { 224 if (isEnabled(session)) { 225 return ConfigUtils.getBoolean( 226 session, 227 DEFAULT_ENABLED, 228 CONFIG_PROP_ENABLED + "." + remoteRepository.getId(), 229 CONFIG_PROP_ENABLED + ".*") 230 && !ConfigUtils.getBoolean( 231 session, 232 DEFAULT_SKIPPED, 233 CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(), 234 CONFIG_PROP_SKIPPED + ".*"); 235 } 236 return false; 237 } 238 239 @Override 240 public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { 241 if (isEnabled(session) && !isRecord(session)) { 242 return new GroupIdFilter(session); 243 } 244 return null; 245 } 246 247 @Override 248 public void postProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) { 249 if (isEnabled(session) && isRecord(session)) { 250 if (onShutdownHandlerRegistered(session).compareAndSet(false, true)) { 251 repositorySystemLifecycle.addOnSystemEndedHandler(() -> saveRecordedLines(session)); 252 } 253 for (ArtifactResult artifactResult : artifactResults) { 254 if (artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository) { 255 RemoteRepository remoteRepository = (RemoteRepository) artifactResult.getRepository(); 256 if (isRepositoryFilteringEnabled(session, remoteRepository)) { 257 ruleFile(session, remoteRepository); // populate it; needed for save 258 String line = "=" + artifactResult.getArtifact().getGroupId(); 259 RemoteRepository normalized = normalizeRemoteRepository(session, remoteRepository); 260 recordedRules(session) 261 .computeIfAbsent(normalized, k -> new TreeSet<>()) 262 .add(line); 263 rules(session) 264 .compute(normalized, (k, v) -> { 265 if (v == null || v == DISABLED || v == ENABLED_NO_INPUT) { 266 v = GroupTree.create("record"); 267 } 268 return v; 269 }) 270 .loadNode(line); 271 } 272 } 273 } 274 } 275 } 276 277 private Path ruleFile(RepositorySystemSession session, RemoteRepository remoteRepository) { 278 return ruleFiles(session) 279 .computeIfAbsent( 280 normalizeRemoteRepository(session, remoteRepository), 281 r -> getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false) 282 .resolve(GROUP_ID_FILE_PREFIX 283 + repositoryKey(session, remoteRepository) 284 + GROUP_ID_FILE_SUFFIX)); 285 } 286 287 private GroupTree cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) { 288 return rules(session) 289 .computeIfAbsent( 290 normalizeRemoteRepository(session, remoteRepository), r -> loadRepositoryRules(session, r)); 291 } 292 293 private static final GroupTree DISABLED = GroupTree.create("disabled"); 294 private static final GroupTree ENABLED_NO_INPUT = GroupTree.create("enabled-no-input"); 295 296 private GroupTree loadRepositoryRules(RepositorySystemSession session, RemoteRepository remoteRepository) { 297 if (isRepositoryFilteringEnabled(session, remoteRepository)) { 298 Path filePath = ruleFile(session, remoteRepository); 299 if (Files.isReadable(filePath)) { 300 try (Stream<String> lines = Files.lines(filePath, StandardCharsets.UTF_8)) { 301 GroupTree groupTree = 302 GroupTree.create(filePath.getFileName().toString()); 303 int rules = groupTree.loadNodes(lines); 304 logger.info("Loaded {} group rules for remote repository {}", rules, remoteRepository.getId()); 305 if (logger.isDebugEnabled()) { 306 groupTree.dump(""); 307 } 308 return groupTree; 309 } catch (IOException e) { 310 throw new UncheckedIOException(e); 311 } 312 } 313 logger.debug("Group rules file for remote repository {} not available", remoteRepository); 314 return ENABLED_NO_INPUT; 315 } 316 logger.debug("Group rules file for remote repository {} disabled", remoteRepository); 317 return DISABLED; 318 } 319 320 private class GroupIdFilter implements RemoteRepositoryFilter { 321 private final RepositorySystemSession session; 322 323 private GroupIdFilter(RepositorySystemSession session) { 324 this.session = session; 325 } 326 327 @Override 328 public Result acceptArtifact(RemoteRepository repository, Artifact artifact) { 329 return acceptGroupId(repository, artifact.getGroupId()); 330 } 331 332 @Override 333 public Result acceptMetadata(RemoteRepository repository, Metadata metadata) { 334 return acceptGroupId(repository, metadata.getGroupId()); 335 } 336 337 private Result acceptGroupId(RemoteRepository repository, String groupId) { 338 GroupTree groupTree = cacheRules(session, repository); 339 if (groupTree == DISABLED) { 340 return result(true, NAME, "Disabled"); 341 } else if (groupTree == ENABLED_NO_INPUT) { 342 return result( 343 ConfigUtils.getBoolean( 344 session, 345 DEFAULT_NO_INPUT_OUTCOME, 346 CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(), 347 CONFIG_PROP_NO_INPUT_OUTCOME), 348 NAME, 349 "No input available"); 350 } 351 352 boolean accepted = groupTree.acceptedGroupId(groupId); 353 return result( 354 accepted, 355 NAME, 356 accepted 357 ? "G:" + groupId + " allowed from " + repository.getId() 358 : "G:" + groupId + " NOT allowed from " + repository.getId()); 359 } 360 } 361 362 /** 363 * Returns {@code true} if given session is recording. 364 */ 365 private boolean isRecord(RepositorySystemSession session) { 366 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_RECORD); 367 } 368 369 /** 370 * On-close handler that saves recorded rules, if any. 371 */ 372 private void saveRecordedLines(RepositorySystemSession session) { 373 ArrayList<Exception> exceptions = new ArrayList<>(); 374 for (Map.Entry<RemoteRepository, Path> entry : ruleFiles(session).entrySet()) { 375 Set<String> recorded = recordedRules(session).get(entry.getKey()); 376 if (recorded != null && !recorded.isEmpty()) { 377 try { 378 ArrayList<String> result = new ArrayList<>(); 379 if (Files.isReadable(entry.getValue())) { 380 result.addAll(Files.readAllLines(entry.getValue())); 381 } 382 result.add("# Recorded entries"); 383 result.addAll(recorded); 384 logger.info("Saving {} groupIds to '{}'", result.size(), entry.getValue()); 385 pathProcessor.writeWithBackup( 386 entry.getValue(), result.stream().collect(Collectors.joining(System.lineSeparator()))); 387 } catch (IOException e) { 388 exceptions.add(e); 389 } 390 } 391 } 392 MultiRuntimeException.mayThrow("session save groupIds failure", exceptions); 393 } 394}