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.ConcurrentMap; 039import java.util.concurrent.atomic.AtomicBoolean; 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.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.resolution.ArtifactResolverPostProcessor; 050import org.eclipse.aether.util.ConfigUtils; 051import org.eclipse.aether.util.FileUtils; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054 055import static java.util.Objects.requireNonNull; 056 057/** 058 * Remote repository filter source filtering on G coordinate. It is backed by a file that lists all allowed groupIds 059 * and groupId not present in this file are filtered out. 060 * <p> 061 * The file can be authored manually: format is one groupId per line, comments starting with "#" (hash) amd empty lines 062 * for structuring are supported. 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. 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 * @since 1.9.0 072 */ 073@Singleton 074@Named(GroupIdRemoteRepositoryFilterSource.NAME) 075public final class GroupIdRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport 076 implements ArtifactResolverPostProcessor { 077 public static final String NAME = "groupId"; 078 079 private static final String CONF_NAME_RECORD = "record"; 080 081 static final String GROUP_ID_FILE_PREFIX = "groupId-"; 082 083 static final String GROUP_ID_FILE_SUFFIX = ".txt"; 084 085 private static final Logger LOGGER = LoggerFactory.getLogger(GroupIdRemoteRepositoryFilterSource.class); 086 087 private final RepositorySystemLifecycle repositorySystemLifecycle; 088 089 @Inject 090 public GroupIdRemoteRepositoryFilterSource(RepositorySystemLifecycle repositorySystemLifecycle) { 091 super(NAME); 092 this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); 093 } 094 095 @SuppressWarnings("unchecked") 096 private ConcurrentMap<Path, Set<String>> rules(RepositorySystemSession session) { 097 return (ConcurrentMap<Path, Set<String>>) 098 session.getData().computeIfAbsent(getClass().getName() + ".rules", ConcurrentHashMap::new); 099 } 100 101 @SuppressWarnings("unchecked") 102 private ConcurrentMap<Path, Boolean> changedRules(RepositorySystemSession session) { 103 return (ConcurrentMap<Path, Boolean>) 104 session.getData().computeIfAbsent(getClass().getName() + ".changedRules", ConcurrentHashMap::new); 105 } 106 107 private AtomicBoolean onShutdownHandlerRegistered(RepositorySystemSession session) { 108 return (AtomicBoolean) session.getData() 109 .computeIfAbsent(getClass().getName() + ".onShutdownHandlerRegistered", AtomicBoolean::new); 110 } 111 112 @Override 113 public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { 114 if (isEnabled(session) && !isRecord(session)) { 115 return new GroupIdFilter(session); 116 } 117 return null; 118 } 119 120 @Override 121 public void postProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) { 122 if (isEnabled(session) && isRecord(session)) { 123 if (onShutdownHandlerRegistered(session).compareAndSet(false, true)) { 124 repositorySystemLifecycle.addOnSystemEndedHandler(() -> saveRecordedLines(session)); 125 } 126 for (ArtifactResult artifactResult : artifactResults) { 127 if (artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository) { 128 Path filePath = filePath( 129 getBasedir(session, false), 130 artifactResult.getRepository().getId()); 131 boolean newGroupId = rules(session) 132 .computeIfAbsent(filePath, f -> Collections.synchronizedSet(new TreeSet<>())) 133 .add(artifactResult.getArtifact().getGroupId()); 134 if (newGroupId) { 135 changedRules(session).put(filePath, Boolean.TRUE); 136 } 137 } 138 } 139 } 140 } 141 142 /** 143 * Returns the groupId path. The file and parents may not exist, this method merely calculate the path. 144 */ 145 private Path filePath(Path basedir, String remoteRepositoryId) { 146 return basedir.resolve(GROUP_ID_FILE_PREFIX + remoteRepositoryId + GROUP_ID_FILE_SUFFIX); 147 } 148 149 private Set<String> cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) { 150 Path filePath = filePath(getBasedir(session, false), remoteRepository.getId()); 151 return rules(session).computeIfAbsent(filePath, r -> { 152 Set<String> rules = loadRepositoryRules(filePath); 153 if (rules != NOT_PRESENT) { 154 LOGGER.info("Loaded {} groupId for remote repository {}", rules.size(), remoteRepository.getId()); 155 } 156 return rules; 157 }); 158 } 159 160 private Set<String> loadRepositoryRules(Path filePath) { 161 if (Files.isReadable(filePath)) { 162 try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { 163 TreeSet<String> result = new TreeSet<>(); 164 String groupId; 165 while ((groupId = reader.readLine()) != null) { 166 if (!groupId.startsWith("#") && !groupId.trim().isEmpty()) { 167 result.add(groupId); 168 } 169 } 170 return Collections.unmodifiableSet(result); 171 } catch (IOException e) { 172 throw new UncheckedIOException(e); 173 } 174 } 175 return NOT_PRESENT; 176 } 177 178 private static final TreeSet<String> NOT_PRESENT = new TreeSet<>(); 179 180 private class GroupIdFilter implements RemoteRepositoryFilter { 181 private final RepositorySystemSession session; 182 183 private GroupIdFilter(RepositorySystemSession session) { 184 this.session = session; 185 } 186 187 @Override 188 public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { 189 return acceptGroupId(remoteRepository, artifact.getGroupId()); 190 } 191 192 @Override 193 public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { 194 return acceptGroupId(remoteRepository, metadata.getGroupId()); 195 } 196 197 private Result acceptGroupId(RemoteRepository remoteRepository, String groupId) { 198 Set<String> groupIds = cacheRules(session, remoteRepository); 199 if (NOT_PRESENT == groupIds) { 200 return NOT_PRESENT_RESULT; 201 } 202 203 if (groupIds.contains(groupId)) { 204 return new SimpleResult(true, "G:" + groupId + " allowed from " + remoteRepository); 205 } else { 206 return new SimpleResult(false, "G:" + groupId + " NOT allowed from " + remoteRepository); 207 } 208 } 209 } 210 211 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = 212 new SimpleResult(true, "GroupId file not present"); 213 214 /** 215 * Returns {@code true} if given session is recording. 216 */ 217 private boolean isRecord(RepositorySystemSession session) { 218 return ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_RECORD)); 219 } 220 221 /** 222 * On-close handler that saves recorded rules, if any. 223 */ 224 private void saveRecordedLines(RepositorySystemSession session) { 225 if (changedRules(session).isEmpty()) { 226 return; 227 } 228 229 ArrayList<Exception> exceptions = new ArrayList<>(); 230 for (Map.Entry<Path, Set<String>> entry : rules(session).entrySet()) { 231 Path filePath = entry.getKey(); 232 if (changedRules(session).get(filePath) != Boolean.TRUE) { 233 continue; 234 } 235 Set<String> recordedLines = entry.getValue(); 236 if (!recordedLines.isEmpty()) { 237 try { 238 TreeSet<String> result = new TreeSet<>(); 239 result.addAll(loadRepositoryRules(filePath)); 240 result.addAll(recordedLines); 241 242 LOGGER.info("Saving {} groupIds to '{}'", result.size(), filePath); 243 FileUtils.writeFileWithBackup(filePath, p -> Files.write(p, result)); 244 } catch (IOException e) { 245 exceptions.add(e); 246 } 247 } 248 } 249 MultiRuntimeException.mayThrow("session save groupIds failure", exceptions); 250 } 251}