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.net.URI;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.Collections;
029import java.util.List;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.ConcurrentMap;
032import java.util.function.Supplier;
033
034import org.eclipse.aether.DefaultRepositorySystemSession;
035import org.eclipse.aether.Keys;
036import org.eclipse.aether.RepositorySystemSession;
037import org.eclipse.aether.artifact.Artifact;
038import org.eclipse.aether.impl.MetadataResolver;
039import org.eclipse.aether.impl.RemoteRepositoryManager;
040import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource;
041import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree;
042import org.eclipse.aether.metadata.DefaultMetadata;
043import org.eclipse.aether.metadata.Metadata;
044import org.eclipse.aether.repository.RemoteRepository;
045import org.eclipse.aether.resolution.MetadataRequest;
046import org.eclipse.aether.resolution.MetadataResult;
047import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
048import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
049import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
050import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
051import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
052import org.eclipse.aether.transfer.NoRepositoryLayoutException;
053import org.eclipse.aether.util.ConfigUtils;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import static java.util.Objects.requireNonNull;
058
059/**
060 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
061 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
062 * path with no corresponding prefix present in this file is filtered out.
063 * <p>
064 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
065 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
066 * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
067 * <p>
068 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
069 * <p>
070 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
071 * noticed.
072 * <p>
073 * Examples of published prefix files:
074 * <ul>
075 *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
076 *     <li>Apache Releases:
077 *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
078 * </ul>
079 *
080 * @since 1.9.0
081 */
082@Singleton
083@Named(PrefixesRemoteRepositoryFilterSource.NAME)
084public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport {
085    public static final String NAME = "prefixes";
086
087    static final String PREFIX_FILE_TYPE = ".meta/prefixes.txt";
088
089    /**
090     * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using
091     * repository ID suffixes.
092     * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without
093     * configuration files, the enabled filter remains dormant and does not interfere with resolution.
094     * <strong>Configuration File Resolution:</strong>
095     * <ol>
096     * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR}
097     *     (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li>
098     * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li>
099     * </ol>
100     * <strong>File Naming:</strong> {@code prefixes-$(repository.id).txt}
101     * <strong>Recommended Setup (Auto-Discovery with Override Capability):</strong>
102     * Start with auto-discovery, but prepare for project-specific overrides. Add to {@code .mvn/maven.config}:
103     * <pre>
104     * -Daether.remoteRepositoryFilter.prefixes=true
105     * -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/
106     * </pre>
107     * <strong>Initial setup:</strong> Don't provide any files - rely on auto-discovery as repositories are accessed.
108     * <strong>Override when needed:</strong> Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and
109     * commit to version control.
110     * <strong>Caching:</strong> Auto-discovered prefix files are cached in the local repository.
111     *
112     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
113     * @configurationType {@link java.lang.Boolean}
114     * @configurationRepoIdSuffix Yes
115     * @configurationDefaultValue {@link #DEFAULT_ENABLED}
116     */
117    public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
118
119    public static final boolean DEFAULT_ENABLED = true;
120
121    /**
122     * Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if {@code true}
123     * the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during
124     * discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while
125     * leaving configuration like {@link #CONFIG_PROP_ENABLED} still show the "real state".
126     *
127     * @since 2.0.14
128     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
129     * @configurationType {@link java.lang.Boolean}
130     * @configurationRepoIdSuffix Yes
131     * @configurationDefaultValue {@link #DEFAULT_SKIPPED}
132     */
133    public static final String CONFIG_PROP_SKIPPED =
134            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped";
135
136    public static final boolean DEFAULT_SKIPPED = false;
137
138    /**
139     * Determines what happens when the filter is enabled, but has no prefixes available for given remote repository
140     * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote
141     * repository when no prefixes are available. When set to {@code false}, the filter blocks all requests toward
142     * given remote repository when no prefixes are available. This setting allows repoId suffix, hence, can
143     * determine "global" or "repository targeted" behaviors.
144     *
145     * @since 2.0.14
146     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
147     * @configurationType {@link java.lang.Boolean}
148     * @configurationRepoIdSuffix Yes
149     * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME}
150     */
151    public static final String CONFIG_PROP_NO_INPUT_OUTCOME =
152            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome";
153
154    public static final boolean DEFAULT_NO_INPUT_OUTCOME = true;
155
156    /**
157     * Configuration to allow Prefixes file resolution attempt from remote repository as "auto discovery". If this
158     * configuration set to {@code false} only user-provided prefixes will be used.
159     *
160     * @since 2.0.14
161     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
162     * @configurationType {@link java.lang.Boolean}
163     * @configurationRepoIdSuffix Yes
164     * @configurationDefaultValue {@link #DEFAULT_RESOLVE_PREFIX_FILES}
165     */
166    public static final String CONFIG_PROP_RESOLVE_PREFIX_FILES =
167            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".resolvePrefixFiles";
168
169    public static final boolean DEFAULT_RESOLVE_PREFIX_FILES = true;
170
171    /**
172     * Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to
173     * work <em>Maven should be aware</em> that given remote repository is mirror and is usually backed by MRM. Given
174     * multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
175     * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
176     *
177     * @since 2.0.14
178     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
179     * @configurationType {@link java.lang.Boolean}
180     * @configurationRepoIdSuffix Yes
181     * @configurationDefaultValue {@link #DEFAULT_USE_MIRRORED_REPOSITORIES}
182     */
183    public static final String CONFIG_PROP_USE_MIRRORED_REPOSITORIES =
184            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useMirroredRepositories";
185
186    public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false;
187
188    /**
189     * Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to
190     * work <em>Maven should be aware</em> that given remote repository is backed by repository manager.
191     * Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
192     * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
193     * <em>Note: as of today, nothing sets this on remote repositories, but is added for future.</em>
194     *
195     * @since 2.0.14
196     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
197     * @configurationType {@link java.lang.Boolean}
198     * @configurationRepoIdSuffix Yes
199     * @configurationDefaultValue {@link #DEFAULT_USE_REPOSITORY_MANAGERS}
200     */
201    public static final String CONFIG_PROP_USE_REPOSITORY_MANAGERS =
202            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useRepositoryManagers";
203
204    public static final boolean DEFAULT_USE_REPOSITORY_MANAGERS = false;
205
206    /**
207     * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
208     *
209     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
210     * @configurationType {@link java.lang.String}
211     * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
212     */
213    public static final String CONFIG_PROP_BASEDIR =
214            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir";
215
216    public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
217
218    static final String PREFIXES_FILE_PREFIX = "prefixes-";
219
220    static final String PREFIXES_FILE_SUFFIX = ".txt";
221
222    private final Logger logger = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class);
223
224    private final Supplier<MetadataResolver> metadataResolver;
225
226    private final Supplier<RemoteRepositoryManager> remoteRepositoryManager;
227
228    private final RepositoryLayoutProvider repositoryLayoutProvider;
229
230    @Inject
231    public PrefixesRemoteRepositoryFilterSource(
232            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
233            Supplier<MetadataResolver> metadataResolver,
234            Supplier<RemoteRepositoryManager> remoteRepositoryManager,
235            RepositoryLayoutProvider repositoryLayoutProvider) {
236        super(repositoryKeyFunctionFactory);
237        this.metadataResolver = requireNonNull(metadataResolver);
238        this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager);
239        this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider);
240    }
241
242    private static final Object PREFIXES_KEY = Keys.of(PrefixesRemoteRepositoryFilterSource.class, "prefixes");
243
244    @SuppressWarnings("unchecked")
245    private ConcurrentMap<RemoteRepository, PrefixTree> prefixes(RepositorySystemSession session) {
246        return (ConcurrentMap<RemoteRepository, PrefixTree>)
247                session.getData().computeIfAbsent(PREFIXES_KEY, ConcurrentHashMap::new);
248    }
249
250    private static final Object LAYOUTS_KEY = Keys.of(PrefixesRemoteRepositoryFilterSource.class, "layouts");
251
252    @SuppressWarnings("unchecked")
253    private ConcurrentMap<RemoteRepository, RepositoryLayout> layouts(RepositorySystemSession session) {
254        return (ConcurrentMap<RemoteRepository, RepositoryLayout>)
255                session.getData().computeIfAbsent(LAYOUTS_KEY, ConcurrentHashMap::new);
256    }
257
258    @Override
259    protected boolean isEnabled(RepositorySystemSession session) {
260        return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED)
261                && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED);
262    }
263
264    private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) {
265        if (isEnabled(session)) {
266            return ConfigUtils.getBoolean(
267                            session,
268                            DEFAULT_ENABLED,
269                            CONFIG_PROP_ENABLED + "." + remoteRepository.getId(),
270                            CONFIG_PROP_ENABLED + ".*")
271                    && !ConfigUtils.getBoolean(
272                            session,
273                            DEFAULT_SKIPPED,
274                            CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(),
275                            CONFIG_PROP_SKIPPED + ".*");
276        }
277        return false;
278    }
279
280    @Override
281    public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
282        if (isEnabled(session)) {
283            return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false));
284        }
285        return null;
286    }
287
288    /**
289     * Caches layout instances for remote repository. In case of unknown layout it returns {@link #NOT_SUPPORTED}.
290     *
291     * @return the layout instance or {@link #NOT_SUPPORTED} if layout not supported.
292     */
293    private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) {
294        return layouts(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> {
295            try {
296                return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository);
297            } catch (NoRepositoryLayoutException e) {
298                return NOT_SUPPORTED;
299            }
300        });
301    }
302
303    private PrefixTree cachePrefixTree(
304            RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) {
305        return prefixes(session)
306                .computeIfAbsent(
307                        normalizeRemoteRepository(session, remoteRepository),
308                        r -> loadPrefixTree(session, basedir, remoteRepository));
309    }
310
311    private static final PrefixTree DISABLED = new PrefixTree("disabled");
312    private static final PrefixTree ENABLED_NO_INPUT = new PrefixTree("enabled-no-input");
313
314    private PrefixTree loadPrefixTree(
315            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
316        if (isRepositoryFilteringEnabled(session, remoteRepository)) {
317            String origin = "user-provided";
318            Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository);
319            if (filePath == null) {
320                if (!supportedResolvePrefixesForRemoteRepository(session, remoteRepository)) {
321                    origin = "unsupported";
322                } else {
323                    origin = "auto-discovered";
324                    filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository);
325                }
326            }
327            if (filePath != null) {
328                PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath);
329                if (prefixesSource.valid()) {
330                    logger.debug(
331                            "Loaded prefixes for remote repository {} from {} file '{}'",
332                            prefixesSource.origin().getId(),
333                            origin,
334                            prefixesSource.path());
335                    PrefixTree prefixTree = new PrefixTree("");
336                    int rules = prefixTree.loadNodes(prefixesSource.entries().stream());
337                    logger.info(
338                            "Loaded {} {} prefixes for remote repository {} ({})",
339                            rules,
340                            origin,
341                            prefixesSource.origin().getId(),
342                            prefixesSource.path().getFileName());
343                    return prefixTree;
344                } else {
345                    logger.info(
346                            "Rejected {} prefixes for remote repository {} ({}): {}",
347                            origin,
348                            prefixesSource.origin().getId(),
349                            prefixesSource.path().getFileName(),
350                            prefixesSource.message());
351                }
352            }
353            logger.debug("Prefix file for remote repository {} not available", remoteRepository);
354            return ENABLED_NO_INPUT;
355        }
356        logger.debug("Prefix file for remote repository {} disabled", remoteRepository);
357        return DISABLED;
358    }
359
360    private Path resolvePrefixesFromLocalConfiguration(
361            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
362        Path filePath =
363                baseDir.resolve(PREFIXES_FILE_PREFIX + repositoryKey(session, remoteRepository) + PREFIXES_FILE_SUFFIX);
364        if (Files.isReadable(filePath)) {
365            return filePath;
366        } else {
367            return null;
368        }
369    }
370
371    private boolean supportedResolvePrefixesForRemoteRepository(
372            RepositorySystemSession session, RemoteRepository remoteRepository) {
373        if (!ConfigUtils.getBoolean(
374                session,
375                DEFAULT_RESOLVE_PREFIX_FILES,
376                CONFIG_PROP_RESOLVE_PREFIX_FILES + "." + remoteRepository.getId(),
377                CONFIG_PROP_RESOLVE_PREFIX_FILES)) {
378            return false;
379        }
380        if (remoteRepository.isRepositoryManager()) {
381            return ConfigUtils.getBoolean(
382                    session, DEFAULT_USE_REPOSITORY_MANAGERS, CONFIG_PROP_USE_REPOSITORY_MANAGERS);
383        } else {
384            return remoteRepository.getMirroredRepositories().isEmpty()
385                    || ConfigUtils.getBoolean(
386                            session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES);
387        }
388    }
389
390    private Path resolvePrefixesFromRemoteRepository(
391            RepositorySystemSession session, RemoteRepository remoteRepository) {
392        MetadataResolver mr = metadataResolver.get();
393        RemoteRepositoryManager rm = remoteRepositoryManager.get();
394        if (mr != null && rm != null) {
395            // retrieve prefix as metadata from repository
396            MetadataResult result = mr.resolveMetadata(
397                            new DefaultRepositorySystemSession(session)
398                                    .setTransferListener(null)
399                                    .setConfigProperty(CONFIG_PROP_SKIPPED, Boolean.TRUE.toString()),
400                            Collections.singleton(new MetadataRequest(
401                                            new DefaultMetadata(PREFIX_FILE_TYPE, Metadata.Nature.RELEASE_OR_SNAPSHOT))
402                                    .setRepository(remoteRepository)
403                                    .setDeleteLocalCopyIfMissing(true)
404                                    .setFavorLocalRepository(true)))
405                    .get(0);
406            if (result.isResolved()) {
407                return result.getMetadata().getPath();
408            } else {
409                return null;
410            }
411        }
412        return null;
413    }
414
415    private class PrefixesFilter implements RemoteRepositoryFilter {
416        private final RepositorySystemSession session;
417        private final Path basedir;
418
419        private PrefixesFilter(RepositorySystemSession session, Path basedir) {
420            this.session = session;
421            this.basedir = basedir;
422        }
423
424        @Override
425        public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
426            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
427            if (repositoryLayout == NOT_SUPPORTED) {
428                return result(true, NAME, "Unsupported layout: " + remoteRepository);
429            }
430            return acceptPrefix(
431                    remoteRepository,
432                    repositoryLayout.getLocation(artifact, false).getPath());
433        }
434
435        @Override
436        public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
437            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
438            if (repositoryLayout == NOT_SUPPORTED) {
439                return result(true, NAME, "Unsupported layout: " + remoteRepository);
440            }
441            return acceptPrefix(
442                    remoteRepository,
443                    repositoryLayout.getLocation(metadata, false).getPath());
444        }
445
446        private Result acceptPrefix(RemoteRepository repository, String path) {
447            PrefixTree prefixTree = cachePrefixTree(session, basedir, repository);
448            if (prefixTree == DISABLED) {
449                return result(true, NAME, "Disabled");
450            } else if (prefixTree == ENABLED_NO_INPUT) {
451                return result(
452                        ConfigUtils.getBoolean(
453                                session,
454                                DEFAULT_NO_INPUT_OUTCOME,
455                                CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(),
456                                CONFIG_PROP_NO_INPUT_OUTCOME),
457                        NAME,
458                        "No input available");
459            }
460            boolean accepted = prefixTree.acceptedPath(path);
461            return result(
462                    accepted,
463                    NAME,
464                    accepted
465                            ? "Path " + path + " allowed from " + repository.getId()
466                            : "Path " + path + " NOT allowed from " + repository.getId());
467        }
468    }
469
470    private static final RepositoryLayout NOT_SUPPORTED = new RepositoryLayout() {
471        @Override
472        public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
473            throw new UnsupportedOperationException();
474        }
475
476        @Override
477        public boolean hasChecksums(Artifact artifact) {
478            throw new UnsupportedOperationException();
479        }
480
481        @Override
482        public URI getLocation(Artifact artifact, boolean upload) {
483            throw new UnsupportedOperationException();
484        }
485
486        @Override
487        public URI getLocation(Metadata metadata, boolean upload) {
488            throw new UnsupportedOperationException();
489        }
490
491        @Override
492        public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
493            throw new UnsupportedOperationException();
494        }
495
496        @Override
497        public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
498            throw new UnsupportedOperationException();
499        }
500    };
501}