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