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