View Javadoc
1   package org.apache.maven.plugins.gpg;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.io.Reader;
26  import java.io.Writer;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.List;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.factory.ArtifactFactory;
33  import org.apache.maven.artifact.metadata.ArtifactMetadata;
34  import org.apache.maven.artifact.repository.ArtifactRepository;
35  import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
36  import org.apache.maven.artifact.repository.MavenArtifactRepository;
37  import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
38  import org.apache.maven.execution.MavenSession;
39  import org.apache.maven.model.InputLocation;
40  import org.apache.maven.model.Model;
41  import org.apache.maven.model.Parent;
42  import org.apache.maven.model.building.DefaultModelBuildingRequest;
43  import org.apache.maven.model.building.ModelBuildingRequest;
44  import org.apache.maven.model.building.ModelProblem;
45  import org.apache.maven.model.building.ModelProblem.Severity;
46  import org.apache.maven.model.building.ModelProblemCollector;
47  import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
48  import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
49  import org.apache.maven.model.validation.ModelValidator;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.plugin.MojoFailureException;
52  import org.apache.maven.plugins.annotations.Component;
53  import org.apache.maven.plugins.annotations.Mojo;
54  import org.apache.maven.plugins.annotations.Parameter;
55  import org.apache.maven.project.MavenProject;
56  import org.apache.maven.project.MavenProjectHelper;
57  import org.apache.maven.project.ProjectBuildingRequest;
58  import org.apache.maven.project.artifact.ProjectArtifactMetadata;
59  import org.apache.maven.shared.transfer.artifact.deploy.ArtifactDeployer;
60  import org.apache.maven.shared.transfer.artifact.deploy.ArtifactDeployerException;
61  import org.codehaus.plexus.util.FileUtils;
62  import org.codehaus.plexus.util.ReaderFactory;
63  import org.codehaus.plexus.util.StringUtils;
64  import org.codehaus.plexus.util.WriterFactory;
65  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
66  
67  /**
68   * Signs artifacts and installs the artifact in the remote repository.
69   *
70   * @author Daniel Kulp
71   * @since 1.0-beta-4
72   */
73  @Mojo( name = "sign-and-deploy-file", requiresProject = false, threadSafe = true )
74  public class SignAndDeployFileMojo
75      extends AbstractGpgMojo
76  {
77  
78      /**
79       * The directory where to store signature files.
80       */
81      @Parameter( property = "gpg.ascDirectory" )
82      private File ascDirectory;
83  
84      /**
85       * Flag whether Maven is currently in online/offline mode.
86       */
87      @Parameter( defaultValue = "${settings.offline}", readonly = true )
88      private boolean offline;
89  
90      /**
91       * GroupId of the artifact to be deployed. Retrieved from POM file if specified.
92       */
93      @Parameter( property = "groupId" )
94      private String groupId;
95  
96      /**
97       * ArtifactId of the artifact to be deployed. Retrieved from POM file if specified.
98       */
99      @Parameter( property = "artifactId" )
100     private String artifactId;
101 
102     /**
103      * Version of the artifact to be deployed. Retrieved from POM file if specified.
104      */
105     @Parameter( property = "version" )
106     private String version;
107 
108     /**
109      * Type of the artifact to be deployed. Retrieved from POM file if specified.
110      * Defaults to file extension if not specified via command line or POM.
111      */
112     @Parameter( property = "packaging" )
113     private String packaging;
114 
115     /**
116      * Add classifier to the artifact
117      */
118     @Parameter( property = "classifier" )
119     private String classifier;
120 
121     /**
122      * Description passed to a generated POM file (in case of generatePom=true).
123      */
124     @Parameter( property = "generatePom.description" )
125     private String description;
126 
127     /**
128      * File to be deployed.
129      */
130     @Parameter( property = "file", required = true )
131     private File file;
132 
133     /**
134      * Location of an existing POM file to be deployed alongside the main artifact, given by the ${file} parameter.
135      */
136     @Parameter( property = "pomFile" )
137     private File pomFile;
138 
139     /**
140      * Upload a POM for this artifact. Will generate a default POM if none is supplied with the pomFile argument.
141      */
142     @Parameter( property = "generatePom", defaultValue = "true" )
143     private boolean generatePom;
144 
145     /**
146      * URL where the artifact will be deployed. <br/>
147      * ie ( file:///C:/m2-repo or scp://host.com/path/to/repo )
148      */
149     @Parameter( property = "url", required = true )
150     private String url;
151 
152     /**
153      * Server Id to map on the &lt;id&gt; under &lt;server&gt; section of <code>settings.xml</code>. In most cases, this
154      * parameter will be required for authentication.
155      */
156     @Parameter( property = "repositoryId", defaultValue = "remote-repository", required = true )
157     private String repositoryId;
158 
159     /**
160      * The type of remote repository layout to deploy to. Try <i>legacy</i> for a Maven 1.x-style repository layout.
161      */
162     @Parameter( property = "repositoryLayout", defaultValue = "default" )
163     private String repositoryLayout;
164 
165     /**
166      */
167     @Component
168     private ArtifactDeployer deployer;
169 
170     /**
171      */
172     @Parameter( defaultValue = "${localRepository}", required = true, readonly = true )
173     private ArtifactRepository localRepository;
174 
175     /**
176      * Component used to create an artifact
177      */
178     @Component
179     private ArtifactFactory artifactFactory;
180 
181     /**
182      * The component used to validate the user-supplied artifact coordinates.
183      */
184     @Component
185     private ModelValidator modelValidator;
186 
187     /**
188      * The default Maven project created when building the plugin
189      *
190      * @since 1.3
191      */
192     @Parameter( defaultValue = "${project}", readonly = true, required = true )
193     private MavenProject project;
194 
195     /**
196      * @since 3.0.0
197      */
198     @Parameter( defaultValue = "${session}", readonly = true, required = true )
199     private MavenSession session;
200 
201     /**
202      * Used for attaching the source and javadoc jars to the project.
203      *
204      * @since 1.3
205      */
206     @Component
207     private MavenProjectHelper projectHelper;
208 
209     /**
210      * The bundled API docs for the artifact.
211      *
212      * @since 1.3
213      */
214     @Parameter( property = "javadoc" )
215     private File javadoc;
216 
217     /**
218      * The bundled sources for the artifact.
219      *
220      * @since 1.3
221      */
222     @Parameter( property = "sources" )
223     private File sources;
224 
225     /**
226      * Parameter used to control how many times a failed deployment will be retried before giving up and failing.
227      * If a value outside the range 1-10 is specified it will be pulled to the nearest value within the range 1-10.
228      *
229      * @since 1.3
230      */
231     @Parameter( property = "retryFailedDeploymentCount", defaultValue = "1" )
232     private int retryFailedDeploymentCount;
233 
234     /**
235      * Parameter used to update the metadata to make the artifact as release.
236      *
237      * @since 1.3
238      */
239     @Parameter( property = "updateReleaseInfo", defaultValue = "false" )
240     protected boolean updateReleaseInfo;
241 
242     /**
243      * A comma separated list of types for each of the extra side artifacts to deploy. If there is a mis-match in
244      * the number of entries in {@link #files} or {@link #classifiers}, then an error will be raised.
245      */
246     @Parameter( property = "types" )
247     private String types;
248 
249     /**
250      * A comma separated list of classifiers for each of the extra side artifacts to deploy. If there is a mis-match in
251      * the number of entries in {@link #files} or {@link #types}, then an error will be raised.
252      */
253     @Parameter( property = "classifiers" )
254     private String classifiers;
255 
256     /**
257      * A comma separated list of files for each of the extra side artifacts to deploy. If there is a mis-match in
258      * the number of entries in {@link #types} or {@link #classifiers}, then an error will be raised.
259      */
260     @Parameter( property = "files" )
261     private String files;
262 
263     private void initProperties()
264         throws MojoExecutionException
265     {
266         // Process the supplied POM (if there is one)
267         if ( pomFile != null )
268         {
269             generatePom = false;
270 
271             Model model = readModel( pomFile );
272 
273             processModel( model );
274         }
275 
276         if ( packaging == null && file != null )
277         {
278             packaging = FileUtils.getExtension( file.getName() );
279         }
280     }
281 
282     @Override
283     public void execute()
284         throws MojoExecutionException, MojoFailureException
285     {
286         AbstractGpgSigner signer = newSigner( null );
287         signer.setOutputDirectory( ascDirectory );
288         signer.setBaseDirectory( new File( "" ).getAbsoluteFile() );
289 
290         if ( offline )
291         {
292             throw new MojoFailureException( "Cannot deploy artifacts when Maven is in offline mode" );
293         }
294 
295         initProperties();
296 
297         validateArtifactInformation();
298 
299         if ( !file.exists() )
300         {
301             throw new MojoFailureException( file.getPath() + " not found." );
302         }
303 
304         
305         ArtifactRepository deploymentRepository = createDeploymentArtifactRepository( repositoryId, url );
306 
307         if ( StringUtils.isEmpty( deploymentRepository.getProtocol() ) )
308         {
309             throw new MojoFailureException( "No transfer protocol found." );
310         }
311 
312         Artifact artifact =
313             artifactFactory.createArtifactWithClassifier( groupId, artifactId, version, packaging, classifier );
314 
315         if ( file.equals( getLocalRepoFile( artifact ) ) )
316         {
317             throw new MojoFailureException( "Cannot deploy artifact from the local repository: " + file );
318         }
319         artifact.setFile( file );
320 
321         File fileSig = signer.generateSignatureForArtifact( file );
322         ArtifactMetadata metadata = new AscArtifactMetadata( artifact, fileSig, false );
323         artifact.addMetadata( metadata );
324 
325         if ( !"pom".equals( packaging ) )
326         {
327             if ( pomFile == null && generatePom )
328             {
329                 pomFile = generatePomFile();
330             }
331             if ( pomFile != null )
332             {
333                 metadata = new ProjectArtifactMetadata( artifact, pomFile );
334                 artifact.addMetadata( metadata );
335 
336                 fileSig = signer.generateSignatureForArtifact( pomFile );
337                 metadata = new AscArtifactMetadata( artifact, fileSig, true );
338                 artifact.addMetadata( metadata );
339             }
340         }
341 
342         if ( updateReleaseInfo )
343         {
344             artifact.setRelease( true );
345         }
346 
347         project.setArtifact( artifact );
348 
349         try
350         {
351             deploy( artifact, deploymentRepository );
352         }
353         catch ( ArtifactDeployerException e )
354         {
355             throw new MojoExecutionException( e.getMessage(), e );
356         }
357 
358         if ( sources != null )
359         {
360             projectHelper.attachArtifact( project, "jar", "sources", sources );
361         }
362 
363         if ( javadoc != null )
364         {
365             projectHelper.attachArtifact( project, "jar", "javadoc", javadoc );
366         }
367 
368         if ( files != null )
369         {
370             if ( types == null )
371             {
372                 throw new MojoExecutionException( "You must specify 'types' if you specify 'files'" );
373             }
374             if ( classifiers == null )
375             {
376                 throw new MojoExecutionException( "You must specify 'classifiers' if you specify 'files'" );
377             }
378             int filesLength = StringUtils.countMatches( files, "," );
379             int typesLength = StringUtils.countMatches( types, "," );
380             int classifiersLength = StringUtils.countMatches( classifiers, "," );
381             if ( typesLength != filesLength )
382             {
383                 throw new MojoExecutionException( "You must specify the same number of entries in 'files' and "
384                     + "'types' (respectively " + filesLength + " and " + typesLength + " entries )" );
385             }
386             if ( classifiersLength != filesLength )
387             {
388                 throw new MojoExecutionException( "You must specify the same number of entries in 'files' and "
389                     + "'classifiers' (respectively " + filesLength + " and " + classifiersLength + " entries )" );
390             }
391             int fi = 0;
392             int ti = 0;
393             int ci = 0;
394             for ( int i = 0; i <= filesLength; i++ )
395             {
396                 int nfi = files.indexOf( ',', fi );
397                 if ( nfi == -1 )
398                 {
399                     nfi = files.length();
400                 }
401                 int nti = types.indexOf( ',', ti );
402                 if ( nti == -1 )
403                 {
404                     nti = types.length();
405                 }
406                 int nci = classifiers.indexOf( ',', ci );
407                 if ( nci == -1 )
408                 {
409                     nci = classifiers.length();
410                 }
411                 File file = new File( files.substring( fi, nfi ) );
412                 if ( !file.isFile() )
413                 {
414                     // try relative to the project basedir just in case
415                     file = new File( project.getBasedir(), files.substring( fi, nfi ) );
416                 }
417                 if ( file.isFile() )
418                 {
419                     if ( StringUtils.isWhitespace( classifiers.substring( ci, nci ) ) )
420                     {
421                         projectHelper.attachArtifact( project, types.substring( ti, nti ).trim(), file );
422                     }
423                     else
424                     {
425                         projectHelper.attachArtifact( project, types.substring( ti, nti ).trim(),
426                                                       classifiers.substring( ci, nci ).trim(), file );
427                     }
428                 }
429                 else
430                 {
431                     throw new MojoExecutionException( "Specified side artifact " + file + " does not exist" );
432                 }
433                 fi = nfi + 1;
434                 ti = nti + 1;
435                 ci = nci + 1;
436             }
437         }
438         else
439         {
440             if ( types != null )
441             {
442                 throw new MojoExecutionException( "You must specify 'files' if you specify 'types'" );
443             }
444             if ( classifiers != null )
445             {
446                 throw new MojoExecutionException( "You must specify 'files' if you specify 'classifiers'" );
447             }
448         }
449 
450         for ( Artifact attached : project.getAttachedArtifacts() )
451         {
452             fileSig = signer.generateSignatureForArtifact( attached.getFile() );
453             attached = new AttachedSignedArtifact( attached, new AscArtifactMetadata( attached, fileSig, false ) );
454             try
455             {
456                 deploy(  attached, deploymentRepository );
457             }
458             catch ( ArtifactDeployerException e )
459             {
460                 throw new MojoExecutionException( "Error deploying attached artifact " + attached.getFile() + ": "
461                     + e.getMessage(), e );
462             }
463         }
464 
465     }
466 
467     /**
468      * Gets the path of the specified artifact within the local repository. Note that the returned path need not exist
469      * (yet).
470      *
471      * @param artifact The artifact whose local repo path should be determined, must not be <code>null</code>.
472      * @return The absolute path to the artifact when installed, never <code>null</code>.
473      */
474     private File getLocalRepoFile( Artifact artifact )
475     {
476         String path = localRepository.pathOf( artifact );
477         return new File( localRepository.getBasedir(), path );
478     }
479 
480     /**
481      * Process the supplied pomFile to get groupId, artifactId, version, and packaging
482      *
483      * @param model The POM to extract missing artifact coordinates from, must not be <code>null</code>.
484      */
485     private void processModel( Model model )
486     {
487         Parent parent = model.getParent();
488 
489         if ( this.groupId == null )
490         {
491             this.groupId = model.getGroupId();
492             if ( this.groupId == null && parent != null )
493             {
494                 this.groupId = parent.getGroupId();
495             }
496         }
497         if ( this.artifactId == null )
498         {
499             this.artifactId = model.getArtifactId();
500         }
501         if ( this.version == null )
502         {
503             this.version = model.getVersion();
504             if ( this.version == null && parent != null )
505             {
506                 this.version = parent.getVersion();
507             }
508         }
509         if ( this.packaging == null )
510         {
511             this.packaging = model.getPackaging();
512         }
513     }
514 
515     /**
516      * Extract the model from the specified POM file.
517      *
518      * @param pomFile The path of the POM file to parse, must not be <code>null</code>.
519      * @return The model from the POM file, never <code>null</code>.
520      * @throws MojoExecutionException If the file doesn't exist of cannot be read.
521      */
522     private Model readModel( File pomFile )
523         throws MojoExecutionException
524     {
525         try ( Reader reader = ReaderFactory.newXmlReader( pomFile ) )
526         {
527             final Model model = new MavenXpp3Reader().read( reader );
528             return model;
529         }
530         catch ( FileNotFoundException e )
531         {
532             throw new MojoExecutionException( "POM not found " + pomFile, e );
533         }
534         catch ( IOException e )
535         {
536             throw new MojoExecutionException( "Error reading POM " + pomFile, e );
537         }
538         catch ( XmlPullParserException e )
539         {
540             throw new MojoExecutionException( "Error parsing POM " + pomFile, e );
541         }
542     }
543 
544     /**
545      * Generates a minimal POM from the user-supplied artifact information.
546      *
547      * @return The path to the generated POM file, never <code>null</code>.
548      * @throws MojoExecutionException If the generation failed.
549      */
550     private File generatePomFile()
551         throws MojoExecutionException
552     {
553         Model model = generateModel();
554         
555         try 
556         {
557             File tempFile = File.createTempFile( "mvndeploy", ".pom" );
558             tempFile.deleteOnExit();
559 
560             try ( Writer fw = WriterFactory.newXmlWriter( tempFile ) )
561             {
562                 new MavenXpp3Writer().write( fw, model );
563             }
564 
565             return tempFile;
566         }
567         catch ( IOException e )
568         {
569             throw new MojoExecutionException( "Error writing temporary pom file: " + e.getMessage(), e );
570         }
571     }
572 
573     /**
574      * Validates the user-supplied artifact information.
575      *
576      * @throws MojoFailureException If any artifact coordinate is invalid.
577      */
578     private void validateArtifactInformation()
579         throws MojoFailureException
580     {
581         Model model = generateModel();
582 
583         ModelBuildingRequest request = new DefaultModelBuildingRequest()
584                         .setValidationLevel( ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 );
585                     
586         List<String> result = new ArrayList<>();
587 
588         SimpleModelProblemCollector problemCollector = new SimpleModelProblemCollector( result );
589 
590         modelValidator.validateEffectiveModel( model, request, problemCollector );
591 
592         if ( !result.isEmpty() )
593         {
594             StringBuilder msg = new StringBuilder( "The artifact information is incomplete or not valid:\n" );
595             for ( String e : result )
596             {
597                 msg.append( " - " + e + '\n' );
598             }
599             throw new MojoFailureException( msg.toString() );
600         }
601     }
602 
603     /**
604      * Generates a minimal model from the user-supplied artifact information.
605      *
606      * @return The generated model, never <code>null</code>.
607      */
608     private Model generateModel()
609     {
610         Model model = new Model();
611 
612         model.setModelVersion( "4.0.0" );
613 
614         model.setGroupId( groupId );
615         model.setArtifactId( artifactId );
616         model.setVersion( version );
617         model.setPackaging( packaging );
618 
619         model.setDescription( description );
620 
621         return model;
622     }
623 
624     /**
625      * Deploy an artifact from a particular file.
626      *
627      * @param artifact the artifact definition
628      * @param deploymentRepository the repository to deploy to
629      * @throws ArtifactDeployerException if an error occurred deploying the artifact
630      */
631     protected void deploy( Artifact artifact,
632                            ArtifactRepository deploymentRepository )
633         throws ArtifactDeployerException
634     {
635         final ProjectBuildingRequest buildingRequest = session.getProjectBuildingRequest();
636         
637         int retryFailedDeploymentCount = Math.max( 1, Math.min( 10, this.retryFailedDeploymentCount ) );
638         ArtifactDeployerException exception = null;
639         for ( int count = 0; count < retryFailedDeploymentCount; count++ )
640         {
641             try
642             {
643                 if ( count > 0 )
644                 {
645                     // CHECKSTYLE_OFF: LineLength
646                     getLog().info( "Retrying deployment attempt " + ( count + 1 ) + " of " + retryFailedDeploymentCount );
647                     // CHECKSTYLE_ON: LineLength
648                 }
649                 deployer.deploy( buildingRequest, deploymentRepository, Collections.singletonList( artifact ) );
650 
651                 for ( Object o : artifact.getMetadataList() )
652                 {
653                     ArtifactMetadata metadata = (ArtifactMetadata) o;
654                     getLog().info( "Metadata[" + metadata.getKey() + "].filename = " + metadata.getRemoteFilename() );
655                 }
656                 exception = null;
657                 break;
658             }
659             catch ( ArtifactDeployerException e )
660             {
661                 if ( count + 1 < retryFailedDeploymentCount )
662                 {
663                     getLog().warn( "Encountered issue during deployment: " + e.getLocalizedMessage() );
664                     getLog().debug( e );
665                 }
666                 if ( exception == null )
667                 {
668                     exception = e;
669                 }
670             }
671         }
672         if ( exception != null )
673         {
674             throw exception;
675         }
676     }
677     
678     protected ArtifactRepository createDeploymentArtifactRepository( String id, String url )
679     {
680         return new MavenArtifactRepository( id, url, new DefaultRepositoryLayout(), new ArtifactRepositoryPolicy(),
681                                             new ArtifactRepositoryPolicy() );
682     }
683 
684     private static class SimpleModelProblemCollector
685         implements ModelProblemCollector
686     {
687 
688         private final List<String> result;
689 
690         SimpleModelProblemCollector( List<String> result )
691         {
692             this.result = result;
693         }
694 
695         public void add( Severity severity, String message, InputLocation location, Exception cause )
696         {
697             if ( !ModelProblem.Severity.WARNING.equals( severity ) )
698             {
699                 result.add( message );
700             }
701         }
702 
703     }
704 }