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