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