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