1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.version.internal;
20
21 import javax.inject.Inject;
22 import javax.inject.Named;
23 import javax.inject.Singleton;
24
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.TreeSet;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.ConcurrentMap;
35
36 import org.apache.maven.artifact.repository.metadata.Metadata;
37 import org.apache.maven.artifact.repository.metadata.Versioning;
38 import org.apache.maven.artifact.repository.metadata.io.MetadataReader;
39 import org.apache.maven.model.Build;
40 import org.apache.maven.model.Plugin;
41 import org.apache.maven.plugin.MavenPluginManager;
42 import org.apache.maven.plugin.PluginResolutionException;
43 import org.apache.maven.plugin.descriptor.PluginDescriptor;
44 import org.apache.maven.plugin.version.PluginVersionRequest;
45 import org.apache.maven.plugin.version.PluginVersionResolutionException;
46 import org.apache.maven.plugin.version.PluginVersionResolver;
47 import org.apache.maven.plugin.version.PluginVersionResult;
48 import org.eclipse.aether.RepositoryEvent;
49 import org.eclipse.aether.RepositoryEvent.EventType;
50 import org.eclipse.aether.RepositoryListener;
51 import org.eclipse.aether.RepositorySystem;
52 import org.eclipse.aether.RepositorySystemSession;
53 import org.eclipse.aether.RequestTrace;
54 import org.eclipse.aether.SessionData;
55 import org.eclipse.aether.metadata.DefaultMetadata;
56 import org.eclipse.aether.repository.ArtifactRepository;
57 import org.eclipse.aether.repository.RemoteRepository;
58 import org.eclipse.aether.resolution.MetadataRequest;
59 import org.eclipse.aether.resolution.MetadataResult;
60 import org.eclipse.aether.version.InvalidVersionSpecificationException;
61 import org.eclipse.aether.version.Version;
62 import org.eclipse.aether.version.VersionScheme;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66
67
68
69
70
71 @Named
72 @Singleton
73 public class DefaultPluginVersionResolver implements PluginVersionResolver {
74 private static final String REPOSITORY_CONTEXT = "plugin";
75
76 private static final Object CACHE_KEY = new Object();
77
78 private final Logger logger = LoggerFactory.getLogger(getClass());
79 private final RepositorySystem repositorySystem;
80 private final MetadataReader metadataReader;
81 private final MavenPluginManager pluginManager;
82 private final VersionScheme versionScheme;
83
84 @Inject
85 public DefaultPluginVersionResolver(
86 RepositorySystem repositorySystem,
87 MetadataReader metadataReader,
88 MavenPluginManager pluginManager,
89 VersionScheme versionScheme) {
90 this.repositorySystem = repositorySystem;
91 this.metadataReader = metadataReader;
92 this.pluginManager = pluginManager;
93 this.versionScheme = versionScheme;
94 }
95
96 @Override
97 public PluginVersionResult resolve(PluginVersionRequest request) throws PluginVersionResolutionException {
98 PluginVersionResult result = resolveFromProject(request);
99
100 if (result == null) {
101 ConcurrentMap<Key, PluginVersionResult> cache = getCache(request);
102 Key key = getKey(request);
103 result = cache.get(key);
104
105 if (result == null) {
106 result = resolveFromRepository(request);
107
108 logger.debug(
109 "Resolved plugin version for {}:{} to {} from repository {}",
110 request.getGroupId(),
111 request.getArtifactId(),
112 result.getVersion(),
113 result.getRepository());
114
115 cache.putIfAbsent(key, result);
116 } else {
117 logger.debug(
118 "Reusing cached resolved plugin version for {}:{} to {} from POM {}",
119 request.getGroupId(),
120 request.getArtifactId(),
121 result.getVersion(),
122 request.getPom());
123 }
124 } else {
125 logger.debug(
126 "Reusing cached resolved plugin version for {}:{} to {} from POM {}",
127 request.getGroupId(),
128 request.getArtifactId(),
129 result.getVersion(),
130 request.getPom());
131 }
132
133 return result;
134 }
135
136 private PluginVersionResult resolveFromRepository(PluginVersionRequest request)
137 throws PluginVersionResolutionException {
138 RequestTrace trace = RequestTrace.newChild(null, request);
139
140 DefaultPluginVersionResult result = new DefaultPluginVersionResult();
141
142 org.eclipse.aether.metadata.Metadata metadata = new DefaultMetadata(
143 request.getGroupId(),
144 request.getArtifactId(),
145 "maven-metadata.xml",
146 DefaultMetadata.Nature.RELEASE_OR_SNAPSHOT);
147
148 List<MetadataRequest> requests = new ArrayList<>();
149
150 requests.add(new MetadataRequest(metadata, null, REPOSITORY_CONTEXT).setTrace(trace));
151
152 for (RemoteRepository repository : request.getRepositories()) {
153 requests.add(new MetadataRequest(metadata, repository, REPOSITORY_CONTEXT).setTrace(trace));
154 }
155
156 List<MetadataResult> results = repositorySystem.resolveMetadata(request.getRepositorySession(), requests);
157
158 Versions versions = new Versions();
159
160 for (MetadataResult res : results) {
161 ArtifactRepository repository = res.getRequest().getRepository();
162 if (repository == null) {
163 repository = request.getRepositorySession().getLocalRepository();
164 }
165
166 mergeMetadata(request.getRepositorySession(), trace, versions, res.getMetadata(), repository);
167 }
168
169 selectVersion(result, request, versions);
170
171 return result;
172 }
173
174 private void selectVersion(DefaultPluginVersionResult result, PluginVersionRequest request, Versions versions)
175 throws PluginVersionResolutionException {
176 String version = null;
177 ArtifactRepository repo = null;
178
179 if (versions.releaseVersion != null && !versions.releaseVersion.isEmpty()) {
180 version = versions.releaseVersion;
181 repo = versions.releaseRepository;
182 } else if (versions.latestVersion != null && !versions.latestVersion.isEmpty()) {
183 version = versions.latestVersion;
184 repo = versions.latestRepository;
185 }
186 if (version != null && !isCompatible(request, version)) {
187 versions.versions.remove(version);
188 version = null;
189 }
190
191 if (version == null) {
192 TreeSet<Version> releases = new TreeSet<>(Collections.reverseOrder());
193 TreeSet<Version> snapshots = new TreeSet<>(Collections.reverseOrder());
194
195 for (String ver : versions.versions.keySet()) {
196 try {
197 Version v = versionScheme.parseVersion(ver);
198
199 if (ver.endsWith("-SNAPSHOT")) {
200 snapshots.add(v);
201 } else {
202 releases.add(v);
203 }
204 } catch (InvalidVersionSpecificationException e) {
205
206 }
207 }
208
209 for (Version v : releases) {
210 String ver = v.toString();
211 if (isCompatible(request, ver)) {
212 version = ver;
213 repo = versions.versions.get(version);
214 break;
215 }
216 }
217
218 if (version == null) {
219 for (Version v : snapshots) {
220 String ver = v.toString();
221 if (isCompatible(request, ver)) {
222 version = ver;
223 repo = versions.versions.get(version);
224 break;
225 }
226 }
227 }
228 }
229
230 if (version != null) {
231 result.setVersion(version);
232 result.setRepository(repo);
233 } else {
234 throw new PluginVersionResolutionException(
235 request.getGroupId(),
236 request.getArtifactId(),
237 request.getRepositorySession().getLocalRepository(),
238 request.getRepositories(),
239 "Plugin not found in any plugin repository");
240 }
241 }
242
243 private boolean isCompatible(PluginVersionRequest request, String version) {
244 Plugin plugin = new Plugin();
245 plugin.setGroupId(request.getGroupId());
246 plugin.setArtifactId(request.getArtifactId());
247 plugin.setVersion(version);
248
249 PluginDescriptor pluginDescriptor;
250
251 try {
252 pluginDescriptor = pluginManager.getPluginDescriptor(
253 plugin, request.getRepositories(), request.getRepositorySession());
254 } catch (PluginResolutionException e) {
255 logger.debug("Ignoring unresolvable plugin version {}", version, e);
256 return false;
257 } catch (Exception e) {
258
259 return true;
260 }
261
262 try {
263 pluginManager.checkPrerequisites(pluginDescriptor);
264 } catch (Exception e) {
265 logger.warn("Ignoring incompatible plugin version {}", version, e);
266 return false;
267 }
268
269 return true;
270 }
271
272 private void mergeMetadata(
273 RepositorySystemSession session,
274 RequestTrace trace,
275 Versions versions,
276 org.eclipse.aether.metadata.Metadata metadata,
277 ArtifactRepository repository) {
278 if (metadata != null && metadata.getFile() != null && metadata.getFile().isFile()) {
279 try {
280 Map<String, ?> options = Collections.singletonMap(MetadataReader.IS_STRICT, Boolean.FALSE);
281
282 Metadata repoMetadata = metadataReader.read(metadata.getFile(), options);
283
284 mergeMetadata(versions, repoMetadata, repository);
285 } catch (IOException e) {
286 invalidMetadata(session, trace, metadata, repository, e);
287 }
288 }
289 }
290
291 private void invalidMetadata(
292 RepositorySystemSession session,
293 RequestTrace trace,
294 org.eclipse.aether.metadata.Metadata metadata,
295 ArtifactRepository repository,
296 Exception exception) {
297 RepositoryListener listener = session.getRepositoryListener();
298 if (listener != null) {
299 RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID);
300 event.setTrace(trace);
301 event.setMetadata(metadata);
302 event.setException(exception);
303 event.setRepository(repository);
304 listener.metadataInvalid(event.build());
305 }
306 }
307
308 private void mergeMetadata(Versions versions, Metadata source, ArtifactRepository repository) {
309 Versioning versioning = source.getVersioning();
310 if (versioning != null) {
311 String timestamp = versioning.getLastUpdated() == null
312 ? ""
313 : versioning.getLastUpdated().trim();
314
315 if (versioning.getRelease() != null
316 && !versioning.getRelease().isEmpty()
317 && timestamp.compareTo(versions.releaseTimestamp) > 0) {
318 versions.releaseVersion = versioning.getRelease();
319 versions.releaseTimestamp = timestamp;
320 versions.releaseRepository = repository;
321 }
322
323 if (versioning.getLatest() != null
324 && !versioning.getLatest().isEmpty()
325 && timestamp.compareTo(versions.latestTimestamp) > 0) {
326 versions.latestVersion = versioning.getLatest();
327 versions.latestTimestamp = timestamp;
328 versions.latestRepository = repository;
329 }
330
331 for (String version : versioning.getVersions()) {
332 if (!versions.versions.containsKey(version)) {
333 versions.versions.put(version, repository);
334 }
335 }
336 }
337 }
338
339 private PluginVersionResult resolveFromProject(PluginVersionRequest request) {
340 PluginVersionResult result = null;
341
342 if (request.getPom() != null && request.getPom().getBuild() != null) {
343 Build build = request.getPom().getBuild();
344
345 result = resolveFromProject(request, build.getPlugins());
346
347 if (result == null && build.getPluginManagement() != null) {
348 result = resolveFromProject(request, build.getPluginManagement().getPlugins());
349 }
350 }
351
352 return result;
353 }
354
355 private PluginVersionResult resolveFromProject(PluginVersionRequest request, List<Plugin> plugins) {
356 for (Plugin plugin : plugins) {
357 if (request.getGroupId().equals(plugin.getGroupId())
358 && request.getArtifactId().equals(plugin.getArtifactId())) {
359 if (plugin.getVersion() != null) {
360 return new DefaultPluginVersionResult(plugin.getVersion());
361 } else {
362 return null;
363 }
364 }
365 }
366 return null;
367 }
368
369 @SuppressWarnings("unchecked")
370 private ConcurrentMap<Key, PluginVersionResult> getCache(PluginVersionRequest request) {
371 SessionData data = request.getRepositorySession().getData();
372 return (ConcurrentMap<Key, PluginVersionResult>)
373 data.computeIfAbsent(CACHE_KEY, () -> new ConcurrentHashMap<>(256));
374 }
375
376 private static Key getKey(PluginVersionRequest request) {
377 return new Key(request.getGroupId(), request.getArtifactId(), request.getRepositories());
378 }
379
380 static class Key {
381 final String groupId;
382 final String artifactId;
383 final List<RemoteRepository> repositories;
384 final int hash;
385
386 Key(String groupId, String artifactId, List<RemoteRepository> repositories) {
387 this.groupId = groupId;
388 this.artifactId = artifactId;
389 this.repositories = repositories;
390 this.hash = Objects.hash(groupId, artifactId, repositories);
391 }
392
393 @Override
394 public boolean equals(Object o) {
395 if (this == o) {
396 return true;
397 }
398 if (o == null || getClass() != o.getClass()) {
399 return false;
400 }
401 Key key = (Key) o;
402 return groupId.equals(key.groupId)
403 && artifactId.equals(key.artifactId)
404 && repositories.equals(key.repositories);
405 }
406
407 @Override
408 public int hashCode() {
409 return hash;
410 }
411 }
412
413 static class Versions {
414
415 String releaseVersion = "";
416
417 String releaseTimestamp = "";
418
419 ArtifactRepository releaseRepository;
420
421 String latestVersion = "";
422
423 String latestTimestamp = "";
424
425 ArtifactRepository latestRepository;
426
427 Map<String, ArtifactRepository> versions = new LinkedHashMap<>();
428 }
429 }