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