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