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