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