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