View Javadoc

1   package org.apache.maven.artifact.deployer;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  import org.apache.commons.lang.StringUtils;
21  import org.apache.commons.lang.SystemUtils;
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.apache.maven.MavenConstants;
25  import org.apache.maven.MavenException;
26  import org.apache.maven.artifact.PomRewriter;
27  import org.apache.maven.project.Project;
28  import org.apache.maven.repository.ArtifactTypeHandler;
29  import org.apache.maven.repository.DefaultArtifactTypeHandler;
30  import org.apache.maven.wagon.ConnectionException;
31  import org.apache.maven.wagon.ResourceDoesNotExistException;
32  import org.apache.maven.wagon.TransferFailedException;
33  import org.apache.maven.wagon.Wagon;
34  import org.apache.maven.wagon.authentication.AuthenticationException;
35  import org.apache.maven.wagon.authentication.AuthenticationInfo;
36  import org.apache.maven.wagon.authorization.AuthorizationException;
37  import org.apache.maven.wagon.events.TransferListener;
38  import org.apache.maven.wagon.observers.ChecksumObserver;
39  import org.apache.maven.wagon.providers.file.FileWagon;
40  import org.apache.maven.wagon.providers.ftp.FtpWagon;
41  import org.apache.maven.wagon.providers.http.HttpWagon;
42  import org.apache.maven.wagon.providers.ssh.external.ScpExternalWagon;
43  import org.apache.maven.wagon.providers.ssh.jsch.ScpWagon;
44  import org.apache.maven.wagon.providers.ssh.jsch.SftpWagon;
45  import org.apache.maven.wagon.repository.Repository;
46  import org.codehaus.plexus.util.FileUtils;
47  import org.codehaus.plexus.util.cli.CommandLineException;
48  import org.codehaus.plexus.util.cli.CommandLineUtils;
49  import org.codehaus.plexus.util.cli.Commandline;
50  import org.codehaus.plexus.util.cli.DefaultConsumer;
51  
52  import java.io.BufferedReader;
53  import java.io.ByteArrayInputStream;
54  import java.io.File;
55  import java.io.IOException;
56  import java.io.InputStream;
57  import java.io.InputStreamReader;
58  import java.net.MalformedURLException;
59  import java.security.NoSuchAlgorithmException;
60  import java.text.DateFormat;
61  import java.text.SimpleDateFormat;
62  import java.util.ArrayList;
63  import java.util.Collections;
64  import java.util.Date;
65  import java.util.HashMap;
66  import java.util.Iterator;
67  import java.util.List;
68  import java.util.Map;
69  import java.util.TimeZone;
70  
71  /**
72   * Default implementation of Artifact Deployer interface.
73   * 
74   * @author <a href="mailto:michal.maczka@dimatics.com">Michal Maczka</a>
75   * @version $Id: DefaultArtifactDeployer.java 538499 2007-05-16 09:30:20Z ltheussl $
76   */
77  public class DefaultArtifactDeployer implements ArtifactDeployer
78  {
79  
80      protected static final String POM_TYPE = "pom";
81  
82      public static final String SIGNATURE_EXTENSION = ".asc";
83  
84      private static final ArtifactTypeHandler POM_ARTIFACT_TYPE_HANDLER = new DefaultArtifactTypeHandler();
85  
86      /**
87       * Date/time stamp which is appended to snapshot filenames
88       */
89      protected final static String SNAPSHOT_FORMAT = "yyyyMMdd.HHmmss";
90  
91      protected String snapshotSignature;
92  
93      private static final Log LOG = LogFactory.getLog( DefaultArtifactDeployer.class );
94  
95      /**
96       * @see ArtifactDeployer#deploy(String, String, Project, ArtifactTypeHandler)
97       */
98      public void deploy( final String artifact, final String type, final Project project,
99                          final ArtifactTypeHandler handler ) throws MavenException
100     {
101         this.handleDeploy( type, project, project.getArtifactId(), artifact, handler, project.getCurrentVersion() );
102     }
103 
104     /**
105      * @see DefaultArtifactDeployer#deploySnapshot(String, String, Project, ArtifactTypeHandler)
106      */
107     public void deploySnapshot( final String artifact, final String type, final Project project,
108                                 final ArtifactTypeHandler handler ) throws MavenException
109     {
110         this.handleDeploy( type, project, project.getArtifactId(), artifact, handler, MavenConstants.SNAPSHOT_SIGNIFIER );
111     }
112 
113     protected void handleDeploy( final String type, final Project project, final String artifactId,
114                                  final String artifact, final ArtifactTypeHandler handler, final String version )
115         throws MavenException
116     {
117         Boolean gpgSkip = Boolean.valueOf( (String) project.getContext().getVariable( "maven.artifact.gpg.skip" ) );
118         String gpgPassphrase = null;
119         if ( !gpgSkip.booleanValue() )
120         {
121             try
122             {
123                 gpgPassphrase = getPassphrase( project );
124             }
125             catch ( IOException e )
126             {
127                 throw new MavenException( "Error while retreiving the passphrase for gpg", e );
128             }
129         }
130         String gpgKeyname = (String) project.getContext().getVariable( "maven.artifact.gpg.keyname" );
131         Boolean gpgUseagent =
132             Boolean.valueOf( (String) project.getContext().getVariable( "maven.artifact.gpg.useagent" ) );
133 
134         File file;
135         if ( DefaultArtifactDeployer.POM_TYPE.equals( type ) )
136         {
137             file = PomRewriter.getRewrittenPom( project );
138         }
139         else
140         {
141             file = this.getFileForArtifact( artifact );
142         }
143 
144         // do not deploy POM twice
145         if ( !DefaultArtifactDeployer.POM_TYPE.equals( type ) )
146         {
147             File pomFile = PomRewriter.getRewrittenPom( project );
148             if ( !gpgSkip.booleanValue() )
149             {
150                 generateSignatureForArtifact( pomFile, gpgPassphrase, gpgUseagent.booleanValue(), gpgKeyname );
151             }
152             this.doDeploy( pomFile, project, artifactId, DefaultArtifactDeployer.POM_ARTIFACT_TYPE_HANDLER, version,
153                            DefaultArtifactDeployer.POM_TYPE, gpgSkip.booleanValue(), gpgPassphrase,
154                            gpgUseagent.booleanValue(), gpgKeyname );
155         }
156         if ( !gpgSkip.booleanValue() )
157         {
158             generateSignatureForArtifact( file, gpgPassphrase, gpgUseagent.booleanValue(), gpgKeyname );
159         }
160         this.doDeploy( file, project, artifactId, handler, version, type, gpgSkip.booleanValue(), gpgPassphrase,
161                        gpgUseagent.booleanValue(), gpgKeyname );
162 
163         this.snapshotSignature = null;
164 
165     }
166 
167     /**
168      * @see ArtifactDeployer#install(String, String, Project, ArtifactTypeHandler)
169      */
170     public void install( final String artifact, final String type, final Project project,
171                          final ArtifactTypeHandler handler ) throws MavenException
172     {
173         this.handleInstall( type, project, artifact, handler, project.getCurrentVersion() );
174     }
175 
176     /**
177      * @see ArtifactDeployer#installSnapshot(String, String, Project, ArtifactTypeHandler)
178      */
179     public void installSnapshot( final String artifact, final String type, final Project project,
180                                  final ArtifactTypeHandler handler ) throws MavenException
181     {
182         this.handleInstall( type, project, artifact, handler, MavenConstants.SNAPSHOT_SIGNIFIER );
183     }
184 
185     private void handleInstall( final String type, final Project project, final String artifact,
186                                 final ArtifactTypeHandler handler, final String version ) throws MavenException
187     {
188         File file;
189         if ( DefaultArtifactDeployer.POM_TYPE.equals( type ) )
190         {
191             file = PomRewriter.getRewrittenPom( project );
192         }
193         else
194         {
195             file = this.getFileForArtifact( artifact );
196         }
197 
198         this.doInstall( file, type, project, version, handler );
199 
200         // do not install twice
201         if ( !DefaultArtifactDeployer.POM_TYPE.equals( type ) )
202         {
203             this.doInstall( PomRewriter.getRewrittenPom( project ), DefaultArtifactDeployer.POM_TYPE, project, version,
204                             DefaultArtifactDeployer.POM_ARTIFACT_TYPE_HANDLER );
205         }
206     }
207 
208     /**
209      * Install given file in local repository
210      * 
211      * @param file
212      *            the artifact file to install
213      * @param type
214      *            The type of the artiafct
215      * @param project
216      * @param version
217      *            String denominating the version of the artifact
218      * @throws MavenException
219      */
220     private void doInstall( final File file, final String type, final Project project, final String version,
221                             final ArtifactTypeHandler handler ) throws MavenException
222     {
223         try
224         {
225             final Repository repository = new Repository( "local", "file:" + project.getContext().getMavenRepoLocal() );
226             final String repositoryPath = handler.constructRepositoryFullPath( type, project, version );
227             this.deployFile( repository, file, repositoryPath, project, true, null, false, null );
228         }
229         catch ( final Exception e )
230         {
231             final String msg = "Cannot install file: '" + file + "'. Reason: " + e.getMessage();
232             throw new MavenException( msg, e );
233         }
234     }
235 
236     protected String findSshIdentity()
237     {
238         String key = this.findSshIdentity( System.getProperty( "user.home" ) );
239         if ( key != null )
240         {
241             return key;
242         }
243         if ( System.getProperty( "user.home" ).equals( System.getProperty( "user.home.env" ) ) == false )
244         {
245             key = this.findSshIdentity( System.getProperty( "user.home.env" ) );
246             if ( key != null )
247             {
248                 return key;
249             }
250         }
251         DefaultArtifactDeployer.LOG.warn( "Unable to locate identity id_rsa, id_dsa or identity - set maven.repo.default.privatekey" );
252         return null;
253     }
254 
255     private String findSshIdentity( final String home )
256     {
257         if ( home == null )
258         {
259             return null;
260         }
261         final File sshHome = new File( home, ".ssh" );
262         DefaultArtifactDeployer.LOG.debug( "Looking for SSH keys in " + sshHome );
263         File key = new File( sshHome, "id_dsa" );
264         if ( key.exists() )
265         {
266             DefaultArtifactDeployer.LOG.debug( "found " + key );
267             return key.getAbsolutePath();
268         }
269         key = new File( sshHome, "id_rsa" );
270         if ( key.exists() )
271         {
272             DefaultArtifactDeployer.LOG.debug( "found " + key );
273             return key.getAbsolutePath();
274         }
275         key = new File( sshHome, "identity" );
276         if ( key.exists() )
277         {
278             DefaultArtifactDeployer.LOG.debug( "found " + key );
279             return key.getAbsolutePath();
280         }
281         return null;
282     }
283 
284     private void doDeploy( final File file, final Project project, final String artifactId,
285                            final ArtifactTypeHandler handler, final String version, final String type,
286                            final boolean gpgSkip, final String gpgPass, final boolean gpgUseAgent,
287                            final String gpgKeyname ) throws MavenException
288     {
289         final List srcFiles = new ArrayList( 3 );
290         final List destFiles = new ArrayList( 3 );
291 
292         srcFiles.add( file );
293         destFiles.add( handler.constructRepositoryFullPath( type, project, version ) );
294 
295         if ( version.indexOf( MavenConstants.SNAPSHOT_SIGNIFIER ) >= 0 )
296         {
297             final String signature = this.getSnapshotSignature();
298             final String v = StringUtils.replace( version, MavenConstants.SNAPSHOT_SIGNIFIER, signature );
299 
300             final File snapshotVersionFile = this.createSnapshotVersionFile( file, v, artifactId, type );
301 
302             final String snapshotVersionsFilename =
303                 handler.constructRepositoryDirectoryPath( type, project ) + artifactId + "-snapshot-version";
304 
305             srcFiles.add( snapshotVersionFile );
306             destFiles.add( snapshotVersionsFilename );
307 
308             final String deployTimestamp =
309                 (String) project.getContext().getVariable( "maven.artifact.deploy.timestamps" );
310             if ( deployTimestamp.equals( "true" ) )
311             {
312                 srcFiles.add( file );
313                 destFiles.add( handler.constructRepositoryFullPath( type, project, v ) );
314             }
315         }
316 
317         // trick add special values to context for default repository;
318 
319         String repoStr = (String) project.getContext().getVariable( "maven.repo.list" );
320 
321         if ( ( repoStr == null ) || ( repoStr.trim().length() == 0 ) )
322         {
323             String central = project.getDistributionSite();
324             String centralDirectory = project.getDistributionDirectory();
325             if ( ( central == null ) || ( central.trim().length() == 0 ) )
326             {
327                 central = (String) project.getContext().getVariable( "maven.repo.central" );
328                 centralDirectory = (String) project.getContext().getVariable( "maven.repo.central.directory" );
329             }
330             if ( ( central != null ) && ( central.trim().length() > 0 ) )
331             {
332                 repoStr = "default";
333                 project.getContext().setVariable( "maven.repo.default", "scp://" + central );
334                 if ( project.getContext().getVariable( "maven.repo.default.privatekey" ) == null )
335                 {
336                     project.getContext().setVariable( "maven.repo.default.privatekey", this.findSshIdentity() );
337                 }
338                 if ( project.getContext().getVariable( "maven.repo.default.passphrase" ) == null )
339                 {
340                     DefaultArtifactDeployer.LOG.warn( "WARNING: assuming empty passphrase. Specify maven.repo.default.passphrase if needed" );
341                     project.getContext().setVariable( "maven.repo.default.passphrase", "" );
342                 }
343                 project.getContext().setVariable( "maven.repo.default.directory", centralDirectory );
344                 project.getContext().setVariable( "maven.repo.default.username",
345                                                   project.getContext().getVariable( "maven.username" ) );
346                 project.getContext().setVariable( "maven.repo.default.group",
347                                                   project.getContext().getVariable( "maven.remote.group" ) );
348             }
349         }
350 
351         final String[] repos = StringUtils.split( repoStr, "," );
352 
353         DefaultArtifactDeployer.LOG.info( "Will deploy to " + repos.length + " repository(ies): " + repoStr );
354         boolean success = false;
355         for ( int i = 0; i < repos.length; i++ )
356         {
357 
358             final String repo = repos[i].trim();
359             DefaultArtifactDeployer.LOG.info( "Deploying to repository: " + repo );
360             final Repository repository = RepositoryBuilder.getRepository( project, repo );
361             final AuthenticationInfo authenticationInfo = RepositoryBuilder.getAuthenticationInfo( project, repo );
362             try
363             {
364                 this.deployFiles( repository, srcFiles, destFiles, authenticationInfo, project, gpgSkip, gpgPass,
365                                   gpgUseAgent, gpgKeyname );
366                 success = true;
367             }
368             catch ( final Exception e )
369             {
370                 final String msg = "Failed to deploy to: " + repository.getId() + " Reason: " + e;
371                 DefaultArtifactDeployer.LOG.warn( msg, e );
372                 // deploy to next repository
373                 continue;
374             }
375 
376         }
377         if ( !success )
378         {
379             throw new MavenException( "Unable to deploy to any repositories" );
380         }
381     }
382 
383     protected void deployFile( final Repository repository, final File src, final String dest, final Project project,
384                                final boolean gpgSkip, final String gpgPass, final boolean gpgUseAgent,
385                                final String gpgKeyname )
386         throws ResourceDoesNotExistException, MalformedURLException, NoSuchAlgorithmException, TransferFailedException,
387         ConnectionException, AuthenticationException, AuthorizationException, MavenException
388     {
389         this.deployFiles( repository, Collections.singletonList( src ), Collections.singletonList( dest ), null,
390                           project, gpgSkip, gpgPass, gpgUseAgent, gpgKeyname );
391     }
392 
393     protected void deployFiles( final Repository repository, final List srcFiles, final List destFiles,
394                                 final AuthenticationInfo authenticationInfo, final Project project,
395                                 final boolean gpgSkip, final String gpgPass, final boolean gpgUseAgent,
396                                 final String gpgKeyname )
397         throws ConnectionException, AuthenticationException, ResourceDoesNotExistException, TransferFailedException,
398         AuthorizationException, MalformedURLException, NoSuchAlgorithmException, MavenException
399     {
400 
401         if ( srcFiles.size() != destFiles.size() )
402         {
403             final String msg = "Lengths of the lists should be the same";
404             throw new IllegalArgumentException( msg );
405         }
406 
407         final Wagon wagon = this.getWagon( repository.getProtocol(), project, repository.getId() );
408 
409         final TransferListener uploadMonitor = new UploadMeter();
410 
411         final Map checksums = new HashMap( 2 );
412 
413         ChecksumObserver observer = new ChecksumObserver( "MD5" );
414         checksums.put( "md5", observer );
415         wagon.addTransferListener( observer );
416         observer = new ChecksumObserver( "SHA-1" );
417         checksums.put( "sha1", observer );
418         wagon.addTransferListener( observer );
419 
420         try
421         {
422             wagon.connect( repository, authenticationInfo );
423             final Iterator srcIterator = srcFiles.iterator();
424             final Iterator destIterator = destFiles.iterator();
425             while ( srcIterator.hasNext() )
426             {
427                 wagon.addTransferListener( uploadMonitor );
428 
429                 final File srcFile = (File) srcIterator.next();
430                 final String destFile = (String) destIterator.next();
431 
432                 wagon.put( srcFile, destFile );
433 
434                 wagon.removeTransferListener( uploadMonitor );
435 
436                 final Map sums = new HashMap( 2 );
437                 for ( final Iterator i = checksums.keySet().iterator(); i.hasNext(); )
438                 {
439                     // store first - a later put will modify them
440                     final String extension = (String) i.next();
441                     observer = (ChecksumObserver) checksums.get( extension );
442                     sums.put( extension, observer.getActualChecksum() );
443                 }
444 
445                 for ( final Iterator i = checksums.keySet().iterator(); i.hasNext(); )
446                 {
447                     final String extension = (String) i.next();
448 
449                     // TODO: shouldn't need a file intermediatary - improve wagon to take a stream
450                     final File temp = File.createTempFile( "maven-artifact", null );
451                     temp.deleteOnExit();
452                     FileUtils.fileWrite( temp.getAbsolutePath(), (String) sums.get( extension ) );
453 
454                     wagon.put( temp, destFile + "." + extension );
455                 }
456 
457                 if ( !gpgSkip )
458                 {
459                     File gpgSignature = generateSignatureForArtifact( srcFile, gpgPass, gpgUseAgent, gpgKeyname );
460                     wagon.put( gpgSignature, destFile + SIGNATURE_EXTENSION );
461                 }
462 
463             }
464         }
465         catch ( final IOException e )
466         {
467             throw new MavenException( "Error creating temporary file to transfer checksums", e );
468         }
469         finally
470         {
471             try
472             {
473                 wagon.disconnect();
474             }
475             catch ( final Exception e )
476             {
477                 DefaultArtifactDeployer.LOG.error( "Error cleaning up from the deployer", e );
478             }
479         }
480     }
481 
482     private Wagon getWagon( final String protocol, final Project project, final String id )
483         throws MalformedURLException
484     {
485         Wagon wagon;
486 
487         if ( protocol.equals( "http" ) )
488         {
489             wagon = new HttpWagon();
490         }
491         else if ( protocol.equals( "ftp" ) )
492         {
493             wagon = new FtpWagon();
494             RepositoryBuilder.configureFtpWagon( project, id, (FtpWagon) wagon );
495         }
496         else if ( protocol.equals( "sftp" ) )
497         {
498             wagon = new SftpWagon();
499             RepositoryBuilder.configureSftpWagon( project, id, (SftpWagon) wagon );
500         }
501         else if ( protocol.equals( "file" ) )
502         {
503             wagon = new FileWagon();
504         }
505         else if ( protocol.equals( "scp" ) )
506         {
507             wagon = new ScpWagon();
508             RepositoryBuilder.configureScpWagon( project, id, (ScpWagon) wagon );
509         }
510         else if ( protocol.equals( "scpexe" ) )
511         {
512             wagon = new ScpExternalWagon();
513             RepositoryBuilder.configureSshExternalWagon( project, id, (ScpExternalWagon) wagon );
514             return wagon;
515         }
516         else
517         {
518             throw new MalformedURLException( "Unknown Wagon protocol: " + protocol );
519         }
520 
521         return wagon;
522     }
523 
524     protected String getSnapshotSignature()
525     {
526         if ( this.snapshotSignature == null )
527         {
528             final DateFormat fmt = new SimpleDateFormat( DefaultArtifactDeployer.SNAPSHOT_FORMAT );
529             fmt.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
530             this.snapshotSignature = fmt.format( new Date() );
531         }
532         return this.snapshotSignature;
533     }
534 
535     protected File getFileForArtifact( final String artifact ) throws MavenException
536     {
537         final File file = new File( artifact );
538         if ( !file.exists() )
539         {
540             final String msg = "Artifact file: '" + artifact + "' must exist";
541             throw new MavenException( msg );
542         }
543         if ( !file.canRead() )
544         {
545             final String msg = "Artifact file: '" + artifact + "' must be readable";
546             throw new MavenException( msg );
547         }
548         if ( file.isDirectory() )
549         {
550             final String msg = "Artifact file: '" + artifact + "' must not be a directory";
551             throw new MavenException( msg );
552         }
553         return file.getAbsoluteFile();
554     }
555 
556     /**
557      * Create a file which contains timestamp of the latetst snapshot
558      */
559     protected File createSnapshotVersionFile( final File artifact, final String snapshotVersion,
560                                               final String artifactId, final String type ) throws MavenException
561     {
562         File file = null;
563         final String filename = artifactId + "-" + type + "-snapshot-version";
564         try
565         {
566             file = new File( artifact.getParent(), filename );
567             FileUtils.fileWrite( file.getAbsolutePath(), snapshotVersion );
568             file.deleteOnExit();
569         }
570         catch ( final Exception e )
571         {
572             throw new MavenException( "Cannot create snapshot-version file:" + file );
573         }
574         return file;
575     }
576 
577     /**
578      * @param pass
579      *            The passphrase to use when signing. "${maven.artifact.gpg.passphrase}"
580      * @param keyname
581      *            The "name" of the key to sign with. Passed to gpg as --local-user. "${maven.artifact.gpg.keyname}"
582      * @param useAgent
583      *            Passes --use-agent or --no-use-agent to gpg. If using an agent, the password is optional as the agent
584      *            will provide it. "${maven.artifact.gpg.useagent}"
585      */
586     protected File generateSignatureForArtifact( File file, String pass, boolean useAgent, String keyname )
587         throws MavenException
588     {
589         File signature = new File( file + SIGNATURE_EXTENSION );
590 
591         if ( signature.exists() )
592         {
593             signature.delete();
594         }
595 
596         Commandline cmd = new Commandline();
597 
598         cmd.setExecutable( "gpg" + ( SystemUtils.IS_OS_WINDOWS ? ".exe" : "" ) );
599 
600         if ( useAgent )
601         {
602             cmd.createArgument().setValue( "--use-agent" );
603         }
604         else
605         {
606             cmd.createArgument().setValue( "--no-use-agent" );
607         }
608 
609         InputStream in = null;
610         if ( null != pass )
611         {
612             cmd.createArgument().setValue( "--passphrase-fd" );
613 
614             cmd.createArgument().setValue( "0" );
615 
616             // Prepare the input stream which will be used to pass the passphrase to the executable
617             in = new ByteArrayInputStream( pass.getBytes() );
618         }
619 
620         if ( null != keyname )
621         {
622             cmd.createArgument().setValue( "--local-user" );
623 
624             cmd.createArgument().setValue( keyname );
625         }
626 
627         cmd.createArgument().setValue( "--armor" );
628 
629         cmd.createArgument().setValue( "--detach-sign" );
630 
631         cmd.createArgument().setFile( file );
632 
633         try
634         {
635             LOG.debug( "GPG cmd : " + cmd );
636             int exitCode = CommandLineUtils.executeCommandLine( cmd, in, new DefaultConsumer(), new DefaultConsumer() );
637 
638             if ( exitCode != 0 )
639             {
640                 throw new MavenException( "Exit code: " + exitCode );
641             }
642         }
643         catch ( CommandLineException e )
644         {
645             throw new MavenException( "Unable to execute gpg command", e );
646         }
647 
648         return signature;
649     }
650 
651     protected String getPassphrase( Project project ) throws IOException
652     {
653         String pass = (String) project.getContext().getVariable( "maven.artifact.gpg.passphrase" );
654         if ( pass == null )
655         {
656             // TODO: with JDK 1.6, we could call System.console().readPassword("GPG Passphrase: ", null);
657 
658             BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) );
659             while ( System.in.available() != 0 )
660             {
661                 // there's some junk already on the input stream, consume it
662                 // so we can get the real passphrase
663                 System.in.read();
664             }
665 
666             System.out.print( "GPG Passphrase: " );
667             MaskingThread thread = new MaskingThread();
668             thread.start();
669 
670             pass = in.readLine();
671 
672             // stop masking
673             thread.stopMasking();
674         }
675         project.getContext().setVariable( "maven.artifact.gpg.passphrase", pass );
676         return pass;
677     }
678 
679     // based on ideas from http://java.sun.com/developer/technicalArticles/Security/pwordmask/
680     class MaskingThread extends Thread
681     {
682         private volatile boolean stop;
683 
684         /**
685          * Begin masking until asked to stop.
686          */
687         public void run()
688         {
689             // this needs to be high priority to make sure the characters don't
690             // really get to the screen.
691 
692             int priority = Thread.currentThread().getPriority();
693             Thread.currentThread().setPriority( Thread.MAX_PRIORITY );
694 
695             try
696             {
697                 stop = false;
698                 while ( !stop )
699                 {
700                     // print a backspace + * to overwrite anything they type
701                     System.out.print( "\010*" );
702                     try
703                     {
704                         // attempt masking at this rate
705                         Thread.sleep( 1 );
706                     }
707                     catch ( InterruptedException iex )
708                     {
709                         Thread.currentThread().interrupt();
710                         return;
711                     }
712                 }
713             }
714             finally
715             {
716                 // restore the original priority
717                 Thread.currentThread().setPriority( priority );
718             }
719         }
720 
721         /**
722          * Instruct the thread to stop masking.
723          */
724         public void stopMasking()
725         {
726             this.stop = true;
727         }
728     }
729 }