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.BufferedReader; 026import java.io.IOException; 027import java.io.UncheckedIOException; 028import java.nio.charset.StandardCharsets; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.util.ArrayList; 032import java.util.Collections; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036import java.util.TreeSet; 037import java.util.concurrent.ConcurrentHashMap; 038import java.util.concurrent.atomic.AtomicBoolean; 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.metadata.Metadata; 045import org.eclipse.aether.repository.RemoteRepository; 046import org.eclipse.aether.resolution.ArtifactResult; 047import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; 048import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor; 049import org.eclipse.aether.util.ConfigUtils; 050import org.eclipse.aether.util.FileUtils; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054import static java.util.Objects.requireNonNull; 055 056/** 057 * Remote repository filter source filtering on G coordinate. It is backed by a file that lists all allowed groupIds 058 * and groupId not present in this file are filtered out. 059 * <p> 060 * The file can be authored manually: format is one groupId per line, comments starting with "#" (hash) amd empty lines 061 * for structuring are supported. 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. 064 * <p> 065 * The groupId file is expected on path "${basedir}/groupId-${repository.id}.txt". 066 * <p> 067 * The groupId file once loaded are cached in component, so in-flight groupId file change during component existence 068 * are NOT noticed. 069 * 070 * @since 1.9.0 071 */ 072@Singleton 073@Named(GroupIdRemoteRepositoryFilterSource.NAME) 074public final class GroupIdRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport 075 implements ArtifactResolverPostProcessor { 076 public static final String NAME = "groupId"; 077 078 private static final String CONFIG_PROPS_PREFIX = 079 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + "."; 080 081 /** 082 * Is filter enabled? 083 * 084 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 085 * @configurationType {@link java.lang.Boolean} 086 * @configurationDefaultValue false 087 */ 088 public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME; 089 090 /** 091 * The basedir where to store filter files. If path is relative, it is resolved from local repository root. 092 * 093 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 094 * @configurationType {@link java.lang.String} 095 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 096 */ 097 public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir"; 098 099 public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters"; 100 101 /** 102 * Should filter go into "record" mode (and collect encountered artifacts)? 103 * 104 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 105 * @configurationType {@link java.lang.Boolean} 106 * @configurationDefaultValue false 107 */ 108 public static final String CONFIG_PROP_RECORD = CONFIG_PROPS_PREFIX + "record"; 109 110 static final String GROUP_ID_FILE_PREFIX = "groupId-"; 111 112 static final String GROUP_ID_FILE_SUFFIX = ".txt"; 113 114 private static final Logger LOGGER = LoggerFactory.getLogger(GroupIdRemoteRepositoryFilterSource.class); 115 116 private final RepositorySystemLifecycle repositorySystemLifecycle; 117 118 private final ConcurrentHashMap<Path, Set<String>> rules; 119 120 private final ConcurrentHashMap<Path, Boolean> changedRules; 121 122 private final AtomicBoolean onShutdownHandlerRegistered; 123 124 @Inject 125 public GroupIdRemoteRepositoryFilterSource(RepositorySystemLifecycle repositorySystemLifecycle) { 126 this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); 127 this.rules = new ConcurrentHashMap<>(); 128 this.changedRules = new ConcurrentHashMap<>(); 129 this.onShutdownHandlerRegistered = new AtomicBoolean(false); 130 } 131 132 @Override 133 protected boolean isEnabled(RepositorySystemSession session) { 134 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED); 135 } 136 137 @Override 138 public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { 139 if (isEnabled(session) && !isRecord(session)) { 140 return new GroupIdFilter(session); 141 } 142 return null; 143 } 144 145 @Override 146 public void postProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) { 147 if (isEnabled(session) && isRecord(session)) { 148 if (onShutdownHandlerRegistered.compareAndSet(false, true)) { 149 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines); 150 } 151 for (ArtifactResult artifactResult : artifactResults) { 152 if (artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository) { 153 Path filePath = filePath( 154 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false), 155 artifactResult.getRepository().getId()); 156 boolean newGroupId = rules.computeIfAbsent( 157 filePath, f -> Collections.synchronizedSet(new TreeSet<>())) 158 .add(artifactResult.getArtifact().getGroupId()); 159 if (newGroupId) { 160 changedRules.put(filePath, Boolean.TRUE); 161 } 162 } 163 } 164 } 165 } 166 167 /** 168 * Returns the groupId path. The file and parents may not exist, this method merely calculate the path. 169 */ 170 private Path filePath(Path basedir, String remoteRepositoryId) { 171 return basedir.resolve(GROUP_ID_FILE_PREFIX + remoteRepositoryId + GROUP_ID_FILE_SUFFIX); 172 } 173 174 private Set<String> cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) { 175 Path filePath = filePath( 176 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false), remoteRepository.getId()); 177 return rules.computeIfAbsent(filePath, r -> { 178 Set<String> rules = loadRepositoryRules(filePath); 179 if (rules != NOT_PRESENT) { 180 LOGGER.info("Loaded {} groupId for remote repository {}", rules.size(), remoteRepository.getId()); 181 } 182 return rules; 183 }); 184 } 185 186 private Set<String> loadRepositoryRules(Path filePath) { 187 if (Files.isReadable(filePath)) { 188 try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { 189 TreeSet<String> result = new TreeSet<>(); 190 String groupId; 191 while ((groupId = reader.readLine()) != null) { 192 if (!groupId.startsWith("#") && !groupId.trim().isEmpty()) { 193 result.add(groupId); 194 } 195 } 196 return Collections.unmodifiableSet(result); 197 } catch (IOException e) { 198 throw new UncheckedIOException(e); 199 } 200 } 201 return NOT_PRESENT; 202 } 203 204 private static final TreeSet<String> NOT_PRESENT = new TreeSet<>(); 205 206 private class GroupIdFilter implements RemoteRepositoryFilter { 207 private final RepositorySystemSession session; 208 209 private GroupIdFilter(RepositorySystemSession session) { 210 this.session = session; 211 } 212 213 @Override 214 public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { 215 return acceptGroupId(remoteRepository, artifact.getGroupId()); 216 } 217 218 @Override 219 public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { 220 return acceptGroupId(remoteRepository, metadata.getGroupId()); 221 } 222 223 private Result acceptGroupId(RemoteRepository remoteRepository, String groupId) { 224 Set<String> groupIds = cacheRules(session, remoteRepository); 225 if (NOT_PRESENT == groupIds) { 226 return NOT_PRESENT_RESULT; 227 } 228 229 if (groupIds.contains(groupId)) { 230 return new SimpleResult(true, "G:" + groupId + " allowed from " + remoteRepository); 231 } else { 232 return new SimpleResult(false, "G:" + groupId + " NOT allowed from " + remoteRepository); 233 } 234 } 235 } 236 237 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = 238 new SimpleResult(true, "GroupId file not present"); 239 240 /** 241 * Returns {@code true} if given session is recording. 242 */ 243 private boolean isRecord(RepositorySystemSession session) { 244 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_RECORD); 245 } 246 247 /** 248 * On-close handler that saves recorded rules, if any. 249 */ 250 private void saveRecordedLines() { 251 if (changedRules.isEmpty()) { 252 return; 253 } 254 255 ArrayList<Exception> exceptions = new ArrayList<>(); 256 for (Map.Entry<Path, Set<String>> entry : rules.entrySet()) { 257 Path filePath = entry.getKey(); 258 if (changedRules.get(filePath) != Boolean.TRUE) { 259 continue; 260 } 261 Set<String> recordedLines = entry.getValue(); 262 if (!recordedLines.isEmpty()) { 263 try { 264 TreeSet<String> result = new TreeSet<>(); 265 result.addAll(loadRepositoryRules(filePath)); 266 result.addAll(recordedLines); 267 268 LOGGER.info("Saving {} groupIds to '{}'", result.size(), filePath); 269 FileUtils.writeFileWithBackup(filePath, p -> Files.write(p, result)); 270 } catch (IOException e) { 271 exceptions.add(e); 272 } 273 } 274 } 275 MultiRuntimeException.mayThrow("session save groupIds failure", exceptions); 276 } 277}