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