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.prefix.internal;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.LinkedHashMap;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  import org.apache.maven.artifact.repository.metadata.Metadata;
34  import org.apache.maven.artifact.repository.metadata.io.MetadataReader;
35  import org.apache.maven.model.Build;
36  import org.apache.maven.model.Model;
37  import org.apache.maven.model.Plugin;
38  import org.apache.maven.model.PluginManagement;
39  import org.apache.maven.plugin.BuildPluginManager;
40  import org.apache.maven.plugin.descriptor.PluginDescriptor;
41  import org.apache.maven.plugin.prefix.NoPluginFoundForPrefixException;
42  import org.apache.maven.plugin.prefix.PluginPrefixRequest;
43  import org.apache.maven.plugin.prefix.PluginPrefixResolver;
44  import org.apache.maven.plugin.prefix.PluginPrefixResult;
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.eclipse.aether.DefaultRepositorySystemSession;
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.metadata.DefaultMetadata;
56  import org.eclipse.aether.repository.ArtifactRepository;
57  import org.eclipse.aether.repository.RemoteRepository;
58  import org.eclipse.aether.repository.RepositoryPolicy;
59  import org.eclipse.aether.resolution.MetadataRequest;
60  import org.eclipse.aether.resolution.MetadataResult;
61  
62  /**
63   * Resolves a plugin prefix.
64   *
65   * @since 3.0
66   * @author Benjamin Bentmann
67   */
68  @Component(role = PluginPrefixResolver.class)
69  public class DefaultPluginPrefixResolver implements PluginPrefixResolver {
70  
71      private static final String REPOSITORY_CONTEXT = "plugin";
72  
73      @Requirement
74      private Logger logger;
75  
76      @Requirement
77      private BuildPluginManager pluginManager;
78  
79      @Requirement
80      private RepositorySystem repositorySystem;
81  
82      @Requirement
83      private MetadataReader metadataReader;
84  
85      public PluginPrefixResult resolve(PluginPrefixRequest request) throws NoPluginFoundForPrefixException {
86          logger.debug("Resolving plugin prefix " + request.getPrefix() + " from " + request.getPluginGroups());
87  
88          Model pom = request.getPom();
89          Build build = pom != null ? pom.getBuild() : null;
90          PluginManagement management = build != null ? build.getPluginManagement() : null;
91  
92          // map of groupId -> Set(artifactId) plugin candidates:
93          // if value is null, keys are coming from settings, and no artifactId filtering is applied
94          // if value is non-null: we allow only plugins that have enlisted artifactId only
95          // ---
96          // end game is: settings enlisted groupIds are obeying order and are "free for all" (artifactId)
97          // while POM enlisted plugins coming from non-enlisted settings groupIds (ie conflict of prefixes)
98          // will prevail/win.
99          LinkedHashMap<String, Set<String>> candidates = Stream.of(build, management)
100                 .flatMap(container -> container != null ? container.getPlugins().stream() : Stream.empty())
101                 .filter(p -> !request.getPluginGroups().contains(p.getGroupId()))
102                 .collect(Collectors.groupingBy(
103                         Plugin::getGroupId,
104                         LinkedHashMap::new,
105                         Collectors.mapping(Plugin::getArtifactId, Collectors.toSet())));
106         request.getPluginGroups().forEach(g -> candidates.put(g, null));
107         PluginPrefixResult result = null;
108         // First, we go through all declared plugins, load them
109         // one by one, and try to find a matching prefix.
110         if (build != null) {
111             result = resolveFromProject(
112                     request,
113                     build.getPlugins(),
114                     management != null ? management.getPlugins() : Collections.emptyList());
115         }
116 
117         // Second, we go use G level metadata to discover prefix
118         // This order allows user managed clashing prefixes (they can declare them in POM)
119         if (result == null) {
120             result = resolveFromRepository(request, candidates);
121         }
122 
123         if (result == null) {
124             throw new NoPluginFoundForPrefixException(
125                     request.getPrefix(),
126                     new ArrayList<>(candidates.keySet()),
127                     request.getRepositorySession().getLocalRepository(),
128                     request.getRepositories());
129         } else {
130             logger.debug("Resolved plugin prefix " + request.getPrefix() + " to " + result.getGroupId() + ":"
131                     + result.getArtifactId() + " from repository "
132                     + (result.getRepository() != null ? result.getRepository().getId() : "null"));
133         }
134 
135         return result;
136     }
137 
138     private PluginPrefixResult resolveFromProject(
139             PluginPrefixRequest request, List<Plugin> plugins, List<Plugin> pluginMgmt) {
140         if (plugins.isEmpty() && pluginMgmt.isEmpty()) {
141             return null;
142         }
143         PluginPrefixResult result = null;
144         // try optimistically; first if A contains prefix?
145         Set<Plugin> candidates = new LinkedHashSet<>();
146         Stream.concat(plugins.stream(), pluginMgmt.stream())
147                 .filter(p -> p.getArtifactId().contains(request.getPrefix()))
148                 .forEach(candidates::add);
149         if (!candidates.isEmpty()) {
150             result = doResolveFromProject(request, candidates);
151         }
152         // if no luck; try the rest
153         if (result == null) {
154             Set<Plugin> remainder = new LinkedHashSet<>(plugins);
155             remainder.removeAll(candidates);
156             result = doResolveFromProject(request, remainder);
157         }
158         return result;
159     }
160 
161     private PluginPrefixResult doResolveFromProject(PluginPrefixRequest request, Collection<Plugin> plugins) {
162         for (Plugin plugin : plugins) {
163             try {
164                 PluginDescriptor pluginDescriptor =
165                         pluginManager.loadPlugin(plugin, request.getRepositories(), request.getRepositorySession());
166 
167                 if (request.getPrefix().equals(pluginDescriptor.getGoalPrefix())) {
168                     return new DefaultPluginPrefixResult(plugin);
169                 }
170             } catch (Exception e) {
171                 if (logger.isDebugEnabled()) {
172                     logger.warn(
173                             "Failed to retrieve plugin descriptor for " + plugin.getId() + ": " + e.getMessage(), e);
174                 } else {
175                     logger.warn("Failed to retrieve plugin descriptor for " + plugin.getId() + ": " + e.getMessage());
176                 }
177             }
178         }
179 
180         return null;
181     }
182 
183     private PluginPrefixResult resolveFromRepository(
184             PluginPrefixRequest request, LinkedHashMap<String, Set<String>> candidates) {
185         RequestTrace trace = RequestTrace.newChild(null, request);
186 
187         List<MetadataRequest> requests = new ArrayList<>();
188 
189         for (String pluginGroup : candidates.keySet()) {
190             org.eclipse.aether.metadata.Metadata metadata =
191                     new DefaultMetadata(pluginGroup, "maven-metadata.xml", DefaultMetadata.Nature.RELEASE_OR_SNAPSHOT);
192 
193             requests.add(new MetadataRequest(metadata, null, REPOSITORY_CONTEXT).setTrace(trace));
194 
195             for (RemoteRepository repository : request.getRepositories()) {
196                 requests.add(new MetadataRequest(metadata, repository, REPOSITORY_CONTEXT).setTrace(trace));
197             }
198         }
199 
200         // initial try, use locally cached metadata
201 
202         List<MetadataResult> results = repositorySystem.resolveMetadata(request.getRepositorySession(), requests);
203         requests.clear();
204 
205         PluginPrefixResult result = processResults(request, trace, results, requests, candidates);
206 
207         if (result != null) {
208             return result;
209         }
210 
211         // second try, refetch all (possibly outdated) metadata that wasn't updated in the first attempt
212 
213         if (!request.getRepositorySession().isOffline() && !requests.isEmpty()) {
214             DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(request.getRepositorySession());
215             session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS);
216 
217             results = repositorySystem.resolveMetadata(session, requests);
218 
219             return processResults(request, trace, results, null, candidates);
220         }
221 
222         return null;
223     }
224 
225     private PluginPrefixResult processResults(
226             PluginPrefixRequest request,
227             RequestTrace trace,
228             List<MetadataResult> results,
229             List<MetadataRequest> requests,
230             LinkedHashMap<String, Set<String>> candidates) {
231         for (MetadataResult res : results) {
232             org.eclipse.aether.metadata.Metadata metadata = res.getMetadata();
233 
234             if (metadata != null) {
235                 ArtifactRepository repository = res.getRequest().getRepository();
236                 if (repository == null) {
237                     repository = request.getRepositorySession().getLocalRepository();
238                 }
239 
240                 PluginPrefixResult result =
241                         resolveFromRepository(request, trace, metadata.getGroupId(), metadata, repository, candidates);
242 
243                 if (result != null) {
244                     return result;
245                 }
246             }
247 
248             if (requests != null && !res.isUpdated()) {
249                 requests.add(res.getRequest());
250             }
251         }
252 
253         return null;
254     }
255 
256     private PluginPrefixResult resolveFromRepository(
257             PluginPrefixRequest request,
258             RequestTrace trace,
259             String pluginGroup,
260             org.eclipse.aether.metadata.Metadata metadata,
261             ArtifactRepository repository,
262             LinkedHashMap<String, Set<String>> candidates) {
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 pluginGroupMetadata = metadataReader.read(metadata.getFile(), options);
268 
269                 List<org.apache.maven.artifact.repository.metadata.Plugin> plugins = pluginGroupMetadata.getPlugins();
270 
271                 if (plugins != null) {
272                     for (org.apache.maven.artifact.repository.metadata.Plugin plugin : plugins) {
273                         if (request.getPrefix().equals(plugin.getPrefix())
274                                 && (candidates.get(pluginGroup) == null
275                                         || candidates.get(pluginGroup).contains(plugin.getArtifactId()))) {
276                             return new DefaultPluginPrefixResult(pluginGroup, plugin.getArtifactId(), repository);
277                         }
278                     }
279                 }
280             } catch (IOException e) {
281                 invalidMetadata(request.getRepositorySession(), trace, metadata, repository, e);
282             }
283         }
284 
285         return null;
286     }
287 
288     private void invalidMetadata(
289             RepositorySystemSession session,
290             RequestTrace trace,
291             org.eclipse.aether.metadata.Metadata metadata,
292             ArtifactRepository repository,
293             Exception exception) {
294         RepositoryListener listener = session.getRepositoryListener();
295         if (listener != null) {
296             RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID);
297             event.setTrace(trace);
298             event.setMetadata(metadata);
299             event.setException(exception);
300             event.setRepository(repository);
301             listener.metadataInvalid(event.build());
302         }
303     }
304 }