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.plugins.gpg;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.nio.file.Files;
29  import java.util.ArrayList;
30  import java.util.List;
31  
32  import org.apache.maven.artifact.handler.ArtifactHandler;
33  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
34  import org.apache.maven.model.Model;
35  import org.apache.maven.model.Parent;
36  import org.apache.maven.model.building.DefaultModelBuildingRequest;
37  import org.apache.maven.model.building.ModelBuildingRequest;
38  import org.apache.maven.model.building.ModelProblem;
39  import org.apache.maven.model.building.ModelProblemCollector;
40  import org.apache.maven.model.building.ModelProblemCollectorRequest;
41  import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
42  import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
43  import org.apache.maven.model.validation.ModelValidator;
44  import org.apache.maven.plugin.MojoExecutionException;
45  import org.apache.maven.plugin.MojoFailureException;
46  import org.apache.maven.plugins.annotations.Mojo;
47  import org.apache.maven.plugins.annotations.Parameter;
48  import org.apache.maven.project.MavenProject;
49  import org.codehaus.plexus.util.FileUtils;
50  import org.codehaus.plexus.util.StringUtils;
51  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
52  import org.eclipse.aether.RepositorySystem;
53  import org.eclipse.aether.artifact.Artifact;
54  import org.eclipse.aether.artifact.DefaultArtifact;
55  import org.eclipse.aether.deployment.DeployRequest;
56  import org.eclipse.aether.deployment.DeploymentException;
57  import org.eclipse.aether.repository.RemoteRepository;
58  
59  /**
60   * Signs artifacts and deploys the artifacts and signatures in the remote repository.
61   *
62   * @author Daniel Kulp
63   * @since 1.0-beta-4
64   */
65  @Mojo(name = "sign-and-deploy-file", requiresProject = false, threadSafe = true)
66  public class SignAndDeployFileMojo extends AbstractGpgMojo {
67  
68      /**
69       * The directory where to store signature files.
70       */
71      @Parameter(property = "gpg.ascDirectory")
72      private File ascDirectory;
73  
74      /**
75       * GroupId of the artifact to be deployed. Retrieved from POM file if specified.
76       */
77      @Parameter(property = "groupId")
78      private String groupId;
79  
80      /**
81       * ArtifactId of the artifact to be deployed. Retrieved from POM file if specified.
82       */
83      @Parameter(property = "artifactId")
84      private String artifactId;
85  
86      /**
87       * Version of the artifact to be deployed. Retrieved from POM file if specified.
88       */
89      @Parameter(property = "version")
90      private String version;
91  
92      /**
93       * Type of the artifact to be deployed. Retrieved from POM file if specified.
94       * Defaults to file extension if not specified via command line or POM.
95       */
96      @Parameter(property = "packaging")
97      private String packaging;
98  
99      /**
100      * Add classifier to the artifact
101      */
102     @Parameter(property = "classifier")
103     private String classifier;
104 
105     /**
106      * Description passed to a generated POM file (in case of generatePom=true).
107      */
108     @Parameter(property = "generatePom.description")
109     private String description;
110 
111     /**
112      * File to be deployed.
113      */
114     @Parameter(property = "file", required = true)
115     private File file;
116 
117     /**
118      * Location of an existing POM file to be deployed alongside the main artifact, given by the ${file} parameter.
119      */
120     @Parameter(property = "pomFile")
121     private File pomFile;
122 
123     /**
124      * Upload a POM for this artifact. Will generate a default POM if none is supplied with the pomFile argument.
125      */
126     @Parameter(property = "generatePom", defaultValue = "true")
127     private boolean generatePom;
128 
129     /**
130      * URL where the artifact will be deployed. <br/>
131      * ie ( file:///C:/m2-repo or https://host.com/path/to/repo )
132      */
133     @Parameter(property = "url", required = true)
134     private String url;
135 
136     /**
137      * Server Id to map on the &lt;id&gt; under &lt;server&gt; section of <code>settings.xml</code>. In most cases, this
138      * parameter will be required for authentication.
139      */
140     @Parameter(property = "repositoryId", defaultValue = "remote-repository", required = true)
141     private String repositoryId;
142 
143     /**
144      * The bundled API docs for the artifact.
145      *
146      * @since 1.3
147      */
148     @Parameter(property = "javadoc")
149     private File javadoc;
150 
151     /**
152      * The bundled sources for the artifact.
153      *
154      * @since 1.3
155      */
156     @Parameter(property = "sources")
157     private File sources;
158 
159     /**
160      * Parameter used to control how many times a failed deployment will be retried before giving up and failing.
161      * If a value outside the range 1-10 is specified it will be pulled to the nearest value within the range 1-10.
162      *
163      * @since 1.3
164      */
165     @Parameter(property = "retryFailedDeploymentCount", defaultValue = "1")
166     private int retryFailedDeploymentCount;
167 
168     /**
169      * A comma separated list of types for each of the extra side artifacts to deploy. If there is a mis-match in
170      * the number of entries in {@link #files} or {@link #classifiers}, then an error will be raised.
171      */
172     @Parameter(property = "types")
173     private String types;
174 
175     /**
176      * A comma separated list of classifiers for each of the extra side artifacts to deploy. If there is a mis-match in
177      * the number of entries in {@link #files} or {@link #types}, then an error will be raised.
178      */
179     @Parameter(property = "classifiers")
180     private String classifiers;
181 
182     /**
183      * A comma separated list of files for each of the extra side artifacts to deploy. If there is a mis-match in
184      * the number of entries in {@link #types} or {@link #classifiers}, then an error will be raised.
185      */
186     @Parameter(property = "files")
187     private String files;
188 
189     private final RepositorySystem repositorySystem;
190 
191     /**
192      * The component used to validate the user-supplied artifact coordinates.
193      */
194     private final ModelValidator modelValidator;
195 
196     /**
197      * The default Maven project created when building the plugin
198      *
199      * @since 1.3
200      */
201     private final MavenProject project;
202 
203     /**
204      * @since 3.2.0
205      */
206     private final ArtifactHandlerManager artifactHandlerManager;
207 
208     @Inject
209     public SignAndDeployFileMojo(
210             RepositorySystem repositorySystem,
211             ModelValidator modelValidator,
212             MavenProject project,
213             ArtifactHandlerManager artifactHandlerManager) {
214         this.repositorySystem = repositorySystem;
215         this.modelValidator = modelValidator;
216         this.project = project;
217         this.artifactHandlerManager = artifactHandlerManager;
218     }
219 
220     private void initProperties() throws MojoExecutionException {
221         // Process the supplied POM (if there is one)
222         if (pomFile != null) {
223             generatePom = false;
224 
225             Model model = readModel(pomFile);
226 
227             processModel(model);
228         }
229 
230         if (packaging == null && file != null) {
231             packaging = FileUtils.getExtension(file.getName());
232         }
233     }
234 
235     @Override
236     protected void doExecute() throws MojoExecutionException, MojoFailureException {
237         if (settings.isOffline()) {
238             throw new MojoFailureException("Cannot deploy artifacts when Maven is in offline mode");
239         }
240 
241         initProperties();
242 
243         validateArtifactInformation();
244 
245         if (!file.exists()) {
246             throw new MojoFailureException(file.getPath() + " not found.");
247         }
248 
249         // create artifacts
250         List<Artifact> artifacts = new ArrayList<>();
251 
252         // main artifact
253         ArtifactHandler handler = artifactHandlerManager.getArtifactHandler(packaging);
254         Artifact main = new DefaultArtifact(
255                         groupId,
256                         artifactId,
257                         classifier == null || classifier.trim().isEmpty() ? handler.getClassifier() : classifier,
258                         handler.getExtension(),
259                         version)
260                 .setFile(file);
261 
262         File localRepoFile = new File(
263                 session.getRepositorySession().getLocalRepository().getBasedir(),
264                 session.getRepositorySession().getLocalRepositoryManager().getPathForLocalArtifact(main));
265         if (file.equals(localRepoFile)) {
266             throw new MojoFailureException("Cannot deploy artifact from the local repository: " + file);
267         }
268         artifacts.add(main);
269 
270         if (!"pom".equals(packaging)) {
271             if (pomFile == null && generatePom) {
272                 pomFile = generatePomFile();
273             }
274             if (pomFile != null) {
275                 artifacts.add(
276                         new DefaultArtifact(main.getGroupId(), main.getArtifactId(), null, "pom", main.getVersion())
277                                 .setFile(pomFile));
278             }
279         }
280 
281         if (sources != null) {
282             artifacts.add(
283                     new DefaultArtifact(main.getGroupId(), main.getArtifactId(), "sources", "jar", main.getVersion())
284                             .setFile(sources));
285         }
286 
287         if (javadoc != null) {
288             artifacts.add(
289                     new DefaultArtifact(main.getGroupId(), main.getArtifactId(), "javadoc", "jar", main.getVersion())
290                             .setFile(javadoc));
291         }
292 
293         if (files != null) {
294             if (types == null) {
295                 throw new MojoExecutionException("You must specify 'types' if you specify 'files'");
296             }
297             if (classifiers == null) {
298                 throw new MojoExecutionException("You must specify 'classifiers' if you specify 'files'");
299             }
300             String[] files = this.files.split(",", -1);
301             String[] types = this.types.split(",", -1);
302             String[] classifiers = this.classifiers.split(",", -1);
303             if (types.length != files.length) {
304                 throw new MojoExecutionException("You must specify the same number of entries in 'files' and "
305                         + "'types' (respectively " + files.length + " and " + types.length + " entries )");
306             }
307             if (classifiers.length != files.length) {
308                 throw new MojoExecutionException("You must specify the same number of entries in 'files' and "
309                         + "'classifiers' (respectively " + files.length + " and " + classifiers.length + " entries )");
310             }
311             for (int i = 0; i < files.length; i++) {
312                 File file = new File(files[i]);
313                 if (!file.isFile()) {
314                     // try relative to the project basedir just in case
315                     file = new File(project.getBasedir(), files[i]);
316                 }
317                 if (file.isFile()) {
318                     Artifact artifact;
319                     String ext =
320                             artifactHandlerManager.getArtifactHandler(types[i]).getExtension();
321                     if (StringUtils.isWhitespace(classifiers[i])) {
322                         artifact = new DefaultArtifact(
323                                 main.getGroupId(), main.getArtifactId(), null, ext, main.getVersion());
324                     } else {
325                         artifact = new DefaultArtifact(
326                                 main.getGroupId(), main.getArtifactId(), classifiers[i], ext, main.getVersion());
327                     }
328                     artifacts.add(artifact.setFile(file));
329                 } else {
330                     throw new MojoExecutionException("Specified side artifact " + file + " does not exist");
331                 }
332             }
333         } else {
334             if (types != null) {
335                 throw new MojoExecutionException("You must specify 'files' if you specify 'types'");
336             }
337             if (classifiers != null) {
338                 throw new MojoExecutionException("You must specify 'files' if you specify 'classifiers'");
339             }
340         }
341 
342         // sign all
343         AbstractGpgSigner signer = newSigner(null);
344         signer.setOutputDirectory(ascDirectory);
345         signer.setBaseDirectory(new File("").getAbsoluteFile());
346 
347         getLog().info("Signer '" + signer.signerName() + "' is signing " + artifacts.size() + " file"
348                 + ((artifacts.size() > 1) ? "s" : "") + " with key " + signer.getKeyInfo());
349 
350         ArrayList<Artifact> signatures = new ArrayList<>();
351         for (Artifact a : artifacts) {
352             signatures.add(new DefaultArtifact(
353                             a.getGroupId(),
354                             a.getArtifactId(),
355                             a.getClassifier(),
356                             a.getExtension() + AbstractGpgSigner.SIGNATURE_EXTENSION,
357                             a.getVersion())
358                     .setFile(signer.generateSignatureForArtifact(a.getFile())));
359         }
360         artifacts.addAll(signatures);
361 
362         // deploy all
363         RemoteRepository deploymentRepository = repositorySystem.newDeploymentRepository(
364                 session.getRepositorySession(), new RemoteRepository.Builder(repositoryId, "default", url).build());
365         try {
366             deploy(deploymentRepository, artifacts);
367         } catch (DeploymentException e) {
368             throw new MojoExecutionException(
369                     "Error deploying attached artifacts " + artifacts + ": " + e.getMessage(), e);
370         }
371     }
372 
373     /**
374      * Process the supplied pomFile to get groupId, artifactId, version, and packaging
375      *
376      * @param model The POM to extract missing artifact coordinates from, must not be <code>null</code>.
377      */
378     private void processModel(Model model) {
379         Parent parent = model.getParent();
380 
381         if (this.groupId == null) {
382             this.groupId = model.getGroupId();
383             if (this.groupId == null && parent != null) {
384                 this.groupId = parent.getGroupId();
385             }
386         }
387         if (this.artifactId == null) {
388             this.artifactId = model.getArtifactId();
389         }
390         if (this.version == null) {
391             this.version = model.getVersion();
392             if (this.version == null && parent != null) {
393                 this.version = parent.getVersion();
394             }
395         }
396         if (this.packaging == null) {
397             this.packaging = model.getPackaging();
398         }
399     }
400 
401     /**
402      * Extract the model from the specified POM file.
403      *
404      * @param pomFile The path of the POM file to parse, must not be <code>null</code>.
405      * @return The model from the POM file, never <code>null</code>.
406      * @throws MojoExecutionException If the file doesn't exist of cannot be read.
407      */
408     private Model readModel(File pomFile) throws MojoExecutionException {
409         try (InputStream inputStream = Files.newInputStream(pomFile.toPath())) {
410             return new MavenXpp3Reader().read(inputStream);
411         } catch (FileNotFoundException e) {
412             throw new MojoExecutionException("POM not found " + pomFile, e);
413         } catch (IOException e) {
414             throw new MojoExecutionException("Error reading POM " + pomFile, e);
415         } catch (XmlPullParserException e) {
416             throw new MojoExecutionException("Error parsing POM " + pomFile, e);
417         }
418     }
419 
420     /**
421      * Generates a minimal POM from the user-supplied artifact information.
422      *
423      * @return The path to the generated POM file, never <code>null</code>.
424      * @throws MojoExecutionException If the generation failed.
425      */
426     private File generatePomFile() throws MojoExecutionException {
427         Model model = generateModel();
428 
429         try {
430             File tempFile = Files.createTempFile("mvndeploy", ".pom").toFile();
431             tempFile.deleteOnExit();
432 
433             try (OutputStream outputStream = Files.newOutputStream(tempFile.toPath())) {
434                 new MavenXpp3Writer().write(outputStream, model);
435             }
436 
437             return tempFile;
438         } catch (IOException e) {
439             throw new MojoExecutionException("Error writing temporary pom file: " + e.getMessage(), e);
440         }
441     }
442 
443     /**
444      * Validates the user-supplied artifact information.
445      *
446      * @throws MojoFailureException If any artifact coordinate is invalid.
447      */
448     private void validateArtifactInformation() throws MojoFailureException {
449         Model model = generateModel();
450 
451         ModelBuildingRequest request =
452                 new DefaultModelBuildingRequest().setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_STRICT);
453 
454         List<String> result = new ArrayList<>();
455 
456         SimpleModelProblemCollector problemCollector = new SimpleModelProblemCollector(result);
457 
458         modelValidator.validateEffectiveModel(model, request, problemCollector);
459 
460         if (!result.isEmpty()) {
461             StringBuilder msg = new StringBuilder("The artifact information is incomplete or not valid:\n");
462             for (String e : result) {
463                 msg.append(" - " + e + '\n');
464             }
465             throw new MojoFailureException(msg.toString());
466         }
467     }
468 
469     /**
470      * Generates a minimal model from the user-supplied artifact information.
471      *
472      * @return The generated model, never <code>null</code>.
473      */
474     private Model generateModel() {
475         Model model = new Model();
476 
477         model.setModelVersion("4.0.0");
478 
479         model.setGroupId(groupId);
480         model.setArtifactId(artifactId);
481         model.setVersion(version);
482         model.setPackaging(packaging);
483 
484         model.setDescription(description);
485 
486         return model;
487     }
488 
489     /**
490      * Deploy an artifact from a particular file.
491      *
492      * @param deploymentRepository the repository to deploy to
493      * @param artifacts the artifacts definition
494      * @throws DeploymentException if an error occurred deploying the artifact
495      */
496     protected void deploy(RemoteRepository deploymentRepository, List<Artifact> artifacts) throws DeploymentException {
497         int retryFailedDeploymentCount = Math.max(1, Math.min(10, this.retryFailedDeploymentCount));
498         DeploymentException exception = null;
499         for (int count = 0; count < retryFailedDeploymentCount; count++) {
500             try {
501                 if (count > 0) {
502                     // CHECKSTYLE_OFF: LineLength
503                     getLog().info("Retrying deployment attempt " + (count + 1) + " of " + retryFailedDeploymentCount);
504                     // CHECKSTYLE_ON: LineLength
505                 }
506                 DeployRequest deployRequest = new DeployRequest();
507                 deployRequest.setRepository(deploymentRepository);
508                 deployRequest.setArtifacts(artifacts);
509 
510                 repositorySystem.deploy(session.getRepositorySession(), deployRequest);
511                 exception = null;
512                 break;
513             } catch (DeploymentException e) {
514                 if (count + 1 < retryFailedDeploymentCount) {
515                     getLog().warn("Encountered issue during deployment: " + e.getLocalizedMessage());
516                     getLog().debug(e);
517                 }
518                 if (exception == null) {
519                     exception = e;
520                 }
521             }
522         }
523         if (exception != null) {
524             throw exception;
525         }
526     }
527 
528     private static class SimpleModelProblemCollector implements ModelProblemCollector {
529 
530         private final List<String> result;
531 
532         SimpleModelProblemCollector(List<String> result) {
533             this.result = result;
534         }
535 
536         public void add(ModelProblemCollectorRequest req) {
537             if (!ModelProblem.Severity.WARNING.equals(req.getSeverity())) {
538                 result.add(req.getMessage());
539             }
540         }
541     }
542 }