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.io.IOException;
26  import java.io.UncheckedIOException;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.TreeSet;
35  import java.util.concurrent.ConcurrentHashMap;
36  import java.util.concurrent.ConcurrentMap;
37  import java.util.concurrent.atomic.AtomicBoolean;
38  import java.util.stream.Collectors;
39  import java.util.stream.Stream;
40  
41  import org.eclipse.aether.MultiRuntimeException;
42  import org.eclipse.aether.RepositorySystemSession;
43  import org.eclipse.aether.artifact.Artifact;
44  import org.eclipse.aether.impl.RepositorySystemLifecycle;
45  import org.eclipse.aether.internal.impl.filter.ruletree.GroupTree;
46  import org.eclipse.aether.metadata.Metadata;
47  import org.eclipse.aether.repository.RemoteRepository;
48  import org.eclipse.aether.resolution.ArtifactResult;
49  import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
50  import org.eclipse.aether.spi.io.PathProcessor;
51  import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
52  import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
53  import org.eclipse.aether.util.ConfigUtils;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import static java.util.Objects.requireNonNull;
58  
59  /**
60   * Remote repository filter source filtering on G coordinate. It is backed by a file that is parsed into {@link GroupTree}.
61   * <p>
62   * The file can be authored manually. The file can also be pre-populated by "record" functionality of this filter.
63   * When "recording", this filter will not filter out anything, but will instead populate the file with all encountered
64   * groupIds recorded as {@code =groupId}. The recorded file should be authored afterward to fine tune it, as there is
65   * no optimization in place (ie to look for smallest common parent groupId and alike).
66   * <p>
67   * The groupId file is expected on path "${basedir}/groupId-${repository.id}.txt".
68   * <p>
69   * The groupId file once loaded are cached in component, so in-flight groupId file change during component existence
70   * are NOT noticed.
71   *
72   * @see GroupTree
73   *
74   * @since 1.9.0
75   */
76  @Singleton
77  @Named(GroupIdRemoteRepositoryFilterSource.NAME)
78  public final class GroupIdRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport
79          implements ArtifactResolverPostProcessor {
80      public static final String NAME = "groupId";
81  
82      /**
83       * Configuration to enable the GroupId filter (enabled by default). Can be fine-tuned per repository using
84       * repository ID suffixes.
85       * <strong>Important:</strong> For this filter to take effect, you must provide configuration files. Without
86       * configuration files, the enabled filter remains dormant and does not interfere with resolution.
87       * <strong>Configuration Files:</strong>
88       * <ul>
89       * <li>Location: Directory specified by {@link #CONFIG_PROP_BASEDIR} (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li>
90       * <li>Naming: {@code groupId-$(repository.id).txt}</li>
91       * <li>Content: One groupId per line to allow/block from the repository</li>
92       * </ul>
93       * <strong>Recommended Setup (Per-Project):</strong>
94       * Use project-specific configuration to avoid repository ID clashes. Add to {@code .mvn/maven.config}:
95       * <pre>
96       * -Daether.remoteRepositoryFilter.groupId=true
97       * -Daether.remoteRepositoryFilter.groupId.basedir=${session.rootDirectory}/.mvn/rrf/
98       * </pre>
99       * Then create {@code groupId-myrepoId.txt} files in the {@code .mvn/rrf/} directory and commit them to version control.
100      *
101      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
102      * @configurationType {@link java.lang.Boolean}
103      * @configurationRepoIdSuffix Yes
104      * @configurationDefaultValue {@link #DEFAULT_ENABLED}
105      */
106     public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
107 
108     public static final boolean DEFAULT_ENABLED = true;
109 
110     /**
111      * Configuration to skip the GroupId filter for given request. This configuration is evaluated and if {@code true}
112      * the GroupId remote filter will not kick in.
113      *
114      * @since 2.0.14
115      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
116      * @configurationType {@link java.lang.Boolean}
117      * @configurationRepoIdSuffix Yes
118      * @configurationDefaultValue {@link #DEFAULT_SKIPPED}
119      */
120     public static final String CONFIG_PROP_SKIPPED =
121             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped";
122 
123     public static final boolean DEFAULT_SKIPPED = false;
124 
125     /**
126      * Determines what happens when the filter is enabled, but has no groupId file available for given remote repository
127      * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote
128      * repository when no groupId file is available. When set to {@code false}, the filter blocks all requests toward
129      * given remote repository when no groupId file is available. This setting allows repoId suffix, hence, can
130      * determine "global" or "repository targeted" behaviors.
131      *
132      * @since 2.0.14
133      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
134      * @configurationType {@link java.lang.Boolean}
135      * @configurationRepoIdSuffix Yes
136      * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME}
137      */
138     public static final String CONFIG_PROP_NO_INPUT_OUTCOME =
139             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome";
140 
141     public static final boolean DEFAULT_NO_INPUT_OUTCOME = true;
142 
143     /**
144      * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
145      *
146      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
147      * @configurationType {@link java.lang.String}
148      * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
149      */
150     public static final String CONFIG_PROP_BASEDIR =
151             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir";
152 
153     public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
154 
155     /**
156      * Should filter go into "record" mode (and collect encountered artifacts)?
157      *
158      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
159      * @configurationType {@link java.lang.Boolean}
160      * @configurationDefaultValue false
161      */
162     public static final String CONFIG_PROP_RECORD =
163             RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".record";
164 
165     static final String GROUP_ID_FILE_PREFIX = "groupId-";
166 
167     static final String GROUP_ID_FILE_SUFFIX = ".txt";
168 
169     private final Logger logger = LoggerFactory.getLogger(GroupIdRemoteRepositoryFilterSource.class);
170 
171     private final RepositorySystemLifecycle repositorySystemLifecycle;
172 
173     private final PathProcessor pathProcessor;
174 
175     @Inject
176     public GroupIdRemoteRepositoryFilterSource(
177             RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
178             RepositorySystemLifecycle repositorySystemLifecycle,
179             PathProcessor pathProcessor) {
180         super(repositoryKeyFunctionFactory);
181         this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle);
182         this.pathProcessor = requireNonNull(pathProcessor);
183     }
184 
185     @SuppressWarnings("unchecked")
186     private ConcurrentMap<RemoteRepository, GroupTree> rules(RepositorySystemSession session) {
187         return (ConcurrentMap<RemoteRepository, GroupTree>)
188                 session.getData().computeIfAbsent(getClass().getName() + ".rules", ConcurrentHashMap::new);
189     }
190 
191     @SuppressWarnings("unchecked")
192     private ConcurrentMap<RemoteRepository, Path> ruleFiles(RepositorySystemSession session) {
193         return (ConcurrentMap<RemoteRepository, Path>)
194                 session.getData().computeIfAbsent(getClass().getName() + ".ruleFiles", ConcurrentHashMap::new);
195     }
196 
197     @SuppressWarnings("unchecked")
198     private ConcurrentMap<RemoteRepository, Set<String>> recordedRules(RepositorySystemSession session) {
199         return (ConcurrentMap<RemoteRepository, Set<String>>)
200                 session.getData().computeIfAbsent(getClass().getName() + ".recordedRules", ConcurrentHashMap::new);
201     }
202 
203     private AtomicBoolean onShutdownHandlerRegistered(RepositorySystemSession session) {
204         return (AtomicBoolean) session.getData()
205                 .computeIfAbsent(getClass().getName() + ".onShutdownHandlerRegistered", AtomicBoolean::new);
206     }
207 
208     @Override
209     protected boolean isEnabled(RepositorySystemSession session) {
210         return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED)
211                 && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED);
212     }
213 
214     private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) {
215         if (isEnabled(session)) {
216             return ConfigUtils.getBoolean(
217                             session,
218                             DEFAULT_ENABLED,
219                             CONFIG_PROP_ENABLED + "." + remoteRepository.getId(),
220                             CONFIG_PROP_ENABLED + ".*")
221                     && !ConfigUtils.getBoolean(
222                             session,
223                             DEFAULT_SKIPPED,
224                             CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(),
225                             CONFIG_PROP_SKIPPED + ".*");
226         }
227         return false;
228     }
229 
230     @Override
231     public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
232         if (isEnabled(session) && !isRecord(session)) {
233             return new GroupIdFilter(session);
234         }
235         return null;
236     }
237 
238     @Override
239     public void postProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) {
240         if (isEnabled(session) && isRecord(session)) {
241             if (onShutdownHandlerRegistered(session).compareAndSet(false, true)) {
242                 repositorySystemLifecycle.addOnSystemEndedHandler(() -> saveRecordedLines(session));
243             }
244             for (ArtifactResult artifactResult : artifactResults) {
245                 if (artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository) {
246                     RemoteRepository remoteRepository = (RemoteRepository) artifactResult.getRepository();
247                     if (isRepositoryFilteringEnabled(session, remoteRepository)) {
248                         ruleFile(session, remoteRepository); // populate it; needed for save
249                         String line = "=" + artifactResult.getArtifact().getGroupId();
250                         RemoteRepository normalized = normalizeRemoteRepository(session, remoteRepository);
251                         recordedRules(session)
252                                 .computeIfAbsent(normalized, k -> new TreeSet<>())
253                                 .add(line);
254                         rules(session)
255                                 .compute(normalized, (k, v) -> {
256                                     if (v == null || v == DISABLED || v == ENABLED_NO_INPUT) {
257                                         v = GroupTree.create("record");
258                                     }
259                                     return v;
260                                 })
261                                 .loadNode(line);
262                     }
263                 }
264             }
265         }
266     }
267 
268     private Path ruleFile(RepositorySystemSession session, RemoteRepository remoteRepository) {
269         return ruleFiles(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> getBasedir(
270                         session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false)
271                 .resolve(GROUP_ID_FILE_PREFIX + repositoryKey(session, remoteRepository) + GROUP_ID_FILE_SUFFIX));
272     }
273 
274     private GroupTree cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) {
275         return rules(session)
276                 .computeIfAbsent(
277                         normalizeRemoteRepository(session, remoteRepository), r -> loadRepositoryRules(session, r));
278     }
279 
280     private static final GroupTree DISABLED = GroupTree.create("disabled");
281     private static final GroupTree ENABLED_NO_INPUT = GroupTree.create("enabled-no-input");
282 
283     private GroupTree loadRepositoryRules(RepositorySystemSession session, RemoteRepository remoteRepository) {
284         if (isRepositoryFilteringEnabled(session, remoteRepository)) {
285             Path filePath = ruleFile(session, remoteRepository);
286             if (Files.isReadable(filePath)) {
287                 try (Stream<String> lines = Files.lines(filePath, StandardCharsets.UTF_8)) {
288                     GroupTree groupTree =
289                             GroupTree.create(filePath.getFileName().toString());
290                     int rules = groupTree.loadNodes(lines);
291                     logger.info("Loaded {} group rules for remote repository {}", rules, remoteRepository.getId());
292                     if (logger.isDebugEnabled()) {
293                         groupTree.dump("");
294                     }
295                     return groupTree;
296                 } catch (IOException e) {
297                     throw new UncheckedIOException(e);
298                 }
299             }
300             logger.debug("Group rules file for remote repository {} not available", remoteRepository);
301             return ENABLED_NO_INPUT;
302         }
303         logger.debug("Group rules file for remote repository {} disabled", remoteRepository);
304         return DISABLED;
305     }
306 
307     private class GroupIdFilter implements RemoteRepositoryFilter {
308         private final RepositorySystemSession session;
309 
310         private GroupIdFilter(RepositorySystemSession session) {
311             this.session = session;
312         }
313 
314         @Override
315         public Result acceptArtifact(RemoteRepository repository, Artifact artifact) {
316             return acceptGroupId(repository, artifact.getGroupId());
317         }
318 
319         @Override
320         public Result acceptMetadata(RemoteRepository repository, Metadata metadata) {
321             return acceptGroupId(repository, metadata.getGroupId());
322         }
323 
324         private Result acceptGroupId(RemoteRepository repository, String groupId) {
325             GroupTree groupTree = cacheRules(session, repository);
326             if (groupTree == DISABLED) {
327                 return result(true, NAME, "Disabled");
328             } else if (groupTree == ENABLED_NO_INPUT) {
329                 return result(
330                         ConfigUtils.getBoolean(
331                                 session,
332                                 DEFAULT_NO_INPUT_OUTCOME,
333                                 CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(),
334                                 CONFIG_PROP_NO_INPUT_OUTCOME),
335                         NAME,
336                         "No input available");
337             }
338 
339             boolean accepted = groupTree.acceptedGroupId(groupId);
340             return result(
341                     accepted,
342                     NAME,
343                     accepted
344                             ? "G:" + groupId + " allowed from " + repository.getId()
345                             : "G:" + groupId + " NOT allowed from " + repository.getId());
346         }
347     }
348 
349     /**
350      * Returns {@code true} if given session is recording.
351      */
352     private boolean isRecord(RepositorySystemSession session) {
353         return ConfigUtils.getBoolean(session, false, CONFIG_PROP_RECORD);
354     }
355 
356     /**
357      * On-close handler that saves recorded rules, if any.
358      */
359     private void saveRecordedLines(RepositorySystemSession session) {
360         ArrayList<Exception> exceptions = new ArrayList<>();
361         for (Map.Entry<RemoteRepository, Path> entry : ruleFiles(session).entrySet()) {
362             Set<String> recorded = recordedRules(session).get(entry.getKey());
363             if (recorded != null && !recorded.isEmpty()) {
364                 try {
365                     ArrayList<String> result = new ArrayList<>();
366                     if (Files.isReadable(entry.getValue())) {
367                         result.addAll(Files.readAllLines(entry.getValue()));
368                     }
369                     result.add("# Recorded entries");
370                     result.addAll(recorded);
371                     logger.info("Saving {} groupIds to '{}'", result.size(), entry.getValue());
372                     pathProcessor.writeWithBackup(
373                             entry.getValue(), result.stream().collect(Collectors.joining(System.lineSeparator())));
374                 } catch (IOException e) {
375                     exceptions.add(e);
376                 }
377             }
378         }
379         MultiRuntimeException.mayThrow("session save groupIds failure", exceptions);
380     }
381 }