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