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;
20  
21  import java.nio.file.Files;
22  import java.nio.file.Path;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.Properties;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.eclipse.aether.RepositorySystemSession;
33  import org.eclipse.aether.artifact.Artifact;
34  import org.eclipse.aether.metadata.Metadata;
35  import org.eclipse.aether.repository.LocalArtifactRegistration;
36  import org.eclipse.aether.repository.LocalArtifactRequest;
37  import org.eclipse.aether.repository.LocalArtifactResult;
38  import org.eclipse.aether.repository.RemoteRepository;
39  import org.eclipse.aether.repository.RepositoryKeyFunction;
40  
41  import static java.util.Objects.requireNonNull;
42  
43  /**
44   * These are implementation details for enhanced local repository manager, subject to change without prior notice.
45   * Repositories from which a cached artifact was resolved are tracked in a properties file named
46   * <code>_remote.repositories</code>, with content key as filename&gt;repo_id and value as empty string. If a file has
47   * been installed in the repository, but not downloaded from a remote repository, it is tracked as empty repository id
48   * and always resolved. For example:
49   *
50   * <pre>
51   * artifact-1.0.pom>=
52   * artifact-1.0.jar>=
53   * artifact-1.0.pom>central=
54   * artifact-1.0.jar>central=
55   * artifact-1.0.zip>central=
56   * artifact-1.0-classifier.zip>central=
57   * artifact-1.0.pom>my_repo_id=
58   * </pre>
59   *
60   * @see EnhancedLocalRepositoryManagerFactory
61   */
62  class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
63  
64      private static final String LOCAL_REPO_ID = "";
65  
66      /**
67       * Shared sentinel for "no tracking data". Mutation is forbidden: the instance is shared
68       * across threads via {@link #trackingFileCache} and returned directly from {@link #readRepos}.
69       */
70      private static final Properties EMPTY_PROPERTIES = new Properties() {
71          @Override
72          public synchronized Object put(Object key, Object value) {
73              throw new UnsupportedOperationException("EMPTY_PROPERTIES is read-only");
74          }
75  
76          @Override
77          public synchronized Object remove(Object key) {
78              throw new UnsupportedOperationException("EMPTY_PROPERTIES is read-only");
79          }
80  
81          @Override
82          public synchronized void clear() {
83              throw new UnsupportedOperationException("EMPTY_PROPERTIES is read-only");
84          }
85      };
86  
87      private final String trackingFilename;
88  
89      private final TrackingFileManager trackingFileManager;
90  
91      private final LocalPathPrefixComposer localPathPrefixComposer;
92  
93      /**
94       * Cache of tracking file contents, keyed by tracking file path. Eliminates redundant disk I/O
95       * when multiple artifacts in the same directory are resolved — they all share the same
96       * {@code _remote.repositories} tracking file. Invalidated on writes via {@link #addRepo}.
97       * <p>
98       * Cached {@link Properties} instances are shared across threads and must be treated as
99       * read-only by callers of {@link #readRepos}. The cache is scoped to this manager instance
100      * (one per session), so concurrent builds in separate JVMs each maintain independent caches.
101      * If another process updates a tracking file after it has been cached here, the stale entry
102      * may cause a redundant (but harmless) download — no data corruption.
103      */
104     private final ConcurrentHashMap<Path, Properties> trackingFileCache = new ConcurrentHashMap<>();
105 
106     EnhancedLocalRepositoryManager(
107             Path basedir,
108             LocalPathComposer localPathComposer,
109             RepositoryKeyFunction repositoryKeyFunction,
110             String trackingFilename,
111             TrackingFileManager trackingFileManager,
112             LocalPathPrefixComposer localPathPrefixComposer) {
113         super(basedir, "enhanced", localPathComposer, repositoryKeyFunction);
114         this.trackingFilename = requireNonNull(trackingFilename);
115         this.trackingFileManager = requireNonNull(trackingFileManager);
116         this.localPathPrefixComposer = requireNonNull(localPathPrefixComposer);
117     }
118 
119     private String concatPaths(String prefix, String artifactPath) {
120         if (prefix == null || prefix.isEmpty()) {
121             return artifactPath;
122         }
123         return prefix + '/' + artifactPath;
124     }
125 
126     @Override
127     public String getPathForLocalArtifact(Artifact artifact) {
128         return concatPaths(
129                 localPathPrefixComposer.getPathPrefixForLocalArtifact(artifact),
130                 super.getPathForLocalArtifact(artifact));
131     }
132 
133     @Override
134     public String getPathForRemoteArtifact(Artifact artifact, RemoteRepository repository, String context) {
135         return concatPaths(
136                 localPathPrefixComposer.getPathPrefixForRemoteArtifact(artifact, repository),
137                 super.getPathForRemoteArtifact(artifact, repository, context));
138     }
139 
140     @Override
141     public String getPathForLocalMetadata(Metadata metadata) {
142         return concatPaths(
143                 localPathPrefixComposer.getPathPrefixForLocalMetadata(metadata),
144                 super.getPathForLocalMetadata(metadata));
145     }
146 
147     @Override
148     public String getPathForRemoteMetadata(Metadata metadata, RemoteRepository repository, String context) {
149         return concatPaths(
150                 localPathPrefixComposer.getPathPrefixForRemoteMetadata(metadata, repository),
151                 super.getPathForRemoteMetadata(metadata, repository, context));
152     }
153 
154     @Override
155     public LocalArtifactResult find(RepositorySystemSession session, LocalArtifactRequest request) {
156         Artifact artifact = request.getArtifact();
157         LocalArtifactResult result = new LocalArtifactResult(request);
158 
159         Path filePath;
160 
161         // Local repository CANNOT have timestamped installed, they are created only during deploy
162         if (Objects.equals(artifact.getVersion(), artifact.getBaseVersion())) {
163             filePath = getAbsolutePathForLocalArtifact(artifact);
164             checkFind(filePath, result);
165         }
166 
167         if (!result.isAvailable()) {
168             for (RemoteRepository repository : request.getRepositories()) {
169                 filePath = getAbsolutePathForRemoteArtifact(artifact, repository, request.getContext());
170 
171                 checkFind(filePath, result);
172 
173                 if (result.isAvailable()) {
174                     break;
175                 }
176             }
177         }
178 
179         return result;
180     }
181 
182     private void checkFind(Path path, LocalArtifactResult result) {
183         if (Files.isRegularFile(path)) {
184             result.setPath(path);
185 
186             Properties props = readRepos(path);
187 
188             if (props.get(getKey(path, LOCAL_REPO_ID)) != null) {
189                 // artifact installed into the local repo is always accepted
190                 result.setAvailable(true);
191             } else {
192                 String context = result.getRequest().getContext();
193                 for (RemoteRepository repository : result.getRequest().getRepositories()) {
194                     if (props.get(getKey(path, getRepositoryKey(repository, context))) != null) {
195                         // artifact downloaded from remote repository is accepted only downloaded from request
196                         // repositories
197                         result.setAvailable(true);
198                         result.setRepository(repository);
199                         break;
200                     }
201                 }
202                 if (!result.isAvailable() && !isTracked(props, path)) {
203                     /*
204                      * NOTE: The artifact is present but not tracked at all, for inter-op with simple local repo, assume
205                      * the artifact was locally installed.
206                      */
207                     result.setAvailable(true);
208                 }
209             }
210         }
211     }
212 
213     @Override
214     public void add(RepositorySystemSession session, LocalArtifactRegistration request) {
215         Collection<String> repositories;
216         if (request.getRepository() == null) {
217             repositories = Collections.singleton(LOCAL_REPO_ID);
218         } else {
219             repositories = getRepositoryKeys(request.getRepository(), request.getContexts());
220         }
221         if (request.getRepository() == null) {
222             addArtifact(request.getArtifact(), repositories, null, null);
223         } else {
224             for (String context : request.getContexts()) {
225                 addArtifact(request.getArtifact(), repositories, request.getRepository(), context);
226             }
227         }
228     }
229 
230     private Collection<String> getRepositoryKeys(RemoteRepository repository, Collection<String> contexts) {
231         Collection<String> keys = new HashSet<>();
232 
233         if (contexts != null) {
234             for (String context : contexts) {
235                 keys.add(getRepositoryKey(repository, context));
236             }
237         }
238 
239         return keys;
240     }
241 
242     private void addArtifact(
243             Artifact artifact, Collection<String> repositories, RemoteRepository repository, String context) {
244         requireNonNull(artifact, "artifact cannot be null");
245         Path file = repository == null
246                 ? getAbsolutePathForLocalArtifact(artifact)
247                 : getAbsolutePathForRemoteArtifact(artifact, repository, context);
248         addRepo(file, repositories);
249     }
250 
251     private Properties readRepos(Path artifactPath) {
252         Path trackingFile = getTrackingFile(artifactPath);
253         return trackingFileCache.computeIfAbsent(trackingFile, tf -> {
254             Properties props = trackingFileManager.read(tf);
255             return (props != null) ? props : EMPTY_PROPERTIES;
256         });
257     }
258 
259     private void addRepo(Path artifactPath, Collection<String> repositories) {
260         Map<String, String> updates = new HashMap<>();
261         for (String repository : repositories) {
262             updates.put(getKey(artifactPath, repository), "");
263         }
264 
265         Path trackingPath = getTrackingFile(artifactPath);
266 
267         // Invalidate cache before write: using put() with the returned Properties would be
268         // racy — two concurrent addRepo() calls could reorder their puts, leaving stale data.
269         // Invalidating forces the next readRepos() to re-read from disk.
270         trackingFileCache.remove(trackingPath);
271         trackingFileManager.update(trackingPath, updates);
272     }
273 
274     private Path getTrackingFile(Path artifactPath) {
275         return artifactPath.getParent().resolve(trackingFilename);
276     }
277 
278     private String getKey(Path path, String repository) {
279         return path.getFileName() + ">" + repository;
280     }
281 
282     private boolean isTracked(Properties props, Path path) {
283         if (props != null) {
284             String keyPrefix = path.getFileName() + ">";
285             for (Object key : props.keySet()) {
286                 if (key.toString().startsWith(keyPrefix)) {
287                     return true;
288                 }
289             }
290         }
291         return false;
292     }
293 }