001package org.eclipse.aether.internal.impl.filter; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import javax.inject.Inject; 023import javax.inject.Named; 024import javax.inject.Singleton; 025 026import java.io.BufferedReader; 027import java.io.IOException; 028import java.io.UncheckedIOException; 029import java.nio.charset.StandardCharsets; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037import java.util.TreeSet; 038import java.util.concurrent.ConcurrentHashMap; 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 076 extends RemoteRepositoryFilterSourceSupport 077 implements ArtifactResolverPostProcessor 078{ 079 public static final String NAME = "groupId"; 080 081 private static final String CONF_NAME_RECORD = "record"; 082 083 static final String GROUP_ID_FILE_PREFIX = "groupId-"; 084 085 static final String GROUP_ID_FILE_SUFFIX = ".txt"; 086 087 private static final Logger LOGGER = LoggerFactory.getLogger( GroupIdRemoteRepositoryFilterSource.class ); 088 089 private final RepositorySystemLifecycle repositorySystemLifecycle; 090 091 private final ConcurrentHashMap<Path, Set<String>> rules; 092 093 private final ConcurrentHashMap<Path, Boolean> changedRules; 094 095 private final AtomicBoolean onShutdownHandlerRegistered; 096 097 @Inject 098 public GroupIdRemoteRepositoryFilterSource( RepositorySystemLifecycle repositorySystemLifecycle ) 099 { 100 super( NAME ); 101 this.repositorySystemLifecycle = requireNonNull( repositorySystemLifecycle ); 102 this.rules = new ConcurrentHashMap<>(); 103 this.changedRules = new ConcurrentHashMap<>(); 104 this.onShutdownHandlerRegistered = new AtomicBoolean( false ); 105 } 106 107 @Override 108 public RemoteRepositoryFilter getRemoteRepositoryFilter( RepositorySystemSession session ) 109 { 110 if ( isEnabled( session ) && !isRecord( session ) ) 111 { 112 return new GroupIdFilter( session ); 113 } 114 return null; 115 } 116 117 @Override 118 public void postProcess( RepositorySystemSession session, List<ArtifactResult> artifactResults ) 119 { 120 if ( isEnabled( session ) && isRecord( session ) ) 121 { 122 if ( onShutdownHandlerRegistered.compareAndSet( false, true ) ) 123 { 124 repositorySystemLifecycle.addOnSystemEndedHandler( this::saveRecordedLines ); 125 } 126 for ( ArtifactResult artifactResult : artifactResults ) 127 { 128 if ( artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository ) 129 { 130 Path filePath = filePath( getBasedir( session, false ), 131 artifactResult.getRepository().getId() ); 132 boolean newGroupId = 133 rules.computeIfAbsent( filePath, f -> Collections.synchronizedSet( new TreeSet<>() ) ) 134 .add( artifactResult.getArtifact().getGroupId() ); 135 if ( newGroupId ) 136 { 137 changedRules.put( filePath, Boolean.TRUE ); 138 } 139 } 140 } 141 } 142 } 143 144 /** 145 * Returns the groupId path. The file and parents may not exist, this method merely calculate the path. 146 */ 147 private Path filePath( Path basedir, String remoteRepositoryId ) 148 { 149 return basedir.resolve( 150 GROUP_ID_FILE_PREFIX + remoteRepositoryId + GROUP_ID_FILE_SUFFIX ); 151 } 152 153 private Set<String> cacheRules( RepositorySystemSession session, 154 RemoteRepository remoteRepository ) 155 { 156 Path filePath = filePath( getBasedir( session, false ), remoteRepository.getId() ); 157 return rules.computeIfAbsent( filePath, r -> 158 { 159 Set<String> rules = loadRepositoryRules( filePath ); 160 if ( rules != NOT_PRESENT ) 161 { 162 LOGGER.info( "Loaded {} groupId for remote repository {}", rules.size(), 163 remoteRepository.getId() ); 164 } 165 return rules; 166 } 167 ); 168 } 169 170 private Set<String> loadRepositoryRules( Path filePath ) 171 { 172 if ( Files.isReadable( filePath ) ) 173 { 174 try ( BufferedReader reader = Files.newBufferedReader( filePath, StandardCharsets.UTF_8 ) ) 175 { 176 TreeSet<String> result = new TreeSet<>(); 177 String groupId; 178 while ( ( groupId = reader.readLine() ) != null ) 179 { 180 if ( !groupId.startsWith( "#" ) && !groupId.trim().isEmpty() ) 181 { 182 result.add( groupId ); 183 } 184 } 185 return Collections.unmodifiableSet( result ); 186 } 187 catch ( IOException e ) 188 { 189 throw new UncheckedIOException( e ); 190 } 191 } 192 return NOT_PRESENT; 193 } 194 195 private static final TreeSet<String> NOT_PRESENT = new TreeSet<>(); 196 197 private class GroupIdFilter implements RemoteRepositoryFilter 198 { 199 private final RepositorySystemSession session; 200 201 private GroupIdFilter( RepositorySystemSession session ) 202 { 203 this.session = session; 204 } 205 206 @Override 207 public Result acceptArtifact( RemoteRepository remoteRepository, Artifact artifact ) 208 { 209 return acceptGroupId( remoteRepository, artifact.getGroupId() ); 210 } 211 212 @Override 213 public Result acceptMetadata( RemoteRepository remoteRepository, Metadata metadata ) 214 { 215 return acceptGroupId( remoteRepository, metadata.getGroupId() ); 216 } 217 218 private Result acceptGroupId( RemoteRepository remoteRepository, String groupId ) 219 { 220 Set<String> groupIds = cacheRules( session, remoteRepository ); 221 if ( NOT_PRESENT == groupIds ) 222 { 223 return NOT_PRESENT_RESULT; 224 } 225 226 if ( groupIds.contains( groupId ) ) 227 { 228 return new SimpleResult( true, 229 "G:" + groupId + " allowed from " + remoteRepository ); 230 } 231 else 232 { 233 return new SimpleResult( false, 234 "G:" + groupId + " NOT allowed from " + remoteRepository ); 235 } 236 } 237 } 238 239 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult( 240 true, "GroupId file not present" ); 241 242 /** 243 * Returns {@code true} if given session is recording. 244 */ 245 private boolean isRecord( RepositorySystemSession session ) 246 { 247 return ConfigUtils.getBoolean( session, false, configPropKey( CONF_NAME_RECORD ) ); 248 } 249 250 /** 251 * On-close handler that saves recorded rules, if any. 252 */ 253 private void saveRecordedLines() 254 { 255 if ( changedRules.isEmpty() ) 256 { 257 return; 258 } 259 260 ArrayList<Exception> exceptions = new ArrayList<>(); 261 for ( Map.Entry<Path, Set<String>> entry : rules.entrySet() ) 262 { 263 Path filePath = entry.getKey(); 264 if ( changedRules.get( filePath ) != Boolean.TRUE ) 265 { 266 continue; 267 } 268 Set<String> recordedLines = entry.getValue(); 269 if ( !recordedLines.isEmpty() ) 270 { 271 try 272 { 273 TreeSet<String> result = new TreeSet<>(); 274 result.addAll( loadRepositoryRules( filePath ) ); 275 result.addAll( recordedLines ); 276 277 LOGGER.info( "Saving {} groupIds to '{}'", result.size(), filePath ); 278 FileUtils.writeFileWithBackup( filePath, p -> Files.write( p, result ) ); 279 } 280 catch ( IOException e ) 281 { 282 exceptions.add( e ); 283 } 284 } 285 } 286 MultiRuntimeException.mayThrow( "session save groupIds failure", exceptions ); 287 } 288}