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