View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.impl.filter;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.net.URI;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.ConcurrentMap;
32  import java.util.function.Supplier;
33  
34  import org.eclipse.aether.DefaultRepositorySystemSession;
35  import org.eclipse.aether.RepositorySystemSession;
36  import org.eclipse.aether.artifact.Artifact;
37  import org.eclipse.aether.impl.MetadataResolver;
38  import org.eclipse.aether.impl.RemoteRepositoryManager;
39  import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource;
40  import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree;
41  import org.eclipse.aether.metadata.DefaultMetadata;
42  import org.eclipse.aether.metadata.Metadata;
43  import org.eclipse.aether.repository.RemoteRepository;
44  import org.eclipse.aether.resolution.MetadataRequest;
45  import org.eclipse.aether.resolution.MetadataResult;
46  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
47  import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
48  import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
49  import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
50  import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
51  import org.eclipse.aether.transfer.NoRepositoryLayoutException;
52  import org.eclipse.aether.util.ConfigUtils;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import static java.util.Objects.requireNonNull;
57  
58  /**
59   * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
60   * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
61   * path with no corresponding prefix present in this file is filtered out.
62   * <p>
63   * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
64   * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
65   * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
66   * <p>
67   * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
68   * <p>
69   * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
70   * noticed.
71   * <p>
72   * Examples of published prefix files:
73   * <ul>
74   *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
75   *     <li>Apache Releases:
76   *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
77   * </ul>
78   *
79   * @since 1.9.0
80   */
81  @Singleton
82  @Named(PrefixesRemoteRepositoryFilterSource.NAME)
83  public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport {
84      public static final String NAME = "prefixes";
85  
86      static final String PREFIX_FILE_TYPE = ".meta/prefixes.txt";
87  
88      /**
89       * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using
90       * repository ID suffixes.
91       * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without
92       * configuration files, the enabled filter remains dormant and does not interfere with resolution.
93       * <strong>Configuration File Resolution:</strong>
94       * <ol>
95       * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR}
96       *     (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li>
97       * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li>
98       * </ol>
99       * <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.
110      *
111      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
112      * @configurationType {@link java.lang.Boolean}
113      * @configurationRepoIdSuffix Yes
114      * @configurationDefaultValue {@link #DEFAULT_ENABLED}
115      */
116     public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
117 
118     public static final boolean DEFAULT_ENABLED = true;
119 
120     /**
121      * Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if {@code true}
122      * the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during
123      * discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while
124      * leaving configuration like {@link #CONFIG_PROP_ENABLED} still show the "real state".
125      *
126      * @since 2.0.14
127      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
128      * @configurationType {@link java.lang.Boolean}
129      * @configurationRepoIdSuffix Yes
130      * @configurationDefaultValue {@link #DEFAULT_SKIPPED}
131      */
132     public static final String CONFIG_PROP_SKIPPED =
133             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped";
134 
135     public static final boolean DEFAULT_SKIPPED = false;
136 
137     /**
138      * Determines what happens when the filter is enabled, but has no prefixes available for given remote repository
139      * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote
140      * repository when no prefixes are available. When set to {@code false}, the filter blocks all requests toward
141      * given remote repository when no prefixes are available. This setting allows repoId suffix, hence, can
142      * determine "global" or "repository targeted" behaviors.
143      *
144      * @since 2.0.14
145      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
146      * @configurationType {@link java.lang.Boolean}
147      * @configurationRepoIdSuffix Yes
148      * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME}
149      */
150     public static final String CONFIG_PROP_NO_INPUT_OUTCOME =
151             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome";
152 
153     public static final boolean DEFAULT_NO_INPUT_OUTCOME = true;
154 
155     /**
156      * Configuration to allow Prefixes file resolution attempt from remote repository as "auto discovery". If this
157      * configuration set to {@code false} only user-provided prefixes will be used.
158      *
159      * @since 2.0.14
160      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
161      * @configurationType {@link java.lang.Boolean}
162      * @configurationRepoIdSuffix Yes
163      * @configurationDefaultValue {@link #DEFAULT_RESOLVE_PREFIX_FILES}
164      */
165     public static final String CONFIG_PROP_RESOLVE_PREFIX_FILES =
166             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".resolvePrefixFiles";
167 
168     public static final boolean DEFAULT_RESOLVE_PREFIX_FILES = true;
169 
170     /**
171      * Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to
172      * work <em>Maven should be aware</em> that given remote repository is mirror and is usually backed by MRM. Given
173      * multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
174      * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
175      *
176      * @since 2.0.14
177      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
178      * @configurationType {@link java.lang.Boolean}
179      * @configurationRepoIdSuffix Yes
180      * @configurationDefaultValue {@link #DEFAULT_USE_MIRRORED_REPOSITORIES}
181      */
182     public static final String CONFIG_PROP_USE_MIRRORED_REPOSITORIES =
183             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useMirroredRepositories";
184 
185     public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false;
186 
187     /**
188      * Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to
189      * work <em>Maven should be aware</em> that given remote repository is backed by repository manager.
190      * Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
191      * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
192      * <em>Note: as of today, nothing sets this on remote repositories, but is added for future.</em>
193      *
194      * @since 2.0.14
195      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
196      * @configurationType {@link java.lang.Boolean}
197      * @configurationRepoIdSuffix Yes
198      * @configurationDefaultValue {@link #DEFAULT_USE_REPOSITORY_MANAGERS}
199      */
200     public static final String CONFIG_PROP_USE_REPOSITORY_MANAGERS =
201             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useRepositoryManagers";
202 
203     public static final boolean DEFAULT_USE_REPOSITORY_MANAGERS = false;
204 
205     /**
206      * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
207      *
208      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
209      * @configurationType {@link java.lang.String}
210      * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
211      */
212     public static final String CONFIG_PROP_BASEDIR =
213             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir";
214 
215     public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
216 
217     static final String PREFIXES_FILE_PREFIX = "prefixes-";
218 
219     static final String PREFIXES_FILE_SUFFIX = ".txt";
220 
221     private final Logger logger = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class);
222 
223     private final Supplier<MetadataResolver> metadataResolver;
224 
225     private final Supplier<RemoteRepositoryManager> remoteRepositoryManager;
226 
227     private final RepositoryLayoutProvider repositoryLayoutProvider;
228 
229     @Inject
230     public PrefixesRemoteRepositoryFilterSource(
231             RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
232             Supplier<MetadataResolver> metadataResolver,
233             Supplier<RemoteRepositoryManager> remoteRepositoryManager,
234             RepositoryLayoutProvider repositoryLayoutProvider) {
235         super(repositoryKeyFunctionFactory);
236         this.metadataResolver = requireNonNull(metadataResolver);
237         this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager);
238         this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider);
239     }
240 
241     @SuppressWarnings("unchecked")
242     private ConcurrentMap<RemoteRepository, PrefixTree> prefixes(RepositorySystemSession session) {
243         return (ConcurrentMap<RemoteRepository, PrefixTree>)
244                 session.getData().computeIfAbsent(getClass().getName() + ".prefixes", ConcurrentHashMap::new);
245     }
246 
247     @SuppressWarnings("unchecked")
248     private ConcurrentMap<RemoteRepository, RepositoryLayout> layouts(RepositorySystemSession session) {
249         return (ConcurrentMap<RemoteRepository, RepositoryLayout>)
250                 session.getData().computeIfAbsent(getClass().getName() + ".layouts", ConcurrentHashMap::new);
251     }
252 
253     @Override
254     protected boolean isEnabled(RepositorySystemSession session) {
255         return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED)
256                 && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED);
257     }
258 
259     private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) {
260         if (isEnabled(session)) {
261             return ConfigUtils.getBoolean(
262                             session,
263                             DEFAULT_ENABLED,
264                             CONFIG_PROP_ENABLED + "." + remoteRepository.getId(),
265                             CONFIG_PROP_ENABLED + ".*")
266                     && !ConfigUtils.getBoolean(
267                             session,
268                             DEFAULT_SKIPPED,
269                             CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(),
270                             CONFIG_PROP_SKIPPED + ".*");
271         }
272         return false;
273     }
274 
275     @Override
276     public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
277         if (isEnabled(session)) {
278             return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false));
279         }
280         return null;
281     }
282 
283     /**
284      * Caches layout instances for remote repository. In case of unknown layout it returns {@link #NOT_SUPPORTED}.
285      *
286      * @return the layout instance or {@link #NOT_SUPPORTED} if layout not supported.
287      */
288     private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) {
289         return layouts(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> {
290             try {
291                 return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository);
292             } catch (NoRepositoryLayoutException e) {
293                 return NOT_SUPPORTED;
294             }
295         });
296     }
297 
298     private PrefixTree cachePrefixTree(
299             RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) {
300         return prefixes(session)
301                 .computeIfAbsent(
302                         normalizeRemoteRepository(session, remoteRepository),
303                         r -> loadPrefixTree(session, basedir, remoteRepository));
304     }
305 
306     private static final PrefixTree DISABLED = new PrefixTree("disabled");
307     private static final PrefixTree ENABLED_NO_INPUT = new PrefixTree("enabled-no-input");
308 
309     private PrefixTree loadPrefixTree(
310             RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
311         if (isRepositoryFilteringEnabled(session, remoteRepository)) {
312             String origin = "user-provided";
313             Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository);
314             if (filePath == null) {
315                 if (!supportedResolvePrefixesForRemoteRepository(session, remoteRepository)) {
316                     origin = "unsupported";
317                 } else {
318                     origin = "auto-discovered";
319                     filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository);
320                 }
321             }
322             if (filePath != null) {
323                 PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath);
324                 if (prefixesSource.valid()) {
325                     logger.debug(
326                             "Loaded prefixes for remote repository {} from {} file '{}'",
327                             prefixesSource.origin().getId(),
328                             origin,
329                             prefixesSource.path());
330                     PrefixTree prefixTree = new PrefixTree("");
331                     int rules = prefixTree.loadNodes(prefixesSource.entries().stream());
332                     logger.info(
333                             "Loaded {} {} prefixes for remote repository {} ({})",
334                             rules,
335                             origin,
336                             prefixesSource.origin().getId(),
337                             prefixesSource.path().getFileName());
338                     return prefixTree;
339                 } else {
340                     logger.info(
341                             "Rejected {} prefixes for remote repository {} ({}): {}",
342                             origin,
343                             prefixesSource.origin().getId(),
344                             prefixesSource.path().getFileName(),
345                             prefixesSource.message());
346                 }
347             }
348             logger.debug("Prefix file for remote repository {} not available", remoteRepository);
349             return ENABLED_NO_INPUT;
350         }
351         logger.debug("Prefix file for remote repository {} disabled", remoteRepository);
352         return DISABLED;
353     }
354 
355     private Path resolvePrefixesFromLocalConfiguration(
356             RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
357         Path filePath =
358                 baseDir.resolve(PREFIXES_FILE_PREFIX + repositoryKey(session, remoteRepository) + PREFIXES_FILE_SUFFIX);
359         if (Files.isReadable(filePath)) {
360             return filePath;
361         } else {
362             return null;
363         }
364     }
365 
366     private boolean supportedResolvePrefixesForRemoteRepository(
367             RepositorySystemSession session, RemoteRepository remoteRepository) {
368         if (!ConfigUtils.getBoolean(
369                 session,
370                 DEFAULT_RESOLVE_PREFIX_FILES,
371                 CONFIG_PROP_RESOLVE_PREFIX_FILES + "." + remoteRepository.getId(),
372                 CONFIG_PROP_RESOLVE_PREFIX_FILES)) {
373             return false;
374         }
375         if (remoteRepository.isRepositoryManager()) {
376             return ConfigUtils.getBoolean(
377                     session, DEFAULT_USE_REPOSITORY_MANAGERS, CONFIG_PROP_USE_REPOSITORY_MANAGERS);
378         } else {
379             return remoteRepository.getMirroredRepositories().isEmpty()
380                     || ConfigUtils.getBoolean(
381                             session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES);
382         }
383     }
384 
385     private Path resolvePrefixesFromRemoteRepository(
386             RepositorySystemSession session, RemoteRepository remoteRepository) {
387         MetadataResolver mr = metadataResolver.get();
388         RemoteRepositoryManager rm = remoteRepositoryManager.get();
389         if (mr != null && rm != null) {
390             // create "prepared" (auth, proxy and mirror equipped repo)
391             RemoteRepository prepared = rm.aggregateRepositories(
392                             session, Collections.emptyList(), Collections.singletonList(remoteRepository), true)
393                     .get(0);
394             // retrieve prefix as metadata from repository
395             MetadataResult result = mr.resolveMetadata(
396                             new DefaultRepositorySystemSession(session)
397                                     .setTransferListener(null)
398                                     .setConfigProperty(CONFIG_PROP_SKIPPED, Boolean.TRUE.toString()),
399                             Collections.singleton(new MetadataRequest(
400                                             new DefaultMetadata(PREFIX_FILE_TYPE, Metadata.Nature.RELEASE_OR_SNAPSHOT))
401                                     .setRepository(prepared)
402                                     .setDeleteLocalCopyIfMissing(true)
403                                     .setFavorLocalRepository(true)))
404                     .get(0);
405             if (result.isResolved()) {
406                 return result.getMetadata().getPath();
407             } else {
408                 return null;
409             }
410         }
411         return null;
412     }
413 
414     private class PrefixesFilter implements RemoteRepositoryFilter {
415         private final RepositorySystemSession session;
416         private final Path basedir;
417 
418         private PrefixesFilter(RepositorySystemSession session, Path basedir) {
419             this.session = session;
420             this.basedir = basedir;
421         }
422 
423         @Override
424         public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
425             RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
426             if (repositoryLayout == NOT_SUPPORTED) {
427                 return result(true, NAME, "Unsupported layout: " + remoteRepository);
428             }
429             return acceptPrefix(
430                     remoteRepository,
431                     repositoryLayout.getLocation(artifact, false).getPath());
432         }
433 
434         @Override
435         public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
436             RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
437             if (repositoryLayout == NOT_SUPPORTED) {
438                 return result(true, NAME, "Unsupported layout: " + remoteRepository);
439             }
440             return acceptPrefix(
441                     remoteRepository,
442                     repositoryLayout.getLocation(metadata, false).getPath());
443         }
444 
445         private Result acceptPrefix(RemoteRepository repository, String path) {
446             PrefixTree prefixTree = cachePrefixTree(session, basedir, repository);
447             if (prefixTree == DISABLED) {
448                 return result(true, NAME, "Disabled");
449             } else if (prefixTree == ENABLED_NO_INPUT) {
450                 return result(
451                         ConfigUtils.getBoolean(
452                                 session,
453                                 DEFAULT_NO_INPUT_OUTCOME,
454                                 CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(),
455                                 CONFIG_PROP_NO_INPUT_OUTCOME),
456                         NAME,
457                         "No input available");
458             }
459             boolean accepted = prefixTree.acceptedPath(path);
460             return result(
461                     accepted,
462                     NAME,
463                     accepted
464                             ? "Path " + path + " allowed from " + repository.getId()
465                             : "Path " + path + " NOT allowed from " + repository.getId());
466         }
467     }
468 
469     private static final RepositoryLayout NOT_SUPPORTED = new RepositoryLayout() {
470         @Override
471         public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
472             throw new UnsupportedOperationException();
473         }
474 
475         @Override
476         public boolean hasChecksums(Artifact artifact) {
477             throw new UnsupportedOperationException();
478         }
479 
480         @Override
481         public URI getLocation(Artifact artifact, boolean upload) {
482             throw new UnsupportedOperationException();
483         }
484 
485         @Override
486         public URI getLocation(Metadata metadata, boolean upload) {
487             throw new UnsupportedOperationException();
488         }
489 
490         @Override
491         public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
492             throw new UnsupportedOperationException();
493         }
494 
495         @Override
496         public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
497             throw new UnsupportedOperationException();
498         }
499     };
500 }