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