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;
20  
21  import javax.annotation.PreDestroy;
22  import javax.inject.Inject;
23  import javax.inject.Named;
24  import javax.inject.Singleton;
25  import javax.xml.stream.XMLStreamException;
26  
27  import java.io.IOException;
28  import java.io.Writer;
29  import java.lang.reflect.Method;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.util.ArrayList;
34  import java.util.Collection;
35  import java.util.List;
36  import java.util.Set;
37  import java.util.concurrent.CopyOnWriteArraySet;
38  import java.util.stream.Collectors;
39  
40  import org.apache.maven.api.Repository;
41  import org.apache.maven.api.feature.Features;
42  import org.apache.maven.api.model.DistributionManagement;
43  import org.apache.maven.api.model.Model;
44  import org.apache.maven.api.model.ModelBase;
45  import org.apache.maven.api.model.Profile;
46  import org.apache.maven.model.building.FileModelSource;
47  import org.apache.maven.model.building.ModelBuilder;
48  import org.apache.maven.model.building.ModelBuildingRequest;
49  import org.apache.maven.model.building.ModelCache;
50  import org.apache.maven.model.building.Result;
51  import org.apache.maven.model.building.TransformerContext;
52  import org.apache.maven.model.v4.MavenModelVersion;
53  import org.apache.maven.model.v4.MavenStaxWriter;
54  import org.apache.maven.project.MavenProject;
55  import org.apache.maven.project.artifact.ProjectArtifact;
56  import org.apache.maven.repository.internal.DefaultModelCache;
57  import org.eclipse.aether.RepositorySystemSession;
58  import org.eclipse.aether.artifact.Artifact;
59  import org.eclipse.aether.artifact.DefaultArtifact;
60  import org.eclipse.aether.deployment.DeployRequest;
61  import org.eclipse.aether.installation.InstallRequest;
62  
63  /**
64   * Consumer POM transformer.
65   *
66   * @since TBD
67   */
68  @Singleton
69  @Named("consumer-pom")
70  public final class ConsumerPomArtifactTransformer {
71  
72      private static final String BOM_PACKAGING = "bom";
73  
74      public static final String POM_PACKAGING = "pom";
75  
76      private static final String CONSUMER_POM_CLASSIFIER = "consumer";
77  
78      private static final String BUILD_POM_CLASSIFIER = "build";
79  
80      private static final String NAMESPACE_FORMAT = "http://maven.apache.org/POM/%s";
81  
82      private static final String SCHEMA_LOCATION_FORMAT = "https://maven.apache.org/xsd/maven-%s.xsd";
83  
84      private final Set<Path> toDelete = new CopyOnWriteArraySet<>();
85  
86      private final ModelBuilder modelBuilder;
87  
88      @Inject
89      ConsumerPomArtifactTransformer(ModelBuilder modelBuilder) {
90          this.modelBuilder = modelBuilder;
91      }
92  
93      public void injectTransformedArtifacts(MavenProject project, RepositorySystemSession session) throws IOException {
94          if (project.getFile() == null) {
95              // If there is no build POM there is no reason to inject artifacts for the consumer POM.
96              return;
97          }
98          if (Features.buildConsumer(session.getUserProperties())) {
99              Path buildDir =
100                     project.getBuild() != null ? Paths.get(project.getBuild().getDirectory()) : null;
101             if (buildDir != null) {
102                 Files.createDirectories(buildDir);
103             }
104             Path consumer = buildDir != null
105                     ? Files.createTempFile(buildDir, CONSUMER_POM_CLASSIFIER + "-", ".pom")
106                     : Files.createTempFile(CONSUMER_POM_CLASSIFIER + "-", ".pom");
107             deferDeleteFile(consumer);
108 
109             project.addAttachedArtifact(createConsumerPomArtifact(project, consumer, session));
110         } else if (project.getModel().isRoot()) {
111             throw new IllegalStateException(
112                     "The use of the root attribute on the model requires the buildconsumer feature to be active");
113         }
114     }
115 
116     public ConsumerPomArtifact createConsumerPomArtifact(
117             MavenProject project, Path consumer, RepositorySystemSession session) {
118         return new ConsumerPomArtifact(project, consumer, session);
119     }
120 
121     private void deferDeleteFile(Path generatedFile) {
122         toDelete.add(generatedFile.toAbsolutePath());
123     }
124 
125     @PreDestroy
126     private void doDeleteFiles() {
127         for (Path file : toDelete) {
128             try {
129                 Files.delete(file);
130             } catch (IOException e) {
131                 // ignore, we did our best...
132             }
133         }
134     }
135 
136     public InstallRequest remapInstallArtifacts(RepositorySystemSession session, InstallRequest request) {
137         if (Features.buildConsumer(session.getUserProperties()) && consumerPomPresent(request.getArtifacts())) {
138             request.setArtifacts(replacePom(request.getArtifacts()));
139         }
140         return request;
141     }
142 
143     public DeployRequest remapDeployArtifacts(RepositorySystemSession session, DeployRequest request) {
144         if (Features.buildConsumer(session.getUserProperties()) && consumerPomPresent(request.getArtifacts())) {
145             request.setArtifacts(replacePom(request.getArtifacts()));
146         }
147         return request;
148     }
149 
150     private boolean consumerPomPresent(Collection<Artifact> artifacts) {
151         return artifacts.stream()
152                 .anyMatch(a -> "pom".equals(a.getExtension()) && CONSUMER_POM_CLASSIFIER.equals(a.getClassifier()));
153     }
154 
155     private Collection<Artifact> replacePom(Collection<Artifact> artifacts) {
156         List<Artifact> consumers = new ArrayList<>();
157         List<Artifact> mains = new ArrayList<>();
158         for (Artifact artifact : artifacts) {
159             if ("pom".equals(artifact.getExtension()) || artifact.getExtension().startsWith("pom.")) {
160                 if (CONSUMER_POM_CLASSIFIER.equals(artifact.getClassifier())) {
161                     consumers.add(artifact);
162                 } else if ("".equals(artifact.getClassifier())) {
163                     mains.add(artifact);
164                 }
165             }
166         }
167         if (!mains.isEmpty() && !consumers.isEmpty()) {
168             ArrayList<Artifact> result = new ArrayList<>(artifacts);
169             for (Artifact main : mains) {
170                 result.remove(main);
171                 result.add(new DefaultArtifact(
172                         main.getGroupId(),
173                         main.getArtifactId(),
174                         BUILD_POM_CLASSIFIER,
175                         main.getExtension(),
176                         main.getVersion(),
177                         main.getProperties(),
178                         main.getFile()));
179             }
180             for (Artifact consumer : consumers) {
181                 result.remove(consumer);
182                 result.add(new DefaultArtifact(
183                         consumer.getGroupId(),
184                         consumer.getArtifactId(),
185                         "",
186                         consumer.getExtension(),
187                         consumer.getVersion(),
188                         consumer.getProperties(),
189                         consumer.getFile()));
190             }
191             artifacts = result;
192         }
193         return artifacts;
194     }
195 
196     /**
197      * Consumer POM is transformed from original POM.
198      */
199     class ConsumerPomArtifact extends TransformedArtifact {
200 
201         private MavenProject project;
202         private RepositorySystemSession session;
203 
204         ConsumerPomArtifact(MavenProject mavenProject, Path target, RepositorySystemSession session) {
205             super(
206                     new ProjectArtifact(mavenProject),
207                     () -> mavenProject.getFile().toPath(),
208                     CONSUMER_POM_CLASSIFIER,
209                     "pom",
210                     target);
211             this.project = mavenProject;
212             this.session = session;
213         }
214 
215         @Override
216         public void transform(Path src, Path dest) {
217             Model model = project.getModel().getDelegate();
218             transform(src, dest, model);
219         }
220 
221         void transform(Path src, Path dest, Model model) {
222             Model consumer = null;
223             String version;
224 
225             String packaging = model.getPackaging();
226             if (POM_PACKAGING.equals(packaging)) {
227                 // This is a bit of a hack, but all models are cached, so not sure why we'd need to parse it again
228                 ModelCache cache = DefaultModelCache.newInstance(session);
229                 Object modelData = cache.get(new FileModelSource(src.toFile()), "raw");
230                 if (modelData != null) {
231                     try {
232                         Method getModel = modelData.getClass().getMethod("getModel");
233                         getModel.setAccessible(true);
234                         org.apache.maven.model.Model cachedModel =
235                                 (org.apache.maven.model.Model) getModel.invoke(modelData);
236                         consumer = cachedModel.getDelegate();
237                     } catch (Exception e) {
238                         throw new RuntimeException(e);
239                     }
240                 }
241 
242                 if (consumer == null) {
243                     TransformerContext context =
244                             (TransformerContext) session.getData().get(TransformerContext.KEY);
245                     Result<? extends org.apache.maven.model.Model> result = modelBuilder.buildRawModel(
246                             src.toFile(), ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL, false, context);
247                     if (result.hasErrors()) {
248                         throw new IllegalStateException(
249                                 "Unable to build POM " + src,
250                                 result.getProblems().iterator().next().getException());
251                     }
252                     consumer = result.get().getDelegate();
253                 }
254 
255                 // raw to consumer transform
256                 consumer = consumer.withRoot(false).withModules(null);
257                 if (consumer.getParent() != null) {
258                     consumer = consumer.withParent(consumer.getParent().withRelativePath(null));
259                 }
260 
261                 if (!consumer.isPreserveModelVersion()) {
262                     consumer = consumer.withPreserveModelVersion(false);
263                     version = new MavenModelVersion().getModelVersion(consumer);
264                     consumer = consumer.withModelVersion(version);
265                 } else {
266                     version = consumer.getModelVersion();
267                 }
268             } else {
269                 Model.Builder builder = prune(
270                         Model.newBuilder(model, true)
271                                 .preserveModelVersion(false)
272                                 .root(false)
273                                 .parent(null)
274                                 .build(null),
275                         model);
276                 boolean isBom = BOM_PACKAGING.equals(packaging);
277                 if (isBom) {
278                     builder.packaging(POM_PACKAGING);
279                 }
280                 builder.profiles(model.getProfiles().stream()
281                         .map(p -> prune(Profile.newBuilder(p, true), p).build())
282                         .collect(Collectors.toList()));
283                 consumer = builder.build();
284                 version = new MavenModelVersion().getModelVersion(consumer);
285                 consumer = consumer.withModelVersion(version);
286             }
287 
288             try {
289                 Files.createDirectories(dest.getParent());
290                 try (Writer w = Files.newBufferedWriter(dest)) {
291                     MavenStaxWriter writer = new MavenStaxWriter();
292                     writer.setNamespace(String.format(NAMESPACE_FORMAT, version));
293                     writer.setSchemaLocation(String.format(SCHEMA_LOCATION_FORMAT, version));
294                     writer.setAddLocationInformation(false);
295                     writer.write(w, consumer);
296                 }
297             } catch (XMLStreamException | IOException e) {
298                 throw new RuntimeException(e);
299             }
300         }
301     }
302 
303     private static <T extends ModelBase.Builder> T prune(T builder, ModelBase model) {
304         builder.properties(null).reporting(null);
305         if (model.getDistributionManagement() != null
306                 && model.getDistributionManagement().getRelocation() != null) {
307             // keep relocation only
308             builder.distributionManagement(DistributionManagement.newBuilder()
309                     .relocation(model.getDistributionManagement().getRelocation())
310                     .build());
311         }
312         // only keep repositories others than 'central'
313         builder.pluginRepositories(pruneRepositories(model.getPluginRepositories()));
314         builder.repositories(pruneRepositories(model.getRepositories()));
315         return builder;
316     }
317 
318     private static List<org.apache.maven.api.model.Repository> pruneRepositories(
319             List<org.apache.maven.api.model.Repository> repositories) {
320         return repositories.stream()
321                 .filter(r -> !Repository.CENTRAL_ID.equals(r.getId()))
322                 .collect(Collectors.toList());
323     }
324 }