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.internal.transformation.impl;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  
24  import java.nio.file.Path;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.function.Function;
29  import java.util.stream.Collectors;
30  
31  import org.apache.maven.api.ArtifactCoordinates;
32  import org.apache.maven.api.Node;
33  import org.apache.maven.api.PathScope;
34  import org.apache.maven.api.SessionData;
35  import org.apache.maven.api.model.Dependency;
36  import org.apache.maven.api.model.DistributionManagement;
37  import org.apache.maven.api.model.Model;
38  import org.apache.maven.api.model.ModelBase;
39  import org.apache.maven.api.model.Profile;
40  import org.apache.maven.api.model.Repository;
41  import org.apache.maven.api.model.Scm;
42  import org.apache.maven.api.services.ModelBuilder;
43  import org.apache.maven.api.services.ModelBuilderException;
44  import org.apache.maven.api.services.ModelBuilderRequest;
45  import org.apache.maven.api.services.ModelBuilderResult;
46  import org.apache.maven.api.services.Sources;
47  import org.apache.maven.api.services.model.LifecycleBindingsInjector;
48  import org.apache.maven.impl.InternalSession;
49  import org.apache.maven.model.v4.MavenModelVersion;
50  import org.apache.maven.project.MavenProject;
51  import org.eclipse.aether.RepositorySystemSession;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  @Named
56  class DefaultConsumerPomBuilder implements PomBuilder {
57      private static final String BOM_PACKAGING = "bom";
58  
59      public static final String POM_PACKAGING = "pom";
60  
61      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultConsumerPomBuilder.class);
62  
63      private final LifecycleBindingsInjector lifecycleBindingsInjector;
64  
65      @Inject
66      @SuppressWarnings("checkstyle:ParameterNumber")
67      DefaultConsumerPomBuilder(LifecycleBindingsInjector lifecycleBindingsInjector) {
68          this.lifecycleBindingsInjector = lifecycleBindingsInjector;
69      }
70  
71      @Override
72      public Model build(RepositorySystemSession session, MavenProject project, Path src) throws ModelBuilderException {
73          Model model = project.getModel().getDelegate();
74          String packaging = model.getPackaging();
75          String originalPackaging = project.getOriginalModel().getPackaging();
76          if (POM_PACKAGING.equals(packaging)) {
77              if (BOM_PACKAGING.equals(originalPackaging)) {
78                  return buildBom(session, project, src);
79              } else {
80                  return buildPom(session, project, src);
81              }
82          } else {
83              return buildNonPom(session, project, src);
84          }
85      }
86  
87      protected Model buildPom(RepositorySystemSession session, MavenProject project, Path src)
88              throws ModelBuilderException {
89          ModelBuilderResult result = buildModel(session, src);
90          Model model = result.getRawModel();
91          return transformPom(model, project);
92      }
93  
94      protected Model buildBom(RepositorySystemSession session, MavenProject project, Path src)
95              throws ModelBuilderException {
96          ModelBuilderResult result = buildModel(session, src);
97          Model model = result.getEffectiveModel();
98          return transformBom(model, project);
99      }
100 
101     protected Model buildNonPom(RepositorySystemSession session, MavenProject project, Path src)
102             throws ModelBuilderException {
103         Model model = buildEffectiveModel(session, src);
104         return transformNonPom(model, project);
105     }
106 
107     private Model buildEffectiveModel(RepositorySystemSession session, Path src) throws ModelBuilderException {
108         InternalSession iSession = InternalSession.from(session);
109         ModelBuilderResult result = buildModel(session, src);
110         Model model = result.getEffectiveModel();
111 
112         if (model.getDependencyManagement() != null
113                 && !model.getDependencyManagement().getDependencies().isEmpty()) {
114             ArtifactCoordinates artifact = iSession.createArtifactCoordinates(
115                     model.getGroupId(), model.getArtifactId(), model.getVersion(), null);
116             Node node = iSession.collectDependencies(
117                     iSession.createDependencyCoordinates(artifact), PathScope.TEST_RUNTIME);
118 
119             Map<String, Node> nodes = node.stream()
120                     .collect(Collectors.toMap(n -> getDependencyKey(n.getDependency()), Function.identity()));
121             Map<String, Dependency> directDependencies = model.getDependencies().stream()
122                     .filter(dependency -> !"import".equals(dependency.getScope()))
123                     .collect(Collectors.toMap(
124                             DefaultConsumerPomBuilder::getDependencyKey,
125                             Function.identity(),
126                             this::merge,
127                             LinkedHashMap::new));
128             Map<String, Dependency> managedDependencies = model.getDependencyManagement().getDependencies().stream()
129                     .filter(dependency ->
130                             nodes.containsKey(getDependencyKey(dependency)) && !"import".equals(dependency.getScope()))
131                     .collect(Collectors.toMap(
132                             DefaultConsumerPomBuilder::getDependencyKey,
133                             Function.identity(),
134                             this::merge,
135                             LinkedHashMap::new));
136 
137             // for each managed dep in the model:
138             // * if there is no corresponding node in the tree, discard the managed dep
139             // * if there's a direct dependency, apply the managed dependency to it and discard the managed dep
140             // * else keep the managed dep
141             managedDependencies.keySet().retainAll(nodes.keySet());
142 
143             directDependencies.replaceAll((key, dependency) -> {
144                 var managedDependency = managedDependencies.get(key);
145                 if (managedDependency != null) {
146                     if (dependency.getVersion() == null && managedDependency.getVersion() != null) {
147                         dependency = dependency.withVersion(managedDependency.getVersion());
148                     }
149                     if (dependency.getScope() == null && managedDependency.getScope() != null) {
150                         dependency = dependency.withScope(managedDependency.getScope());
151                     }
152                     if (dependency.getOptional() == null && managedDependency.getOptional() != null) {
153                         dependency = dependency.withOptional(managedDependency.getOptional());
154                     }
155                     if (dependency.getExclusions().isEmpty()
156                             && !managedDependency.getExclusions().isEmpty()) {
157                         dependency = dependency.withExclusions(managedDependency.getExclusions());
158                     }
159                 }
160                 return dependency;
161             });
162             managedDependencies.keySet().removeAll(directDependencies.keySet());
163 
164             model = model.withDependencyManagement(
165                             managedDependencies.isEmpty()
166                                     ? null
167                                     : model.getDependencyManagement().withDependencies(managedDependencies.values()))
168                     .withDependencies(directDependencies.isEmpty() ? null : directDependencies.values());
169         }
170 
171         return model;
172     }
173 
174     private Dependency merge(Dependency dep1, Dependency dep2) {
175         throw new IllegalArgumentException("Duplicate dependency: " + dep1);
176     }
177 
178     private static String getDependencyKey(org.apache.maven.api.Dependency dependency) {
179         return dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
180                 + dependency.getType().id() + ":" + dependency.getClassifier();
181     }
182 
183     private static String getDependencyKey(Dependency dependency) {
184         return dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
185                 + (dependency.getType() != null ? dependency.getType() : "") + ":"
186                 + (dependency.getClassifier() != null ? dependency.getClassifier() : "");
187     }
188 
189     private ModelBuilderResult buildModel(RepositorySystemSession session, Path src) throws ModelBuilderException {
190         InternalSession iSession = InternalSession.from(session);
191         ModelBuilderRequest.ModelBuilderRequestBuilder request = ModelBuilderRequest.builder();
192         request.requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER);
193         request.session(iSession);
194         request.source(Sources.buildSource(src));
195         request.locationTracking(false);
196         request.systemProperties(session.getSystemProperties());
197         request.userProperties(session.getUserProperties());
198         request.lifecycleBindingsInjector(lifecycleBindingsInjector::injectLifecycleBindings);
199         ModelBuilder.ModelBuilderSession mbSession =
200                 iSession.getData().get(SessionData.key(ModelBuilder.ModelBuilderSession.class));
201         return mbSession.build(request.build());
202     }
203 
204     static Model transformNonPom(Model model, MavenProject project) {
205         boolean preserveModelVersion = model.isPreserveModelVersion();
206 
207         Model.Builder builder = prune(
208                         Model.newBuilder(model, true)
209                                 .preserveModelVersion(false)
210                                 .root(false)
211                                 .parent(null)
212                                 .build(null),
213                         model)
214                 .mailingLists(null)
215                 .issueManagement(null)
216                 .scm(
217                         model.getScm() != null
218                                 ? Scm.newBuilder(model.getScm(), true)
219                                         .childScmConnectionInheritAppendPath(null)
220                                         .childScmUrlInheritAppendPath(null)
221                                         .childScmDeveloperConnectionInheritAppendPath(null)
222                                         .build()
223                                 : null);
224         builder.profiles(prune(model.getProfiles()));
225 
226         model = builder.build();
227         String modelVersion = new MavenModelVersion().getModelVersion(model);
228         if (!ModelBuilder.MODEL_VERSION_4_0_0.equals(modelVersion) && !preserveModelVersion) {
229             warnNotDowngraded(project);
230         }
231         model = model.withModelVersion(modelVersion);
232         return model;
233     }
234 
235     static Model transformBom(Model model, MavenProject project) {
236         boolean preserveModelVersion = model.isPreserveModelVersion();
237 
238         Model.Builder builder = prune(
239                 Model.newBuilder(model, true)
240                         .preserveModelVersion(false)
241                         .root(false)
242                         .parent(null)
243                         .build(null),
244                 model);
245         builder.packaging(POM_PACKAGING);
246         builder.profiles(prune(model.getProfiles()));
247 
248         model = builder.build();
249         String modelVersion = new MavenModelVersion().getModelVersion(model);
250         if (!ModelBuilder.MODEL_VERSION_4_0_0.equals(modelVersion) && !preserveModelVersion) {
251             warnNotDowngraded(project);
252         }
253         model = model.withModelVersion(modelVersion);
254         return model;
255     }
256 
257     static Model transformPom(Model model, MavenProject project) {
258         boolean preserveModelVersion = model.isPreserveModelVersion();
259 
260         // raw to consumer transform
261         model = model.withRoot(false).withModules(null).withSubprojects(null);
262         if (model.getParent() != null) {
263             model = model.withParent(model.getParent().withRelativePath(null));
264         }
265 
266         if (!preserveModelVersion) {
267             model = model.withPreserveModelVersion(false);
268             String modelVersion = new MavenModelVersion().getModelVersion(model);
269             model = model.withModelVersion(modelVersion);
270         }
271         return model;
272     }
273 
274     static void warnNotDowngraded(MavenProject project) {
275         LOGGER.warn("The consumer POM for " + project.getId() + " cannot be downgraded to 4.0.0. "
276                 + "If you intent your build to be consumed with Maven 3 projects, you need to remove "
277                 + "the features that request a newer model version.  If you're fine with having the "
278                 + "consumer POM not consumable with Maven 3, add the `preserve.model.version='true'` "
279                 + "attribute on the <project> element of your POM.");
280     }
281 
282     private static List<Profile> prune(List<Profile> profiles) {
283         return profiles.stream()
284                 .map(p -> {
285                     Profile.Builder builder = Profile.newBuilder(p, true);
286                     prune((ModelBase.Builder) builder, p);
287                     return builder.build(null).build();
288                 })
289                 .filter(p -> !isEmpty(p))
290                 .collect(Collectors.toList());
291     }
292 
293     private static boolean isEmpty(Profile profile) {
294         return profile.getActivation() == null
295                 && profile.getBuild() == null
296                 && profile.getDependencies().isEmpty()
297                 && (profile.getDependencyManagement() == null
298                         || profile.getDependencyManagement().getDependencies().isEmpty())
299                 && profile.getDistributionManagement() == null
300                 && profile.getModules().isEmpty()
301                 && profile.getSubprojects().isEmpty()
302                 && profile.getProperties().isEmpty()
303                 && profile.getRepositories().isEmpty()
304                 && profile.getPluginRepositories().isEmpty()
305                 && profile.getReporting() == null;
306     }
307 
308     private static <T extends ModelBase.Builder> T prune(T builder, ModelBase model) {
309         builder.properties(null).reporting(null);
310         if (model.getDistributionManagement() != null
311                 && model.getDistributionManagement().getRelocation() != null) {
312             // keep relocation only
313             builder.distributionManagement(DistributionManagement.newBuilder()
314                     .relocation(model.getDistributionManagement().getRelocation())
315                     .build());
316         }
317         // only keep repositories other than 'central'
318         builder.repositories(pruneRepositories(model.getRepositories()));
319         builder.pluginRepositories(null);
320         return builder;
321     }
322 
323     private static List<Repository> pruneRepositories(List<Repository> repositories) {
324         return repositories.stream()
325                 .filter(r -> !org.apache.maven.api.Repository.CENTRAL_ID.equals(r.getId()))
326                 .collect(Collectors.toList());
327     }
328 }