1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
282
283
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
312
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
445
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 }