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. In case of unknown layout it returns {@code null}.
116      *
117      * @return the layout instance of {@code null} if layout not supported.
118      */
119     private RepositoryLayout cacheLayout( RepositorySystemSession session, RemoteRepository remoteRepository )
120     {
121         return layouts.computeIfAbsent( remoteRepository, r ->
122         {
123             try
124             {
125                 return repositoryLayoutProvider.newRepositoryLayout( session, remoteRepository );
126             }
127             catch ( NoRepositoryLayoutException e )
128             {
129                 return null;
130             }
131         } );
132     }
133 
134     /**
135      * Caches prefixes instances for remote repository.
136      */
137     private Node cacheNode( Path basedir,
138                             RemoteRepository remoteRepository )
139     {
140         return prefixes.computeIfAbsent( remoteRepository, r -> loadRepositoryPrefixes( basedir, remoteRepository ) );
141     }
142 
143     /**
144      * Loads prefixes file and preprocesses it into {@link Node} instance.
145      */
146     private Node loadRepositoryPrefixes( Path baseDir, RemoteRepository remoteRepository )
147     {
148         Path filePath = baseDir.resolve( PREFIXES_FILE_PREFIX + remoteRepository.getId() + PREFIXES_FILE_SUFFIX );
149         if ( Files.isReadable( filePath ) )
150         {
151             try ( BufferedReader reader = Files.newBufferedReader( filePath, StandardCharsets.UTF_8 ) )
152             {
153                 LOGGER.debug( "Loading prefixes for remote repository {} from file '{}'", remoteRepository.getId(),
154                         filePath );
155                 Node root = new Node( "" );
156                 String prefix;
157                 int lines = 0;
158                 while ( ( prefix = reader.readLine() ) != null )
159                 {
160                     if ( !prefix.startsWith( "#" ) && !prefix.trim().isEmpty() )
161                     {
162                         lines++;
163                         Node currentNode = root;
164                         for ( String element : elementsOf( prefix ) )
165                         {
166                             currentNode = currentNode.addSibling( element );
167                         }
168                     }
169                 }
170                 LOGGER.info( "Loaded {} prefixes for remote repository {}", lines, remoteRepository.getId() );
171                 return root;
172             }
173             catch ( FileNotFoundException e )
174             {
175                 // strange: we tested for it above, still, we should not fail
176             }
177             catch ( IOException e )
178             {
179                 throw new UncheckedIOException( e );
180             }
181         }
182         LOGGER.debug( "Prefix file for remote repository {} not found at '{}'", remoteRepository, filePath );
183         return NOT_PRESENT_NODE;
184     }
185 
186     private class PrefixesFilter implements RemoteRepositoryFilter
187     {
188         private final RepositorySystemSession session;
189 
190         private final Path basedir;
191 
192         private PrefixesFilter( RepositorySystemSession session, Path basedir )
193         {
194             this.session = session;
195             this.basedir = basedir;
196         }
197 
198         @Override
199         public Result acceptArtifact( RemoteRepository remoteRepository, Artifact artifact )
200         {
201             RepositoryLayout repositoryLayout = cacheLayout( session, remoteRepository );
202             if ( repositoryLayout == null )
203             {
204                 return new SimpleResult( true, "Unsupported layout: " + remoteRepository );
205             }
206             return acceptPrefix( remoteRepository,
207                     repositoryLayout.getLocation( artifact, false ).getPath() );
208         }
209 
210         @Override
211         public Result acceptMetadata( RemoteRepository remoteRepository, Metadata metadata )
212         {
213             RepositoryLayout repositoryLayout = cacheLayout( session, remoteRepository );
214             if ( repositoryLayout == null )
215             {
216                 return new SimpleResult( true, "Unsupported layout: " + remoteRepository );
217             }
218             return acceptPrefix( remoteRepository,
219                     repositoryLayout.getLocation( metadata, false ).getPath() );
220         }
221 
222         private Result acceptPrefix( RemoteRepository remoteRepository, String path )
223         {
224             Node root = cacheNode( basedir, remoteRepository );
225             if ( NOT_PRESENT_NODE == root )
226             {
227                 return NOT_PRESENT_RESULT;
228             }
229             List<String> prefix = new ArrayList<>();
230             final List<String> pathElements = elementsOf( path );
231             Node currentNode = root;
232             for ( String pathElement : pathElements )
233             {
234                 prefix.add( pathElement );
235                 currentNode = currentNode.getSibling( pathElement );
236                 if ( currentNode == null || currentNode.isLeaf() )
237                 {
238                     break;
239                 }
240             }
241             if ( currentNode != null && currentNode.isLeaf() )
242             {
243                 return new SimpleResult( true, "Prefix "
244                         + String.join( "/", prefix ) + " allowed from " + remoteRepository );
245             }
246             else
247             {
248                 return new SimpleResult( false, "Prefix "
249                         + String.join( "/", prefix ) + " NOT allowed from " + remoteRepository );
250             }
251         }
252     }
253 
254     private static final Node NOT_PRESENT_NODE = new Node(
255             "not-present-node" );
256 
257     private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult(
258             true, "Prefix file not present" );
259 
260     private static class Node
261     {
262         private final String name;
263 
264         private final HashMap<String, Node> siblings;
265 
266         private Node( String name )
267         {
268             this.name = name;
269             this.siblings = new HashMap<>();
270         }
271 
272         public String getName()
273         {
274             return name;
275         }
276 
277         public boolean isLeaf()
278         {
279             return siblings.isEmpty();
280         }
281 
282         public Node addSibling( String name )
283         {
284             Node sibling = siblings.get( name );
285             if ( sibling == null )
286             {
287                 sibling = new Node( name );
288                 siblings.put( name, sibling );
289             }
290             return sibling;
291         }
292 
293         public Node getSibling( String name )
294         {
295             return siblings.get( name );
296         }
297     }
298 
299     private static List<String> elementsOf( final String path )
300     {
301         return Arrays.stream( path.split( "/" ) ).filter( e -> e != null && !e.isEmpty() ).collect( toList() );
302     }
303 }