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