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.FileNotFoundException;
28  import java.io.IOException;
29  import java.io.UncheckedIOException;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.concurrent.ConcurrentHashMap;
38  
39  import org.eclipse.aether.RepositorySystemSession;
40  import org.eclipse.aether.artifact.Artifact;
41  import org.eclipse.aether.metadata.Metadata;
42  import org.eclipse.aether.repository.RemoteRepository;
43  import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
44  import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
45  import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
46  import org.eclipse.aether.transfer.NoRepositoryLayoutException;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import static java.util.Objects.requireNonNull;
51  import static java.util.stream.Collectors.toList;
52  
53  /**
54   * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
55   * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
56   * path with no corresponding prefix present in this file is filtered out.
57   * <p>
58   * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
59   * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
60   * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
61   * <p>
62   * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
63   * <p>
64   * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
65   * noticed.
66   * <p>
67   * Examples of published prefix files:
68   * <ul>
69   *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
70   *     <li>Apache Releases:
71   *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
72   * </ul>
73   *
74   * @since 1.9.0
75   */
76  @Singleton
77  @Named( PrefixesRemoteRepositoryFilterSource.NAME )
78  public final class PrefixesRemoteRepositoryFilterSource
79          extends RemoteRepositoryFilterSourceSupport
80  {
81      public static final String NAME = "prefixes";
82  
83      static final String PREFIXES_FILE_PREFIX = "prefixes-";
84  
85      static final String PREFIXES_FILE_SUFFIX = ".txt";
86  
87      private static final Logger LOGGER = LoggerFactory.getLogger( PrefixesRemoteRepositoryFilterSource.class );
88  
89      private final RepositoryLayoutProvider repositoryLayoutProvider;
90  
91      private final ConcurrentHashMap<RemoteRepository, Node> prefixes;
92  
93      private final ConcurrentHashMap<RemoteRepository, RepositoryLayout> layouts;
94  
95      @Inject
96      public PrefixesRemoteRepositoryFilterSource( RepositoryLayoutProvider repositoryLayoutProvider )
97      {
98          super( NAME );
99          this.repositoryLayoutProvider = requireNonNull( repositoryLayoutProvider );
100         this.prefixes = new ConcurrentHashMap<>();
101         this.layouts = new ConcurrentHashMap<>();
102     }
103 
104     @Override
105     public RemoteRepositoryFilter getRemoteRepositoryFilter( RepositorySystemSession session )
106     {
107         if ( isEnabled( session ) )
108         {
109             return new PrefixesFilter( session, getBasedir( session, false ) );
110         }
111         return null;
112     }
113 
114     /**
115      * Caches layout instances for remote repository.
116      */
117     private RepositoryLayout cacheLayout( RepositorySystemSession session, RemoteRepository remoteRepository )
118     {
119         return layouts.computeIfAbsent( remoteRepository, r ->
120         {
121             try
122             {
123                 return repositoryLayoutProvider.newRepositoryLayout( session, remoteRepository );
124             }
125             catch ( NoRepositoryLayoutException e )
126             {
127                 throw new RuntimeException( e );
128             }
129         } );
130     }
131 
132     /**
133      * Caches prefixes instances for remote repository.
134      */
135     private Node cacheNode( Path basedir,
136                             RemoteRepository remoteRepository )
137     {
138         return prefixes.computeIfAbsent( remoteRepository, r -> loadRepositoryPrefixes( basedir, remoteRepository ) );
139     }
140 
141     /**
142      * Loads prefixes file and preprocesses it into {@link Node} instance.
143      */
144     private Node loadRepositoryPrefixes( Path baseDir, RemoteRepository remoteRepository )
145     {
146         Path filePath = baseDir.resolve( PREFIXES_FILE_PREFIX + remoteRepository.getId() + PREFIXES_FILE_SUFFIX );
147         if ( Files.isReadable( filePath ) )
148         {
149             try ( BufferedReader reader = Files.newBufferedReader( filePath, StandardCharsets.UTF_8 ) )
150             {
151                 LOGGER.debug( "Loading prefixes for remote repository {} from file '{}'", remoteRepository.getId(),
152                         filePath );
153                 Node root = new Node( "" );
154                 String prefix;
155                 int lines = 0;
156                 while ( ( prefix = reader.readLine() ) != null )
157                 {
158                     if ( !prefix.startsWith( "#" ) && !prefix.trim().isEmpty() )
159                     {
160                         lines++;
161                         Node currentNode = root;
162                         for ( String element : elementsOf( prefix ) )
163                         {
164                             currentNode = currentNode.addSibling( element );
165                         }
166                     }
167                 }
168                 LOGGER.info( "Loaded {} prefixes for remote repository {}", lines, remoteRepository.getId() );
169                 return root;
170             }
171             catch ( FileNotFoundException e )
172             {
173                 // strange: we tested for it above, still, we should not fail
174             }
175             catch ( IOException e )
176             {
177                 throw new UncheckedIOException( e );
178             }
179         }
180         LOGGER.debug( "Prefix file for remote repository {} not found at '{}'", remoteRepository, filePath );
181         return NOT_PRESENT_NODE;
182     }
183 
184     private class PrefixesFilter implements RemoteRepositoryFilter
185     {
186         private final RepositorySystemSession session;
187 
188         private final Path basedir;
189 
190         private PrefixesFilter( RepositorySystemSession session, Path basedir )
191         {
192             this.session = session;
193             this.basedir = basedir;
194         }
195 
196         @Override
197         public Result acceptArtifact( RemoteRepository remoteRepository, Artifact artifact )
198         {
199             return acceptPrefix( remoteRepository,
200                     cacheLayout( session, remoteRepository ).getLocation( artifact, false ).getPath() );
201         }
202 
203         @Override
204         public Result acceptMetadata( RemoteRepository remoteRepository, Metadata metadata )
205         {
206             return acceptPrefix( remoteRepository,
207                     cacheLayout( session, remoteRepository ).getLocation( metadata, false ).getPath() );
208         }
209 
210         private Result acceptPrefix( RemoteRepository remoteRepository, String path )
211         {
212             Node root = cacheNode( basedir, remoteRepository );
213             if ( NOT_PRESENT_NODE == root )
214             {
215                 return NOT_PRESENT_RESULT;
216             }
217             List<String> prefix = new ArrayList<>();
218             final List<String> pathElements = elementsOf( path );
219             Node currentNode = root;
220             for ( String pathElement : pathElements )
221             {
222                 prefix.add( pathElement );
223                 currentNode = currentNode.getSibling( pathElement );
224                 if ( currentNode == null || currentNode.isLeaf() )
225                 {
226                     break;
227                 }
228             }
229             if ( currentNode != null && currentNode.isLeaf() )
230             {
231                 return new SimpleResult( true, "Prefix "
232                         + String.join( "/", prefix ) + " allowed from " + remoteRepository );
233             }
234             else
235             {
236                 return new SimpleResult( false, "Prefix "
237                         + String.join( "/", prefix ) + " NOT allowed from " + remoteRepository );
238             }
239         }
240     }
241 
242     private static final Node NOT_PRESENT_NODE = new Node(
243             "not-present-node" );
244 
245     private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult(
246             true, "Prefix file not present" );
247 
248     private static class Node
249     {
250         private final String name;
251 
252         private final HashMap<String, Node> siblings;
253 
254         private Node( String name )
255         {
256             this.name = name;
257             this.siblings = new HashMap<>();
258         }
259 
260         public String getName()
261         {
262             return name;
263         }
264 
265         public boolean isLeaf()
266         {
267             return siblings.isEmpty();
268         }
269 
270         public Node addSibling( String name )
271         {
272             Node sibling = siblings.get( name );
273             if ( sibling == null )
274             {
275                 sibling = new Node( name );
276                 siblings.put( name, sibling );
277             }
278             return sibling;
279         }
280 
281         public Node getSibling( String name )
282         {
283             return siblings.get( name );
284         }
285     }
286 
287     private static List<String> elementsOf( final String path )
288     {
289         return Arrays.stream( path.split( "/" ) ).filter( e -> e != null && !e.isEmpty() ).collect( toList() );
290     }
291 }