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.FileNotFoundException;
028import java.io.IOException;
029import java.io.UncheckedIOException;
030import java.nio.charset.StandardCharsets;
031import java.nio.file.Files;
032import java.nio.file.Path;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.HashMap;
036import java.util.List;
037import java.util.concurrent.ConcurrentHashMap;
038
039import org.eclipse.aether.RepositorySystemSession;
040import org.eclipse.aether.artifact.Artifact;
041import org.eclipse.aether.metadata.Metadata;
042import org.eclipse.aether.repository.RemoteRepository;
043import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
044import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
045import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
046import org.eclipse.aether.transfer.NoRepositoryLayoutException;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import static java.util.Objects.requireNonNull;
051import static java.util.stream.Collectors.toList;
052
053/**
054 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
055 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
056 * path with no corresponding prefix present in this file is filtered out.
057 * <p>
058 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
059 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
060 * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
061 * <p>
062 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
063 * <p>
064 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
065 * noticed.
066 * <p>
067 * Examples of published prefix files:
068 * <ul>
069 *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
070 *     <li>Apache Releases:
071 *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
072 * </ul>
073 *
074 * @since 1.9.0
075 */
076@Singleton
077@Named( PrefixesRemoteRepositoryFilterSource.NAME )
078public final class PrefixesRemoteRepositoryFilterSource
079        extends RemoteRepositoryFilterSourceSupport
080{
081    public static final String NAME = "prefixes";
082
083    static final String PREFIXES_FILE_PREFIX = "prefixes-";
084
085    static final String PREFIXES_FILE_SUFFIX = ".txt";
086
087    private static final Logger LOGGER = LoggerFactory.getLogger( PrefixesRemoteRepositoryFilterSource.class );
088
089    private final RepositoryLayoutProvider repositoryLayoutProvider;
090
091    private final ConcurrentHashMap<RemoteRepository, Node> prefixes;
092
093    private final ConcurrentHashMap<RemoteRepository, RepositoryLayout> layouts;
094
095    @Inject
096    public PrefixesRemoteRepositoryFilterSource( RepositoryLayoutProvider repositoryLayoutProvider )
097    {
098        super( NAME );
099        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}