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