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.BufferedReader;
26  import java.io.IOException;
27  import java.io.UncheckedIOException;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.TreeSet;
37  import java.util.concurrent.ConcurrentHashMap;
38  import java.util.concurrent.atomic.AtomicBoolean;
39  
40  import org.eclipse.aether.MultiRuntimeException;
41  import org.eclipse.aether.RepositorySystemSession;
42  import org.eclipse.aether.artifact.Artifact;
43  import org.eclipse.aether.impl.RepositorySystemLifecycle;
44  import org.eclipse.aether.metadata.Metadata;
45  import org.eclipse.aether.repository.RemoteRepository;
46  import org.eclipse.aether.resolution.ArtifactResult;
47  import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
48  import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
49  import org.eclipse.aether.util.ConfigUtils;
50  import org.eclipse.aether.util.FileUtils;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  import static java.util.Objects.requireNonNull;
55  
56  /**
57   * Remote repository filter source filtering on G coordinate. It is backed by a file that lists all allowed groupIds
58   * and groupId not present in this file are filtered out.
59   * <p>
60   * The file can be authored manually: format is one groupId per line, comments starting with "#" (hash) amd empty lines
61   * for structuring are supported. The file can also be pre-populated by "record" functionality of this filter.
62   * When "recording", this filter will not filter out anything, but will instead populate the file with all encountered
63   * groupIds.
64   * <p>
65   * The groupId file is expected on path "${basedir}/groupId-${repository.id}.txt".
66   * <p>
67   * The groupId file once loaded are cached in component, so in-flight groupId file change during component existence
68   * are NOT noticed.
69   *
70   * @since 1.9.0
71   */
72  @Singleton
73  @Named(GroupIdRemoteRepositoryFilterSource.NAME)
74  public final class GroupIdRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport
75          implements ArtifactResolverPostProcessor {
76      public static final String NAME = "groupId";
77  
78      private static final String CONFIG_PROPS_PREFIX =
79              RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".";
80  
81      /**
82       * Is filter enabled?
83       *
84       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
85       * @configurationType {@link java.lang.Boolean}
86       * @configurationDefaultValue false
87       */
88      public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
89  
90      /**
91       * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
92       *
93       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
94       * @configurationType {@link java.lang.String}
95       * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
96       */
97      public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir";
98  
99      public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
100 
101     /**
102      * Should filter go into "record" mode (and collect encountered artifacts)?
103      *
104      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
105      * @configurationType {@link java.lang.Boolean}
106      * @configurationDefaultValue false
107      */
108     public static final String CONFIG_PROP_RECORD = CONFIG_PROPS_PREFIX + "record";
109 
110     static final String GROUP_ID_FILE_PREFIX = "groupId-";
111 
112     static final String GROUP_ID_FILE_SUFFIX = ".txt";
113 
114     private static final Logger LOGGER = LoggerFactory.getLogger(GroupIdRemoteRepositoryFilterSource.class);
115 
116     private final RepositorySystemLifecycle repositorySystemLifecycle;
117 
118     private final ConcurrentHashMap<Path, Set<String>> rules;
119 
120     private final ConcurrentHashMap<Path, Boolean> changedRules;
121 
122     private final AtomicBoolean onShutdownHandlerRegistered;
123 
124     @Inject
125     public GroupIdRemoteRepositoryFilterSource(RepositorySystemLifecycle repositorySystemLifecycle) {
126         this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle);
127         this.rules = new ConcurrentHashMap<>();
128         this.changedRules = new ConcurrentHashMap<>();
129         this.onShutdownHandlerRegistered = new AtomicBoolean(false);
130     }
131 
132     @Override
133     protected boolean isEnabled(RepositorySystemSession session) {
134         return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED);
135     }
136 
137     @Override
138     public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
139         if (isEnabled(session) && !isRecord(session)) {
140             return new GroupIdFilter(session);
141         }
142         return null;
143     }
144 
145     @Override
146     public void postProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) {
147         if (isEnabled(session) && isRecord(session)) {
148             if (onShutdownHandlerRegistered.compareAndSet(false, true)) {
149                 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines);
150             }
151             for (ArtifactResult artifactResult : artifactResults) {
152                 if (artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository) {
153                     Path filePath = filePath(
154                             getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false),
155                             artifactResult.getRepository().getId());
156                     boolean newGroupId = rules.computeIfAbsent(
157                                     filePath, f -> Collections.synchronizedSet(new TreeSet<>()))
158                             .add(artifactResult.getArtifact().getGroupId());
159                     if (newGroupId) {
160                         changedRules.put(filePath, Boolean.TRUE);
161                     }
162                 }
163             }
164         }
165     }
166 
167     /**
168      * Returns the groupId path. The file and parents may not exist, this method merely calculate the path.
169      */
170     private Path filePath(Path basedir, String remoteRepositoryId) {
171         return basedir.resolve(GROUP_ID_FILE_PREFIX + remoteRepositoryId + GROUP_ID_FILE_SUFFIX);
172     }
173 
174     private Set<String> cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) {
175         Path filePath = filePath(
176                 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false), remoteRepository.getId());
177         return rules.computeIfAbsent(filePath, r -> {
178             Set<String> rules = loadRepositoryRules(filePath);
179             if (rules != NOT_PRESENT) {
180                 LOGGER.info("Loaded {} groupId for remote repository {}", rules.size(), remoteRepository.getId());
181             }
182             return rules;
183         });
184     }
185 
186     private Set<String> loadRepositoryRules(Path filePath) {
187         if (Files.isReadable(filePath)) {
188             try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
189                 TreeSet<String> result = new TreeSet<>();
190                 String groupId;
191                 while ((groupId = reader.readLine()) != null) {
192                     if (!groupId.startsWith("#") && !groupId.trim().isEmpty()) {
193                         result.add(groupId);
194                     }
195                 }
196                 return Collections.unmodifiableSet(result);
197             } catch (IOException e) {
198                 throw new UncheckedIOException(e);
199             }
200         }
201         return NOT_PRESENT;
202     }
203 
204     private static final TreeSet<String> NOT_PRESENT = new TreeSet<>();
205 
206     private class GroupIdFilter implements RemoteRepositoryFilter {
207         private final RepositorySystemSession session;
208 
209         private GroupIdFilter(RepositorySystemSession session) {
210             this.session = session;
211         }
212 
213         @Override
214         public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
215             return acceptGroupId(remoteRepository, artifact.getGroupId());
216         }
217 
218         @Override
219         public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
220             return acceptGroupId(remoteRepository, metadata.getGroupId());
221         }
222 
223         private Result acceptGroupId(RemoteRepository remoteRepository, String groupId) {
224             Set<String> groupIds = cacheRules(session, remoteRepository);
225             if (NOT_PRESENT == groupIds) {
226                 return NOT_PRESENT_RESULT;
227             }
228 
229             if (groupIds.contains(groupId)) {
230                 return new SimpleResult(true, "G:" + groupId + " allowed from " + remoteRepository);
231             } else {
232                 return new SimpleResult(false, "G:" + groupId + " NOT allowed from " + remoteRepository);
233             }
234         }
235     }
236 
237     private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT =
238             new SimpleResult(true, "GroupId file not present");
239 
240     /**
241      * Returns {@code true} if given session is recording.
242      */
243     private boolean isRecord(RepositorySystemSession session) {
244         return ConfigUtils.getBoolean(session, false, CONFIG_PROP_RECORD);
245     }
246 
247     /**
248      * On-close handler that saves recorded rules, if any.
249      */
250     private void saveRecordedLines() {
251         if (changedRules.isEmpty()) {
252             return;
253         }
254 
255         ArrayList<Exception> exceptions = new ArrayList<>();
256         for (Map.Entry<Path, Set<String>> entry : rules.entrySet()) {
257             Path filePath = entry.getKey();
258             if (changedRules.get(filePath) != Boolean.TRUE) {
259                 continue;
260             }
261             Set<String> recordedLines = entry.getValue();
262             if (!recordedLines.isEmpty()) {
263                 try {
264                     TreeSet<String> result = new TreeSet<>();
265                     result.addAll(loadRepositoryRules(filePath));
266                     result.addAll(recordedLines);
267 
268                     LOGGER.info("Saving {} groupIds to '{}'", result.size(), filePath);
269                     FileUtils.writeFileWithBackup(filePath, p -> Files.write(p, result));
270                 } catch (IOException e) {
271                     exceptions.add(e);
272                 }
273             }
274         }
275         MultiRuntimeException.mayThrow("session save groupIds failure", exceptions);
276     }
277 }