View Javadoc
1   package org.eclipse.aether.internal.impl.filter;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import javax.inject.Inject;
23  import javax.inject.Named;
24  import javax.inject.Singleton;
25  
26  import java.io.BufferedReader;
27  import java.io.IOException;
28  import java.io.UncheckedIOException;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.TreeSet;
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.concurrent.atomic.AtomicBoolean;
40  
41  import org.eclipse.aether.MultiRuntimeException;
42  import org.eclipse.aether.RepositorySystemSession;
43  import org.eclipse.aether.artifact.Artifact;
44  import org.eclipse.aether.impl.RepositorySystemLifecycle;
45  import org.eclipse.aether.metadata.Metadata;
46  import org.eclipse.aether.repository.RemoteRepository;
47  import org.eclipse.aether.resolution.ArtifactResult;
48  import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
49  import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
50  import org.eclipse.aether.util.ConfigUtils;
51  import org.eclipse.aether.util.FileUtils;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import static java.util.Objects.requireNonNull;
56  
57  /**
58   * Remote repository filter source filtering on G coordinate. It is backed by a file that lists all allowed groupIds
59   * and groupId not present in this file are filtered out.
60   * <p>
61   * The file can be authored manually: format is one groupId per line, comments starting with "#" (hash) amd empty lines
62   * for structuring are supported. The file can also be pre-populated by "record" functionality of this filter.
63   * When "recording", this filter will not filter out anything, but will instead populate the file with all encountered
64   * groupIds.
65   * <p>
66   * The groupId file is expected on path "${basedir}/groupId-${repository.id}.txt".
67   * <p>
68   * The groupId file once loaded are cached in component, so in-flight groupId file change during component existence
69   * are NOT noticed.
70   *
71   * @since 1.9.0
72   */
73  @Singleton
74  @Named( GroupIdRemoteRepositoryFilterSource.NAME )
75  public final class GroupIdRemoteRepositoryFilterSource
76          extends RemoteRepositoryFilterSourceSupport
77          implements ArtifactResolverPostProcessor
78  {
79      public static final String NAME = "groupId";
80  
81      private static final String CONF_NAME_RECORD = "record";
82  
83      static final String GROUP_ID_FILE_PREFIX = "groupId-";
84  
85      static final String GROUP_ID_FILE_SUFFIX = ".txt";
86  
87      private static final Logger LOGGER = LoggerFactory.getLogger( GroupIdRemoteRepositoryFilterSource.class );
88  
89      private final RepositorySystemLifecycle repositorySystemLifecycle;
90  
91      private final ConcurrentHashMap<Path, Set<String>> rules;
92  
93      private final ConcurrentHashMap<Path, Boolean> changedRules;
94  
95      private final AtomicBoolean onShutdownHandlerRegistered;
96  
97      @Inject
98      public GroupIdRemoteRepositoryFilterSource( RepositorySystemLifecycle repositorySystemLifecycle )
99      {
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 }