001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.impl.filter;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.BufferedReader;
026import java.io.FileNotFoundException;
027import java.io.IOException;
028import java.io.UncheckedIOException;
029import java.net.URI;
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;
038import java.util.concurrent.ConcurrentMap;
039
040import org.eclipse.aether.RepositorySystemSession;
041import org.eclipse.aether.artifact.Artifact;
042import org.eclipse.aether.metadata.Metadata;
043import org.eclipse.aether.repository.RemoteRepository;
044import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
045import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
046import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
047import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
048import org.eclipse.aether.transfer.NoRepositoryLayoutException;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052import static java.util.Objects.requireNonNull;
053import static java.util.stream.Collectors.toList;
054
055/**
056 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
057 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
058 * path with no corresponding prefix present in this file is filtered out.
059 * <p>
060 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
061 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
062 * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
063 * <p>
064 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
065 * <p>
066 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
067 * noticed.
068 * <p>
069 * Examples of published prefix files:
070 * <ul>
071 *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
072 *     <li>Apache Releases:
073 *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
074 * </ul>
075 *
076 * @since 1.9.0
077 */
078@Singleton
079@Named(PrefixesRemoteRepositoryFilterSource.NAME)
080public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport {
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    @Inject
092    public PrefixesRemoteRepositoryFilterSource(RepositoryLayoutProvider repositoryLayoutProvider) {
093        super(NAME);
094        this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider);
095    }
096
097    @SuppressWarnings("unchecked")
098    private ConcurrentMap<RemoteRepository, Node> prefixes(RepositorySystemSession session) {
099        return (ConcurrentMap<RemoteRepository, Node>)
100                session.getData().computeIfAbsent(getClass().getName() + ".prefixes", ConcurrentHashMap::new);
101    }
102
103    @SuppressWarnings("unchecked")
104    private ConcurrentMap<RemoteRepository, RepositoryLayout> layouts(RepositorySystemSession session) {
105        return (ConcurrentMap<RemoteRepository, RepositoryLayout>)
106                session.getData().computeIfAbsent(getClass().getName() + ".layouts", ConcurrentHashMap::new);
107    }
108
109    @Override
110    public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
111        if (isEnabled(session)) {
112            return new PrefixesFilter(session, getBasedir(session, false));
113        }
114        return null;
115    }
116
117    /**
118     * Caches layout instances for remote repository. In case of unknown layout it returns {@link #NOT_SUPPORTED}.
119     *
120     * @return the layout instance of {@link #NOT_SUPPORTED} if layout not supported.
121     */
122    private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) {
123        return layouts(session).computeIfAbsent(remoteRepository, r -> {
124            try {
125                return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository);
126            } catch (NoRepositoryLayoutException e) {
127                return NOT_SUPPORTED;
128            }
129        });
130    }
131
132    /**
133     * Caches prefixes instances for remote repository.
134     */
135    private Node cacheNode(RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) {
136        return prefixes(session)
137                .computeIfAbsent(remoteRepository, r -> loadRepositoryPrefixes(basedir, remoteRepository));
138    }
139
140    /**
141     * Loads prefixes file and preprocesses it into {@link Node} instance.
142     */
143    private Node loadRepositoryPrefixes(Path baseDir, RemoteRepository remoteRepository) {
144        Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX + remoteRepository.getId() + PREFIXES_FILE_SUFFIX);
145        if (Files.isReadable(filePath)) {
146            try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
147                LOGGER.debug(
148                        "Loading prefixes for remote repository {} from file '{}'", remoteRepository.getId(), filePath);
149                Node root = new Node("");
150                String prefix;
151                int lines = 0;
152                while ((prefix = reader.readLine()) != null) {
153                    if (!prefix.startsWith("#") && !prefix.trim().isEmpty()) {
154                        lines++;
155                        Node currentNode = root;
156                        for (String element : elementsOf(prefix)) {
157                            currentNode = currentNode.addSibling(element);
158                        }
159                    }
160                }
161                LOGGER.info("Loaded {} prefixes for remote repository {}", lines, remoteRepository.getId());
162                return root;
163            } catch (FileNotFoundException e) {
164                // strange: we tested for it above, still, we should not fail
165            } catch (IOException e) {
166                throw new UncheckedIOException(e);
167            }
168        }
169        LOGGER.debug("Prefix file for remote repository {} not found at '{}'", remoteRepository, filePath);
170        return NOT_PRESENT_NODE;
171    }
172
173    private class PrefixesFilter implements RemoteRepositoryFilter {
174        private final RepositorySystemSession session;
175
176        private final Path basedir;
177
178        private PrefixesFilter(RepositorySystemSession session, Path basedir) {
179            this.session = session;
180            this.basedir = basedir;
181        }
182
183        @Override
184        public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
185            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
186            if (repositoryLayout == NOT_SUPPORTED) {
187                return new SimpleResult(true, "Unsupported layout: " + remoteRepository);
188            }
189            return acceptPrefix(
190                    remoteRepository,
191                    repositoryLayout.getLocation(artifact, false).getPath());
192        }
193
194        @Override
195        public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
196            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
197            if (repositoryLayout == NOT_SUPPORTED) {
198                return new SimpleResult(true, "Unsupported layout: " + remoteRepository);
199            }
200            return acceptPrefix(
201                    remoteRepository,
202                    repositoryLayout.getLocation(metadata, false).getPath());
203        }
204
205        private Result acceptPrefix(RemoteRepository remoteRepository, String path) {
206            Node root = cacheNode(session, basedir, remoteRepository);
207            if (NOT_PRESENT_NODE == root) {
208                return NOT_PRESENT_RESULT;
209            }
210            List<String> prefix = new ArrayList<>();
211            final List<String> pathElements = elementsOf(path);
212            Node currentNode = root;
213            for (String pathElement : pathElements) {
214                prefix.add(pathElement);
215                currentNode = currentNode.getSibling(pathElement);
216                if (currentNode == null || currentNode.isLeaf()) {
217                    break;
218                }
219            }
220            if (currentNode != null && currentNode.isLeaf()) {
221                return new SimpleResult(
222                        true, "Prefix " + String.join("/", prefix) + " allowed from " + remoteRepository);
223            } else {
224                return new SimpleResult(
225                        false, "Prefix " + String.join("/", prefix) + " NOT allowed from " + remoteRepository);
226            }
227        }
228    }
229
230    private static final Node NOT_PRESENT_NODE = new Node("not-present-node");
231
232    private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT =
233            new SimpleResult(true, "Prefix file not present");
234
235    private static final RepositoryLayout NOT_SUPPORTED = new RepositoryLayout() {
236        @Override
237        public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
238            throw new UnsupportedOperationException();
239        }
240
241        @Override
242        public boolean hasChecksums(Artifact artifact) {
243            throw new UnsupportedOperationException();
244        }
245
246        @Override
247        public URI getLocation(Artifact artifact, boolean upload) {
248            throw new UnsupportedOperationException();
249        }
250
251        @Override
252        public URI getLocation(Metadata metadata, boolean upload) {
253            throw new UnsupportedOperationException();
254        }
255
256        @Override
257        public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
258            throw new UnsupportedOperationException();
259        }
260
261        @Override
262        public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
263            throw new UnsupportedOperationException();
264        }
265    };
266
267    private static class Node {
268        private final String name;
269
270        private final HashMap<String, Node> siblings;
271
272        private Node(String name) {
273            this.name = name;
274            this.siblings = new HashMap<>();
275        }
276
277        public String getName() {
278            return name;
279        }
280
281        public boolean isLeaf() {
282            return siblings.isEmpty();
283        }
284
285        public Node addSibling(String name) {
286            Node sibling = siblings.get(name);
287            if (sibling == null) {
288                sibling = new Node(name);
289                siblings.put(name, sibling);
290            }
291            return sibling;
292        }
293
294        public Node getSibling(String name) {
295            return siblings.get(name);
296        }
297    }
298
299    private static List<String> elementsOf(final String path) {
300        return Arrays.stream(path.split("/"))
301                .filter(e -> e != null && !e.isEmpty())
302                .collect(toList());
303    }
304}