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