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