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