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 CONF_NAME_RECORD = "record";
79  
80      static final String GROUP_ID_FILE_PREFIX = "groupId-";
81  
82      static final String GROUP_ID_FILE_SUFFIX = ".txt";
83  
84      private static final Logger LOGGER = LoggerFactory.getLogger(GroupIdRemoteRepositoryFilterSource.class);
85  
86      private final RepositorySystemLifecycle repositorySystemLifecycle;
87  
88      private final ConcurrentHashMap<Path, Set<String>> rules;
89  
90      private final ConcurrentHashMap<Path, Boolean> changedRules;
91  
92      private final AtomicBoolean onShutdownHandlerRegistered;
93  
94      @Inject
95      public GroupIdRemoteRepositoryFilterSource(RepositorySystemLifecycle repositorySystemLifecycle) {
96          super(NAME);
97          this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle);
98          this.rules = new ConcurrentHashMap<>();
99          this.changedRules = new ConcurrentHashMap<>();
100         this.onShutdownHandlerRegistered = new AtomicBoolean(false);
101     }
102 
103     @Override
104     public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
105         if (isEnabled(session) && !isRecord(session)) {
106             return new GroupIdFilter(session);
107         }
108         return null;
109     }
110 
111     @Override
112     public void postProcess(RepositorySystemSession session, List<ArtifactResult> artifactResults) {
113         if (isEnabled(session) && isRecord(session)) {
114             if (onShutdownHandlerRegistered.compareAndSet(false, true)) {
115                 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines);
116             }
117             for (ArtifactResult artifactResult : artifactResults) {
118                 if (artifactResult.isResolved() && artifactResult.getRepository() instanceof RemoteRepository) {
119                     Path filePath = filePath(
120                             getBasedir(session, false),
121                             artifactResult.getRepository().getId());
122                     boolean newGroupId = rules.computeIfAbsent(
123                                     filePath, f -> Collections.synchronizedSet(new TreeSet<>()))
124                             .add(artifactResult.getArtifact().getGroupId());
125                     if (newGroupId) {
126                         changedRules.put(filePath, Boolean.TRUE);
127                     }
128                 }
129             }
130         }
131     }
132 
133     /**
134      * Returns the groupId path. The file and parents may not exist, this method merely calculate the path.
135      */
136     private Path filePath(Path basedir, String remoteRepositoryId) {
137         return basedir.resolve(GROUP_ID_FILE_PREFIX + remoteRepositoryId + GROUP_ID_FILE_SUFFIX);
138     }
139 
140     private Set<String> cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) {
141         Path filePath = filePath(getBasedir(session, false), remoteRepository.getId());
142         return rules.computeIfAbsent(filePath, r -> {
143             Set<String> rules = loadRepositoryRules(filePath);
144             if (rules != NOT_PRESENT) {
145                 LOGGER.info("Loaded {} groupId for remote repository {}", rules.size(), remoteRepository.getId());
146             }
147             return rules;
148         });
149     }
150 
151     private Set<String> loadRepositoryRules(Path filePath) {
152         if (Files.isReadable(filePath)) {
153             try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
154                 TreeSet<String> result = new TreeSet<>();
155                 String groupId;
156                 while ((groupId = reader.readLine()) != null) {
157                     if (!groupId.startsWith("#") && !groupId.trim().isEmpty()) {
158                         result.add(groupId);
159                     }
160                 }
161                 return Collections.unmodifiableSet(result);
162             } catch (IOException e) {
163                 throw new UncheckedIOException(e);
164             }
165         }
166         return NOT_PRESENT;
167     }
168 
169     private static final TreeSet<String> NOT_PRESENT = new TreeSet<>();
170 
171     private class GroupIdFilter implements RemoteRepositoryFilter {
172         private final RepositorySystemSession session;
173 
174         private GroupIdFilter(RepositorySystemSession session) {
175             this.session = session;
176         }
177 
178         @Override
179         public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
180             return acceptGroupId(remoteRepository, artifact.getGroupId());
181         }
182 
183         @Override
184         public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
185             return acceptGroupId(remoteRepository, metadata.getGroupId());
186         }
187 
188         private Result acceptGroupId(RemoteRepository remoteRepository, String groupId) {
189             Set<String> groupIds = cacheRules(session, remoteRepository);
190             if (NOT_PRESENT == groupIds) {
191                 return NOT_PRESENT_RESULT;
192             }
193 
194             if (groupIds.contains(groupId)) {
195                 return new SimpleResult(true, "G:" + groupId + " allowed from " + remoteRepository);
196             } else {
197                 return new SimpleResult(false, "G:" + groupId + " NOT allowed from " + remoteRepository);
198             }
199         }
200     }
201 
202     private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT =
203             new SimpleResult(true, "GroupId file not present");
204 
205     /**
206      * Returns {@code true} if given session is recording.
207      */
208     private boolean isRecord(RepositorySystemSession session) {
209         return ConfigUtils.getBoolean(session, false, configPropKey(CONF_NAME_RECORD));
210     }
211 
212     /**
213      * On-close handler that saves recorded rules, if any.
214      */
215     private void saveRecordedLines() {
216         if (changedRules.isEmpty()) {
217             return;
218         }
219 
220         ArrayList<Exception> exceptions = new ArrayList<>();
221         for (Map.Entry<Path, Set<String>> entry : rules.entrySet()) {
222             Path filePath = entry.getKey();
223             if (changedRules.get(filePath) != Boolean.TRUE) {
224                 continue;
225             }
226             Set<String> recordedLines = entry.getValue();
227             if (!recordedLines.isEmpty()) {
228                 try {
229                     TreeSet<String> result = new TreeSet<>();
230                     result.addAll(loadRepositoryRules(filePath));
231                     result.addAll(recordedLines);
232 
233                     LOGGER.info("Saving {} groupIds to '{}'", result.size(), filePath);
234                     FileUtils.writeFileWithBackup(filePath, p -> Files.write(p, result));
235                 } catch (IOException e) {
236                     exceptions.add(e);
237                 }
238             }
239         }
240         MultiRuntimeException.mayThrow("session save groupIds failure", exceptions);
241     }
242 }