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