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