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.
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}