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.nio.file.Files;
026import java.nio.file.Path;
027import java.util.Collections;
028import java.util.Objects;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.function.Supplier;
031
032import org.eclipse.aether.DefaultRepositorySystemSession;
033import org.eclipse.aether.RepositorySystemSession;
034import org.eclipse.aether.artifact.Artifact;
035import org.eclipse.aether.impl.MetadataResolver;
036import org.eclipse.aether.impl.RemoteRepositoryManager;
037import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource;
038import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree;
039import org.eclipse.aether.metadata.DefaultMetadata;
040import org.eclipse.aether.metadata.Metadata;
041import org.eclipse.aether.repository.RemoteRepository;
042import org.eclipse.aether.resolution.MetadataRequest;
043import org.eclipse.aether.resolution.MetadataResult;
044import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
045import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
046import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
047import org.eclipse.aether.transfer.NoRepositoryLayoutException;
048import org.eclipse.aether.util.ConfigUtils;
049import org.eclipse.aether.util.repository.RepositoryIdHelper;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053import static java.util.Objects.requireNonNull;
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    private static final String CONFIG_PROPS_PREFIX =
084            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".";
085
086    private static final String PREFIX_FILE_PATH = ".meta/prefixes.txt";
087
088    /**
089     * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using
090     * repository ID suffixes.
091     * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without
092     * configuration files, the enabled filter remains dormant and does not interfere with resolution.
093     * <strong>Configuration File Resolution:</strong>
094     * <ol>
095     * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR}
096     *     (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li>
097     * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li>
098     * </ol>
099     * <strong>File Naming:</strong> {@code prefixes-$(repository.id).txt}
100     * <strong>Recommended Setup (Auto-Discovery with Override Capability):</strong>
101     * Start with auto-discovery, but prepare for project-specific overrides. Add to {@code .mvn/maven.config}:
102     * <pre>
103     * -Daether.remoteRepositoryFilter.prefixes=true
104     * -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/
105     * </pre>
106     * <strong>Initial setup:</strong> Don't provide any files - rely on auto-discovery as repositories are accessed.
107     * <strong>Override when needed:</strong> Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and
108     * commit to version control.
109     * <strong>Caching:</strong> Auto-discovered prefix files are cached in the local repository with unique IDs
110     * (using {@link RepositoryIdHelper#remoteRepositoryUniqueId(RemoteRepository)}) to prevent conflicts that
111     * could cause build failures.
112     *
113     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
114     * @configurationType {@link java.lang.Boolean}
115     * @configurationRepoIdSuffix Yes
116     * @configurationDefaultValue {@link #DEFAULT_ENABLED}
117     */
118    public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
119
120    public static final boolean DEFAULT_ENABLED = true;
121
122    /**
123     * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
124     *
125     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
126     * @configurationType {@link java.lang.String}
127     * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
128     */
129    public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir";
130
131    public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
132
133    static final String PREFIXES_FILE_PREFIX = "prefixes-";
134
135    static final String PREFIXES_FILE_SUFFIX = ".txt";
136
137    private final Logger logger = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class);
138
139    private final Supplier<MetadataResolver> metadataResolver;
140
141    private final Supplier<RemoteRepositoryManager> remoteRepositoryManager;
142
143    private final RepositoryLayoutProvider repositoryLayoutProvider;
144
145    private final ConcurrentHashMap<RemoteRepository, PrefixTree> prefixes;
146
147    private final ConcurrentHashMap<RemoteRepository, RepositoryLayout> layouts;
148
149    private final ConcurrentHashMap<RemoteRepository, Boolean> ongoingUpdates;
150
151    @Inject
152    public PrefixesRemoteRepositoryFilterSource(
153            Supplier<MetadataResolver> metadataResolver,
154            Supplier<RemoteRepositoryManager> remoteRepositoryManager,
155            RepositoryLayoutProvider repositoryLayoutProvider) {
156        this.metadataResolver = requireNonNull(metadataResolver);
157        this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager);
158        this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider);
159        this.prefixes = new ConcurrentHashMap<>();
160        this.layouts = new ConcurrentHashMap<>();
161        this.ongoingUpdates = new ConcurrentHashMap<>();
162    }
163
164    @Override
165    protected boolean isEnabled(RepositorySystemSession session) {
166        return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED);
167    }
168
169    private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) {
170        if (isEnabled(session)) {
171            return ConfigUtils.getBoolean(
172                    session,
173                    ConfigUtils.getBoolean(session, true, CONFIG_PROP_ENABLED + ".*"),
174                    CONFIG_PROP_ENABLED + "." + remoteRepository.getId());
175        }
176        return false;
177    }
178
179    @Override
180    public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
181        if (isEnabled(session)) {
182            return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false));
183        }
184        return null;
185    }
186
187    /**
188     * Caches layout instances for remote repository. In case of unknown layout it returns {@code null}.
189     *
190     * @return the layout instance of {@code null} if layout not supported.
191     */
192    private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) {
193        return layouts.computeIfAbsent(remoteRepository, r -> {
194            try {
195                return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository);
196            } catch (NoRepositoryLayoutException e) {
197                return null;
198            }
199        });
200    }
201
202    private PrefixTree cachePrefixTree(
203            RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) {
204        return ongoingUpdatesGuard(
205                remoteRepository,
206                () -> prefixes.computeIfAbsent(
207                        remoteRepository, r -> loadPrefixTree(session, basedir, remoteRepository)),
208                () -> PrefixTree.SENTINEL);
209    }
210
211    private <T> T ongoingUpdatesGuard(RemoteRepository remoteRepository, Supplier<T> unblocked, Supplier<T> blocked) {
212        if (!remoteRepository.isBlocked() && null == ongoingUpdates.putIfAbsent(remoteRepository, Boolean.TRUE)) {
213            try {
214                return unblocked.get();
215            } finally {
216                ongoingUpdates.remove(remoteRepository);
217            }
218        }
219        return blocked.get();
220    }
221
222    private PrefixTree loadPrefixTree(
223            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
224        if (isRepositoryFilteringEnabled(session, remoteRepository)) {
225            String origin = "user-provided";
226            Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository);
227            if (filePath == null) {
228                origin = "auto-discovered";
229                filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository);
230            }
231            if (filePath != null) {
232                PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath);
233                if (prefixesSource.valid()) {
234                    logger.debug(
235                            "Loaded prefixes for remote repository {} from {} file '{}'",
236                            prefixesSource.origin().getId(),
237                            origin,
238                            prefixesSource.path());
239                    PrefixTree prefixTree = new PrefixTree("");
240                    int rules = prefixTree.loadNodes(prefixesSource.entries().stream());
241                    logger.info(
242                            "Loaded {} {} prefixes for remote repository {} ({})",
243                            rules,
244                            origin,
245                            prefixesSource.origin().getId(),
246                            prefixesSource.path().getFileName());
247                    return prefixTree;
248                } else {
249                    logger.info(
250                            "Rejected {} prefixes for remote repository {} ({}): {}",
251                            origin,
252                            prefixesSource.origin().getId(),
253                            prefixesSource.path().getFileName(),
254                            prefixesSource.message());
255                }
256            }
257            logger.debug("Prefix file for remote repository {} not available", remoteRepository);
258            return PrefixTree.SENTINEL;
259        }
260        logger.debug("Prefix file for remote repository {} disabled", remoteRepository);
261        return PrefixTree.SENTINEL;
262    }
263
264    private Path resolvePrefixesFromLocalConfiguration(
265            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
266        Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX
267                + RepositoryIdHelper.cachedIdToPathSegment(session).apply(remoteRepository)
268                + PREFIXES_FILE_SUFFIX);
269        if (Files.isReadable(filePath)) {
270            return filePath;
271        } else {
272            return null;
273        }
274    }
275
276    private Path resolvePrefixesFromRemoteRepository(
277            RepositorySystemSession session, RemoteRepository remoteRepository) {
278        MetadataResolver mr = metadataResolver.get();
279        RemoteRepositoryManager rm = remoteRepositoryManager.get();
280        if (mr != null && rm != null) {
281            // create "prepared" (auth, proxy and mirror equipped repo)
282            RemoteRepository prepared = rm.aggregateRepositories(
283                            session, Collections.emptyList(), Collections.singletonList(remoteRepository), true)
284                    .get(0);
285            // make it unique
286            RemoteRepository unique = new RemoteRepository.Builder(prepared)
287                    .setId(RepositoryIdHelper.remoteRepositoryUniqueId(remoteRepository))
288                    .build();
289            // supplier for path
290            Supplier<Path> supplier = () -> {
291                MetadataRequest request =
292                        new MetadataRequest(new DefaultMetadata(PREFIX_FILE_PATH, Metadata.Nature.RELEASE_OR_SNAPSHOT));
293                // use unique repository; this will result in prefix (repository metadata) cached under unique id
294                request.setRepository(unique);
295                request.setDeleteLocalCopyIfMissing(true);
296                request.setFavorLocalRepository(true);
297                MetadataResult result = mr.resolveMetadata(
298                                new DefaultRepositorySystemSession(session).setTransferListener(null),
299                                Collections.singleton(request))
300                        .get(0);
301                if (result.isResolved()) {
302                    return result.getMetadata().getPath();
303                } else {
304                    return null;
305                }
306            };
307
308            // prevent recursive calls; but we need extra work if not dealing with Central (as in that case outer call
309            // shields us)
310            if (Objects.equals(prepared.getId(), unique.getId())) {
311                return supplier.get();
312            } else {
313                return ongoingUpdatesGuard(unique, supplier, () -> null);
314            }
315        }
316        return null;
317    }
318
319    private class PrefixesFilter implements RemoteRepositoryFilter {
320        private final RepositorySystemSession session;
321        private final Path basedir;
322
323        private PrefixesFilter(RepositorySystemSession session, Path basedir) {
324            this.session = session;
325            this.basedir = basedir;
326        }
327
328        @Override
329        public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
330            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
331            if (repositoryLayout == null) {
332                return new SimpleResult(true, "Unsupported layout: " + remoteRepository);
333            }
334            return acceptPrefix(
335                    remoteRepository,
336                    repositoryLayout.getLocation(artifact, false).getPath());
337        }
338
339        @Override
340        public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
341            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
342            if (repositoryLayout == null) {
343                return new SimpleResult(true, "Unsupported layout: " + remoteRepository);
344            }
345            return acceptPrefix(
346                    remoteRepository,
347                    repositoryLayout.getLocation(metadata, false).getPath());
348        }
349
350        private Result acceptPrefix(RemoteRepository remoteRepository, String path) {
351            PrefixTree prefixTree = cachePrefixTree(session, basedir, remoteRepository);
352            if (PrefixTree.SENTINEL == prefixTree) {
353                return NOT_PRESENT_RESULT;
354            }
355            if (prefixTree.acceptedPath(path)) {
356                return new SimpleResult(true, "Path " + path + " allowed from " + remoteRepository);
357            } else {
358                return new SimpleResult(false, "Prefix " + path + " NOT allowed from " + remoteRepository);
359            }
360        }
361    }
362
363    private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT =
364            new SimpleResult(true, "Prefix file not present");
365}