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