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.apache.maven.repository.internal;
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.InputStream;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  
36  import org.apache.maven.artifact.repository.metadata.Snapshot;
37  import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
38  import org.apache.maven.artifact.repository.metadata.Versioning;
39  import org.apache.maven.metadata.v4.MetadataStaxReader;
40  import org.eclipse.aether.RepositoryCache;
41  import org.eclipse.aether.RepositoryEvent;
42  import org.eclipse.aether.RepositoryEvent.EventType;
43  import org.eclipse.aether.RepositorySystemSession;
44  import org.eclipse.aether.RequestTrace;
45  import org.eclipse.aether.SyncContext;
46  import org.eclipse.aether.artifact.Artifact;
47  import org.eclipse.aether.impl.MetadataResolver;
48  import org.eclipse.aether.impl.RepositoryEventDispatcher;
49  import org.eclipse.aether.impl.VersionResolver;
50  import org.eclipse.aether.metadata.DefaultMetadata;
51  import org.eclipse.aether.metadata.Metadata;
52  import org.eclipse.aether.repository.ArtifactRepository;
53  import org.eclipse.aether.repository.LocalRepository;
54  import org.eclipse.aether.repository.RemoteRepository;
55  import org.eclipse.aether.repository.WorkspaceReader;
56  import org.eclipse.aether.repository.WorkspaceRepository;
57  import org.eclipse.aether.resolution.MetadataRequest;
58  import org.eclipse.aether.resolution.MetadataResult;
59  import org.eclipse.aether.resolution.VersionRequest;
60  import org.eclipse.aether.resolution.VersionResolutionException;
61  import org.eclipse.aether.resolution.VersionResult;
62  import org.eclipse.aether.spi.synccontext.SyncContextFactory;
63  import org.eclipse.aether.util.ConfigUtils;
64  
65  /**
66   * @deprecated since 4.0.0, use {@code maven-api-impl} jar instead
67   */
68  @Named
69  @Singleton
70  @Deprecated(since = "4.0.0")
71  public class DefaultVersionResolver implements VersionResolver {
72  
73      private static final String MAVEN_METADATA_XML = "maven-metadata.xml";
74  
75      private static final String RELEASE = "RELEASE";
76  
77      private static final String LATEST = "LATEST";
78  
79      private static final String SNAPSHOT = "SNAPSHOT";
80  
81      private final MetadataResolver metadataResolver;
82      private final SyncContextFactory syncContextFactory;
83      private final RepositoryEventDispatcher repositoryEventDispatcher;
84  
85      @Inject
86      public DefaultVersionResolver(
87              MetadataResolver metadataResolver,
88              SyncContextFactory syncContextFactory,
89              RepositoryEventDispatcher repositoryEventDispatcher) {
90          this.metadataResolver = Objects.requireNonNull(metadataResolver, "metadataResolver cannot be null");
91          this.syncContextFactory = Objects.requireNonNull(syncContextFactory, "syncContextFactory cannot be null");
92          this.repositoryEventDispatcher =
93                  Objects.requireNonNull(repositoryEventDispatcher, "repositoryEventDispatcher cannot be null");
94      }
95  
96      @SuppressWarnings("checkstyle:methodlength")
97      @Override
98      public VersionResult resolveVersion(RepositorySystemSession session, VersionRequest request)
99              throws VersionResolutionException {
100         RequestTrace trace = RequestTrace.newChild(request.getTrace(), request);
101 
102         Artifact artifact = request.getArtifact();
103 
104         String version = artifact.getVersion();
105 
106         VersionResult result = new VersionResult(request);
107 
108         Key cacheKey = null;
109         RepositoryCache cache = session.getCache();
110         if (cache != null && !ConfigUtils.getBoolean(session, false, "aether.versionResolver.noCache")) {
111             cacheKey = new Key(session, request);
112 
113             Object obj = cache.get(session, cacheKey);
114             if (obj instanceof Record record) {
115                 result.setVersion(record.version);
116                 result.setRepository(
117                         getRepository(session, request.getRepositories(), record.repoClass, record.repoId));
118                 return result;
119             }
120         }
121 
122         Metadata metadata;
123 
124         if (RELEASE.equals(version)) {
125             metadata = new DefaultMetadata(
126                     artifact.getGroupId(), artifact.getArtifactId(), MAVEN_METADATA_XML, Metadata.Nature.RELEASE);
127         } else if (LATEST.equals(version)) {
128             metadata = new DefaultMetadata(
129                     artifact.getGroupId(),
130                     artifact.getArtifactId(),
131                     MAVEN_METADATA_XML,
132                     Metadata.Nature.RELEASE_OR_SNAPSHOT);
133         } else if (version.endsWith(SNAPSHOT)) {
134             WorkspaceReader workspace = session.getWorkspaceReader();
135             if (workspace != null && workspace.findVersions(artifact).contains(version)) {
136                 metadata = null;
137                 result.setRepository(workspace.getRepository());
138             } else {
139                 metadata = new DefaultMetadata(
140                         artifact.getGroupId(),
141                         artifact.getArtifactId(),
142                         version,
143                         MAVEN_METADATA_XML,
144                         Metadata.Nature.SNAPSHOT);
145             }
146         } else {
147             metadata = null;
148         }
149 
150         if (metadata == null) {
151             result.setVersion(version);
152         } else {
153             List<MetadataRequest> metadataReqs =
154                     new ArrayList<>(request.getRepositories().size());
155 
156             metadataReqs.add(new MetadataRequest(metadata, null, request.getRequestContext()));
157 
158             for (RemoteRepository repository : request.getRepositories()) {
159                 MetadataRequest metadataRequest =
160                         new MetadataRequest(metadata, repository, request.getRequestContext());
161                 metadataRequest.setDeleteLocalCopyIfMissing(true);
162                 metadataRequest.setFavorLocalRepository(true);
163                 metadataRequest.setTrace(trace);
164                 metadataReqs.add(metadataRequest);
165             }
166 
167             List<MetadataResult> metadataResults = metadataResolver.resolveMetadata(session, metadataReqs);
168 
169             Map<String, VersionInfo> infos = new HashMap<>();
170 
171             for (MetadataResult metadataResult : metadataResults) {
172                 result.addException(metadataResult.getException());
173 
174                 ArtifactRepository repository = metadataResult.getRequest().getRepository();
175                 if (repository == null) {
176                     repository = session.getLocalRepository();
177                 }
178 
179                 Versioning v = readVersions(session, trace, metadataResult.getMetadata(), repository, result);
180                 merge(artifact, infos, v, repository);
181             }
182 
183             if (RELEASE.equals(version)) {
184                 resolve(result, infos, RELEASE);
185             } else if (LATEST.equals(version)) {
186                 if (!resolve(result, infos, LATEST)) {
187                     resolve(result, infos, RELEASE);
188                 }
189 
190                 if (result.getVersion() != null && result.getVersion().endsWith(SNAPSHOT)) {
191                     VersionRequest subRequest = new VersionRequest();
192                     subRequest.setArtifact(artifact.setVersion(result.getVersion()));
193                     if (result.getRepository() instanceof RemoteRepository remoteRepository) {
194                         subRequest.setRepositories(Collections.singletonList(remoteRepository));
195                     } else {
196                         subRequest.setRepositories(request.getRepositories());
197                     }
198                     VersionResult subResult = resolveVersion(session, subRequest);
199                     result.setVersion(subResult.getVersion());
200                     result.setRepository(subResult.getRepository());
201                     for (Exception exception : subResult.getExceptions()) {
202                         result.addException(exception);
203                     }
204                 }
205             } else {
206                 String key = SNAPSHOT + getKey(artifact.getClassifier(), artifact.getExtension());
207                 merge(infos, SNAPSHOT, key);
208                 if (!resolve(result, infos, key)) {
209                     result.setVersion(version);
210                 }
211             }
212 
213             if (result.getVersion() == null || result.getVersion().isEmpty()) {
214                 throw new VersionResolutionException(result);
215             }
216         }
217 
218         if (cacheKey != null && metadata != null && isSafelyCacheable(session, artifact)) {
219             cache.put(session, cacheKey, new Record(result.getVersion(), result.getRepository()));
220         }
221 
222         return result;
223     }
224 
225     private boolean resolve(VersionResult result, Map<String, VersionInfo> infos, String key) {
226         VersionInfo info = infos.get(key);
227         if (info != null) {
228             result.setVersion(info.version);
229             result.setRepository(info.repository);
230         }
231         return info != null;
232     }
233 
234     private Versioning readVersions(
235             RepositorySystemSession session,
236             RequestTrace trace,
237             Metadata metadata,
238             ArtifactRepository repository,
239             VersionResult result) {
240         Versioning versioning = null;
241         try {
242             if (metadata != null) {
243                 try (SyncContext syncContext = syncContextFactory.newInstance(session, true)) {
244                     syncContext.acquire(null, Collections.singleton(metadata));
245 
246                     if (metadata.getPath() != null && Files.exists(metadata.getPath())) {
247                         try (InputStream in = Files.newInputStream(metadata.getPath())) {
248                             versioning = new Versioning(
249                                     new MetadataStaxReader().read(in, false).getVersioning());
250 
251                             /*
252                             NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata
253                             of the local repository. This is especially troublesome during snapshot resolution so we try
254                             to handle that gracefully.
255                              */
256                             if (versioning != null
257                                     && repository instanceof LocalRepository
258                                     && versioning.getSnapshot() != null
259                                     && versioning.getSnapshot().getBuildNumber() > 0) {
260                                 final Versioning repaired = new Versioning();
261                                 repaired.setLastUpdated(versioning.getLastUpdated());
262                                 repaired.setSnapshot(new Snapshot());
263                                 repaired.getSnapshot().setLocalCopy(true);
264                                 versioning = repaired;
265                                 throw new IOException("Snapshot information corrupted with remote repository data"
266                                         + ", please verify that no remote repository uses the id '"
267                                         + repository.getId() + "'");
268                             }
269                         }
270                     }
271                 }
272             }
273         } catch (Exception e) {
274             invalidMetadata(session, trace, metadata, repository, e);
275             result.addException(e);
276         }
277 
278         return (versioning != null) ? versioning : new Versioning();
279     }
280 
281     private void invalidMetadata(
282             RepositorySystemSession session,
283             RequestTrace trace,
284             Metadata metadata,
285             ArtifactRepository repository,
286             Exception exception) {
287         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID);
288         event.setTrace(trace);
289         event.setMetadata(metadata);
290         event.setException(exception);
291         event.setRepository(repository);
292 
293         repositoryEventDispatcher.dispatch(event.build());
294     }
295 
296     private void merge(
297             Artifact artifact, Map<String, VersionInfo> infos, Versioning versioning, ArtifactRepository repository) {
298         if (versioning.getRelease() != null && !versioning.getRelease().isEmpty()) {
299             merge(RELEASE, infos, versioning.getLastUpdated(), versioning.getRelease(), repository);
300         }
301 
302         if (versioning.getLatest() != null && !versioning.getLatest().isEmpty()) {
303             merge(LATEST, infos, versioning.getLastUpdated(), versioning.getLatest(), repository);
304         }
305 
306         for (SnapshotVersion sv : versioning.getSnapshotVersions()) {
307             if (sv.getVersion() != null && !sv.getVersion().isEmpty()) {
308                 String key = getKey(sv.getClassifier(), sv.getExtension());
309                 merge(SNAPSHOT + key, infos, sv.getUpdated(), sv.getVersion(), repository);
310             }
311         }
312 
313         Snapshot snapshot = versioning.getSnapshot();
314         if (snapshot != null && versioning.getSnapshotVersions().isEmpty()) {
315             String version = artifact.getVersion();
316             if (snapshot.getTimestamp() != null && snapshot.getBuildNumber() > 0) {
317                 String qualifier = snapshot.getTimestamp() + '-' + snapshot.getBuildNumber();
318                 version = version.substring(0, version.length() - SNAPSHOT.length()) + qualifier;
319             }
320             merge(SNAPSHOT, infos, versioning.getLastUpdated(), version, repository);
321         }
322     }
323 
324     private void merge(
325             String key,
326             Map<String, VersionInfo> infos,
327             String timestamp,
328             String version,
329             ArtifactRepository repository) {
330         VersionInfo info = infos.get(key);
331         if (info == null) {
332             info = new VersionInfo(timestamp, version, repository);
333             infos.put(key, info);
334         } else if (info.isOutdated(timestamp)) {
335             info.version = version;
336             info.repository = repository;
337             info.timestamp = timestamp;
338         }
339     }
340 
341     private void merge(Map<String, VersionInfo> infos, String srcKey, String dstKey) {
342         VersionInfo srcInfo = infos.get(srcKey);
343         VersionInfo dstInfo = infos.get(dstKey);
344 
345         if (dstInfo == null
346                 || (srcInfo != null
347                         && dstInfo.isOutdated(srcInfo.timestamp)
348                         && srcInfo.repository != dstInfo.repository)) {
349             infos.put(dstKey, srcInfo);
350         }
351     }
352 
353     private String getKey(String classifier, String extension) {
354         return (classifier == null ? "" : classifier.trim()) + ':' + (extension == null ? "" : extension.trim());
355     }
356 
357     private ArtifactRepository getRepository(
358             RepositorySystemSession session, List<RemoteRepository> repositories, Class<?> repoClass, String repoId) {
359         if (repoClass != null) {
360             if (WorkspaceRepository.class.isAssignableFrom(repoClass)) {
361                 return session.getWorkspaceReader().getRepository();
362             } else if (LocalRepository.class.isAssignableFrom(repoClass)) {
363                 return session.getLocalRepository();
364             } else {
365                 for (RemoteRepository repository : repositories) {
366                     if (repoId.equals(repository.getId())) {
367                         return repository;
368                     }
369                 }
370             }
371         }
372         return null;
373     }
374 
375     private boolean isSafelyCacheable(RepositorySystemSession session, Artifact artifact) {
376         /*
377          * The workspace/reactor is in flux so we better not assume definitive information for any of its
378          * artifacts/projects.
379          */
380 
381         WorkspaceReader workspace = session.getWorkspaceReader();
382         if (workspace == null) {
383             return true;
384         }
385 
386         Artifact pomArtifact = ArtifactDescriptorUtils.toPomArtifact(artifact);
387 
388         return workspace.findArtifact(pomArtifact) == null;
389     }
390 
391     private static class VersionInfo {
392 
393         String timestamp;
394 
395         String version;
396 
397         ArtifactRepository repository;
398 
399         VersionInfo(String timestamp, String version, ArtifactRepository repository) {
400             this.timestamp = (timestamp != null) ? timestamp : "";
401             this.version = version;
402             this.repository = repository;
403         }
404 
405         boolean isOutdated(String timestamp) {
406             return timestamp != null && timestamp.compareTo(this.timestamp) > 0;
407         }
408     }
409 
410     private static class Key {
411 
412         private final String groupId;
413 
414         private final String artifactId;
415 
416         private final String classifier;
417 
418         private final String extension;
419 
420         private final String version;
421 
422         private final String context;
423 
424         private final Path localRepo;
425 
426         private final WorkspaceRepository workspace;
427 
428         private final List<RemoteRepository> repositories;
429 
430         private final int hashCode;
431 
432         Key(RepositorySystemSession session, VersionRequest request) {
433             Artifact artifact = request.getArtifact();
434             groupId = artifact.getGroupId();
435             artifactId = artifact.getArtifactId();
436             classifier = artifact.getClassifier();
437             extension = artifact.getExtension();
438             version = artifact.getVersion();
439             localRepo = session.getLocalRepository().getBasePath();
440             WorkspaceReader reader = session.getWorkspaceReader();
441             workspace = (reader != null) ? reader.getRepository() : null;
442             repositories = new ArrayList<>(request.getRepositories().size());
443             boolean repoMan = false;
444             for (RemoteRepository repository : request.getRepositories()) {
445                 if (repository.isRepositoryManager()) {
446                     repoMan = true;
447                     repositories.addAll(repository.getMirroredRepositories());
448                 } else {
449                     repositories.add(repository);
450                 }
451             }
452             context = repoMan ? request.getRequestContext() : "";
453 
454             int hash = 17;
455             hash = hash * 31 + groupId.hashCode();
456             hash = hash * 31 + artifactId.hashCode();
457             hash = hash * 31 + classifier.hashCode();
458             hash = hash * 31 + extension.hashCode();
459             hash = hash * 31 + version.hashCode();
460             hash = hash * 31 + localRepo.hashCode();
461             hash = hash * 31 + repositories.hashCode();
462             hashCode = hash;
463         }
464 
465         @Override
466         public boolean equals(Object obj) {
467             if (obj == this) {
468                 return true;
469             } else if (obj == null || !getClass().equals(obj.getClass())) {
470                 return false;
471             }
472 
473             Key that = (Key) obj;
474             return artifactId.equals(that.artifactId)
475                     && groupId.equals(that.groupId)
476                     && classifier.equals(that.classifier)
477                     && extension.equals(that.extension)
478                     && version.equals(that.version)
479                     && context.equals(that.context)
480                     && localRepo.equals(that.localRepo)
481                     && Objects.equals(workspace, that.workspace)
482                     && repositories.equals(that.repositories);
483         }
484 
485         @Override
486         public int hashCode() {
487             return hashCode;
488         }
489     }
490 
491     private static class Record {
492         final String version;
493 
494         final String repoId;
495 
496         final Class<?> repoClass;
497 
498         Record(String version, ArtifactRepository repository) {
499             this.version = version;
500             if (repository != null) {
501                 repoId = repository.getId();
502                 repoClass = repository.getClass();
503             } else {
504                 repoId = null;
505                 repoClass = null;
506             }
507         }
508     }
509 }