View Javadoc

1   package org.apache.maven.wagon.providers.scm;
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.scm.ScmBranch;
23  import org.apache.maven.scm.ScmException;
24  import org.apache.maven.scm.ScmFile;
25  import org.apache.maven.scm.ScmFileSet;
26  import org.apache.maven.scm.ScmResult;
27  import org.apache.maven.scm.ScmRevision;
28  import org.apache.maven.scm.ScmTag;
29  import org.apache.maven.scm.ScmVersion;
30  import org.apache.maven.scm.command.add.AddScmResult;
31  import org.apache.maven.scm.command.checkout.CheckOutScmResult;
32  import org.apache.maven.scm.command.list.ListScmResult;
33  import org.apache.maven.scm.manager.NoSuchScmProviderException;
34  import org.apache.maven.scm.manager.ScmManager;
35  import org.apache.maven.scm.provider.ScmProvider;
36  import org.apache.maven.scm.provider.ScmProviderRepository;
37  import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
38  import org.apache.maven.scm.repository.ScmRepository;
39  import org.apache.maven.scm.repository.ScmRepositoryException;
40  import org.apache.maven.wagon.AbstractWagon;
41  import org.apache.maven.wagon.ConnectionException;
42  import org.apache.maven.wagon.ResourceDoesNotExistException;
43  import org.apache.maven.wagon.TransferFailedException;
44  import org.apache.maven.wagon.authorization.AuthorizationException;
45  import org.apache.maven.wagon.events.TransferEvent;
46  import org.apache.maven.wagon.resource.Resource;
47  import org.codehaus.plexus.util.FileUtils;
48  import org.codehaus.plexus.util.StringUtils;
49  
50  import java.io.File;
51  import java.io.IOException;
52  import java.text.DecimalFormat;
53  import java.util.ArrayList;
54  import java.util.Iterator;
55  import java.util.List;
56  import java.util.Random;
57  import java.util.Stack;
58  
59  /**
60   * Wagon provider to get and put files from and to SCM systems, using Maven-SCM as underlying transport.
61   * <p/>
62   * TODO it probably creates problems if the same wagon is used in two different SCM protocols, as instance variables can
63   * keep incorrect state.
64   * TODO: For doing releases we either have to be able to add files with checking out the repository structure which may not be
65   * possible, or the checkout directory needs to be a constant. Doing releases won't scale if you have to checkout the
66   * whole repository structure in order to add 3 files.
67   *
68   * @author <a href="brett@apache.org">Brett Porter</a>
69   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
70   * @author <a href="carlos@apache.org">Carlos Sanchez</a>
71   * @author Jason van Zyl
72   * @version $Id: ScmWagon.java 1174113 2011-09-22 13:16:22Z olamy $
73   * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="scm" instantiation-strategy="per-lookup"
74   */
75  public class ScmWagon
76      extends AbstractWagon
77  {
78      /**
79       * @plexus.requirement
80       */
81      private ScmManager scmManager;
82  
83      /**
84       * The SCM version, if any.
85       *
86       * @parameter
87       */
88      private String scmVersion;
89  
90      /**
91       * The SCM version type, if any. Defaults to "branch".
92       *
93       * @parameter
94       */
95      private String scmVersionType;
96  
97      private File checkoutDirectory;
98  
99      /**
100      * Get the {@link ScmManager} used in this Wagon
101      *
102      * @return the {@link ScmManager}
103      */
104     public ScmManager getScmManager()
105     {
106         return scmManager;
107     }
108 
109     /**
110      * Set the {@link ScmManager} used in this Wagon
111      *
112      * @param scmManager
113      */
114     public void setScmManager( ScmManager scmManager )
115     {
116         this.scmManager = scmManager;
117     }
118 
119     /**
120      * Get the scmVersion used in this Wagon
121      *
122      * @return the scmVersion
123      */
124     public String getScmVersion()
125     {
126         return scmVersion;
127     }
128 
129     /**
130      * Set the scmVersion
131      *
132      * @param scmVersion the scmVersion to set
133      */
134     public void setScmVersion( String scmVersion )
135     {
136         this.scmVersion = scmVersion;
137     }
138 
139     /**
140      * Get the scmVersionType used in this Wagon
141      *
142      * @return the scmVersionType
143      */
144     public String getScmVersionType()
145     {
146         return scmVersionType;
147     }
148 
149     /**
150      * Set the scmVersionType
151      *
152      * @param scmVersionType the scmVersionType to set
153      */
154     public void setScmVersionType( String scmVersionType )
155     {
156         this.scmVersionType = scmVersionType;
157     }
158 
159     /**
160      * Get the directory where Wagon will checkout files from SCM. This directory will be deleted!
161      *
162      * @return directory
163      */
164     public File getCheckoutDirectory()
165     {
166         return checkoutDirectory;
167     }
168 
169     /**
170      * Set the directory where Wagon will checkout files from SCM. This directory will be deleted!
171      *
172      * @param checkoutDirectory
173      */
174     public void setCheckoutDirectory( File checkoutDirectory )
175     {
176         this.checkoutDirectory = checkoutDirectory;
177     }
178 
179     /**
180      * Convenience method to get the {@link ScmProvider} implementation to handle the provided SCM type
181      *
182      * @param scmType type of SCM, eg. <code>svn</code>, <code>cvs</code>
183      * @return the {@link ScmProvider} that will handle provided SCM type
184      * @throws NoSuchScmProviderException if there is no {@link ScmProvider} able to handle that SCM type
185      */
186     public ScmProvider getScmProvider( String scmType )
187         throws NoSuchScmProviderException
188     {
189         return getScmManager().getProviderByType( scmType );
190     }
191 
192     /**
193      * This will cleanup the checkout directory
194      */
195     public void openConnectionInternal()
196         throws ConnectionException
197     {
198         if ( checkoutDirectory == null )
199         {
200             checkoutDirectory = createCheckoutDirectory();
201         }
202 
203         if ( checkoutDirectory.exists() )
204         {
205             removeCheckoutDirectory();
206         }
207 
208         checkoutDirectory.mkdirs();
209     }
210 
211     private File createCheckoutDirectory()
212     {
213         File checkoutDirectory;
214 
215         DecimalFormat fmt = new DecimalFormat( "#####" );
216 
217         Random rand = new Random( System.currentTimeMillis() + Runtime.getRuntime().freeMemory() );
218 
219         synchronized ( rand )
220         {
221             do
222             {
223                 checkoutDirectory = new File( System.getProperty( "java.io.tmpdir" ),
224                                               "wagon-scm" + fmt.format( Math.abs( rand.nextInt() ) ) + ".checkout" );
225             }
226             while ( checkoutDirectory.exists() );
227         }
228 
229         return checkoutDirectory;
230     }
231 
232 
233     private void removeCheckoutDirectory()
234         throws ConnectionException
235     {
236         if ( checkoutDirectory == null )
237         {
238             return; // Silently return.
239         }
240 
241         try
242         {
243             FileUtils.deleteDirectory( checkoutDirectory );
244         }
245         catch ( IOException e )
246         {
247             throw new ConnectionException( "Unable to cleanup checkout directory", e );
248         }
249     }
250 
251     /**
252      * Construct the ScmVersion to use for operations.
253      * <p/>
254      * <p>If scmVersion is supplied, scmVersionType must also be supplied to
255      * take effect.</p>
256      */
257     private ScmVersion makeScmVersion()
258     {
259         if ( StringUtils.isBlank( scmVersion ) )
260         {
261             return null;
262         }
263         if ( scmVersion.length() > 0 )
264         {
265             if ( "revision".equals( scmVersionType ) )
266             {
267                 return new ScmRevision( scmVersion );
268             }
269             else if ( "tag".equals( scmVersionType ) )
270             {
271                 return new ScmTag( scmVersion );
272             }
273             else if ( "branch".equals( scmVersionType ) )
274             {
275                 return new ScmBranch( scmVersion );
276             }
277         }
278 
279         return null;
280     }
281 
282     private ScmRepository getScmRepository( String url )
283         throws ScmRepositoryException, NoSuchScmProviderException
284     {
285         String username = null;
286 
287         String password = null;
288 
289         String privateKey = null;
290 
291         String passphrase = null;
292 
293         if ( authenticationInfo != null )
294         {
295             username = authenticationInfo.getUserName();
296 
297             password = authenticationInfo.getPassword();
298 
299             privateKey = authenticationInfo.getPrivateKey();
300 
301             passphrase = authenticationInfo.getPassphrase();
302         }
303 
304         ScmRepository scmRepository = getScmManager().makeScmRepository( url );
305 
306         ScmProviderRepository providerRepository = scmRepository.getProviderRepository();
307 
308         if ( StringUtils.isNotEmpty( username ) )
309         {
310             providerRepository.setUser( username );
311         }
312 
313         if ( StringUtils.isNotEmpty( password ) )
314         {
315             providerRepository.setPassword( password );
316         }
317 
318         if ( providerRepository instanceof ScmProviderRepositoryWithHost )
319         {
320             ScmProviderRepositoryWithHost providerRepo = (ScmProviderRepositoryWithHost) providerRepository;
321 
322             if ( StringUtils.isNotEmpty( privateKey ) )
323             {
324                 providerRepo.setPrivateKey( privateKey );
325             }
326 
327             if ( StringUtils.isNotEmpty( passphrase ) )
328             {
329                 providerRepo.setPassphrase( passphrase );
330             }
331         }
332 
333         return scmRepository;
334     }
335 
336     public void put( File source, String targetName )
337         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
338     {
339         if ( source.isDirectory() )
340         {
341             throw new IllegalArgumentException( "Source is a directory: " + source );
342         }
343         putInternal( source, targetName );
344     }
345 
346     /**
347      * Puts both files and directories
348      *
349      * @param source
350      * @param targetName
351      * @throws TransferFailedException
352      */
353     private void putInternal( File source, String targetName )
354         throws TransferFailedException
355     {
356         Resource target = new Resource( targetName );
357 
358         firePutInitiated( target, source );
359 
360         try
361         {
362             ScmRepository scmRepository = getScmRepository( getRepository().getUrl() );
363 
364             target.setContentLength( source.length() );
365             target.setLastModified( source.lastModified() );
366 
367             firePutStarted( target, source );
368 
369             String msg = "Wagon: Adding " + source.getName() + " to repository";
370 
371             ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() );
372 
373             String checkoutTargetName = source.isDirectory() ? targetName : getDirname( targetName );
374             String relPath = checkOut( scmProvider, scmRepository, checkoutTargetName, target );
375 
376             File newCheckoutDirectory = new File( checkoutDirectory, relPath );
377 
378             File scmFile = new File( newCheckoutDirectory, source.isDirectory() ? "" : getFilename( targetName ) );
379 
380             boolean fileAlreadyInScm = scmFile.exists();
381 
382             if ( !scmFile.equals( source ) )
383             {
384                 if ( source.isDirectory() )
385                 {
386                     FileUtils.copyDirectoryStructure( source, scmFile );
387                 }
388                 else
389                 {
390                     FileUtils.copyFile( source, scmFile );
391                 }
392             }
393 
394             if ( !fileAlreadyInScm || scmFile.isDirectory() )
395             {
396                 int addedFiles = addFiles( scmProvider, scmRepository, newCheckoutDirectory,
397                                            source.isDirectory() ? "" : scmFile.getName() );
398 
399                 if ( !fileAlreadyInScm && addedFiles == 0 )
400                 {
401                     throw new ScmException(
402                         "Unable to add file to SCM: " + scmFile + "; see error messages above for more information" );
403                 }
404             }
405 
406             ScmResult result =
407                 scmProvider.checkIn( scmRepository, new ScmFileSet( checkoutDirectory ), makeScmVersion(), msg );
408 
409             checkScmResult( result );
410         }
411         catch ( ScmException e )
412         {
413             fireTransferError( target, e, TransferEvent.REQUEST_GET );
414 
415             throw new TransferFailedException( "Error interacting with SCM: " + e.getMessage(), e );
416         }
417         catch ( IOException e )
418         {
419             fireTransferError( target, e, TransferEvent.REQUEST_GET );
420 
421             throw new TransferFailedException( "Error interacting with SCM: " + e.getMessage(), e );
422         }
423 
424         if ( source.isFile() )
425         {
426             postProcessListeners( target, source, TransferEvent.REQUEST_PUT );
427         }
428 
429         firePutCompleted( target, source );
430     }
431 
432     /**
433      * Returns the relative path to targetName in the checkout dir. If the targetName already exists in the scm, this
434      * will be the empty string.
435      *
436      * @param scmProvider
437      * @param scmRepository
438      * @param targetName
439      * @return
440      * @throws TransferFailedException
441      */
442     private String checkOut( ScmProvider scmProvider, ScmRepository scmRepository, String targetName,
443                              Resource resource )
444         throws TransferFailedException
445     {
446         checkoutDirectory = createCheckoutDirectory();
447 
448         Stack stack = new Stack();
449 
450         String target = targetName;
451 
452         // totally ignore scmRepository parent stuff since that is not supported by all scms.
453         // Instead, assume that that url exists. If not, then that's an error.
454         // Check whether targetName, which is a relative path into the scm, exists.
455         // If it doesn't, check the parent, etc.
456 
457         try
458         {
459             while ( target.length() > 0 && !scmProvider.list( scmRepository,
460                                                               new ScmFileSet( new File( "." ), new File( target ) ),
461                                                               false, makeScmVersion() ).isSuccess() )
462             {
463                 stack.push( getFilename( target ) );
464                 target = getDirname( target );
465             }
466         }
467         catch ( ScmException e )
468         {
469             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
470 
471             throw new TransferFailedException( "Error listing repository: " + e.getMessage(), e );
472         }
473 
474         // ok, we've established that target exists, or is empty.
475         // Check the resource out; if it doesn't exist, that means we're in the svn repo url root,
476         // and the configuration is incorrect. We will not try repo.getParent since most scm's don't
477         // implement that.
478 
479         try
480         {
481             String repoUrl = getRepository().getUrl();
482             if ( "svn".equals( scmProvider.getScmType() ) )
483             {
484                 // Subversion is the only SCM that adds path structure to represent tags and branches.
485                 // The rest use scmVersion and scmVersionType.
486                 repoUrl += "/" + target.replace( '\\', '/' );
487             }
488             scmRepository = getScmRepository( repoUrl );
489             CheckOutScmResult ret =
490                 scmProvider.checkOut( scmRepository, new ScmFileSet( new File( checkoutDirectory, "" ) ),
491                                       makeScmVersion(), false );
492 
493             checkScmResult( ret );
494         }
495         catch ( ScmException e )
496         {
497             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
498 
499             throw new TransferFailedException( "Error checking out: " + e.getMessage(), e );
500         }
501 
502         // now create the subdirs in target, if it's a parent of targetName
503 
504         String relPath = "";
505 
506         while ( !stack.isEmpty() )
507         {
508             String p = (String) stack.pop();
509             relPath += p + "/";
510 
511             File newDir = new File( checkoutDirectory, relPath );
512             if ( !newDir.mkdirs() )
513             {
514                 throw new TransferFailedException(
515                     "Failed to create directory " + newDir.getAbsolutePath() + "; parent should exist: "
516                         + checkoutDirectory );
517             }
518 
519             try
520             {
521                 addFiles( scmProvider, scmRepository, checkoutDirectory, relPath );
522             }
523             catch ( ScmException e )
524             {
525                 fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
526 
527                 throw new TransferFailedException( "Failed to add directory " + newDir + " to working copy", e );
528             }
529         }
530 
531         return relPath;
532     }
533 
534     /**
535      * Add a file or directory to a SCM repository. If it's a directory all its contents are added recursively.
536      * <p/>
537      * TODO this is less than optimal, SCM API should provide a way to add a directory recursively
538      *
539      * @param scmProvider   SCM provider
540      * @param scmRepository SCM repository
541      * @param basedir       local directory corresponding to scmRepository
542      * @param scmFilePath   path of the file or directory to add, relative to basedir
543      * @return the number of files added.
544      * @throws ScmException
545      */
546     private int addFiles( ScmProvider scmProvider, ScmRepository scmRepository, File basedir, String scmFilePath )
547         throws ScmException
548     {
549         int addedFiles = 0;
550 
551         File scmFile = new File( basedir, scmFilePath );
552 
553         if ( scmFilePath.length() != 0 )
554         {
555             AddScmResult result = scmProvider.add( scmRepository, new ScmFileSet( basedir, new File( scmFilePath ) ) );
556 
557             /*
558              * TODO dirty fix to work around files with property svn:eol-style=native if a file has that property, first
559              * time file is added it fails, second time it succeeds the solution is check if the scm provider is svn and
560              * unset that property when the SCM API allows it
561              */
562             if ( !result.isSuccess() )
563             {
564                 result = scmProvider.add( scmRepository, new ScmFileSet( basedir, new File( scmFilePath ) ) );
565             }
566 
567             addedFiles = result.getAddedFiles().size();
568         }
569 
570         String reservedScmFile = scmProvider.getScmSpecificFilename();
571 
572         if ( scmFile.isDirectory() )
573         {
574             File[] files = scmFile.listFiles();
575 
576             for ( int i = 0; i < files.length; i++ )
577             {
578                 if ( reservedScmFile != null && !reservedScmFile.equals( files[i].getName() ) )
579                 {
580                     addedFiles += addFiles( scmProvider, scmRepository, basedir,
581                                             ( scmFilePath.length() == 0 ? "" : scmFilePath + "/" )
582                                                 + files[i].getName() );
583                 }
584             }
585         }
586 
587         return addedFiles;
588     }
589 
590     /**
591      * @return true
592      */
593     public boolean supportsDirectoryCopy()
594     {
595         return true;
596     }
597 
598     public void putDirectory( File sourceDirectory, String destinationDirectory )
599         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
600     {
601         if ( !sourceDirectory.isDirectory() )
602         {
603             throw new IllegalArgumentException( "Source is not a directory: " + sourceDirectory );
604         }
605 
606         putInternal( sourceDirectory, destinationDirectory );
607     }
608 
609     /**
610      * Check that the ScmResult was a successful operation
611      *
612      * @param result
613      * @throws TransferFailedException if result was not a successful operation
614      * @throws ScmException
615      */
616     private void checkScmResult( ScmResult result )
617         throws ScmException
618     {
619         if ( !result.isSuccess() )
620         {
621             throw new ScmException(
622                 "Unable to commit file. " + result.getProviderMessage() + " " + ( result.getCommandOutput() == null
623                     ? ""
624                     : result.getCommandOutput() ) );
625         }
626     }
627 
628     public void closeConnection()
629         throws ConnectionException
630     {
631         removeCheckoutDirectory();
632     }
633 
634     /**
635      * Not implemented
636      *
637      * @throws UnsupportedOperationException always
638      */
639     public boolean getIfNewer( String resourceName, File destination, long timestamp )
640         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
641     {
642         throw new UnsupportedOperationException( "Not currently supported: getIfNewer" );
643     }
644 
645     public void get( String resourceName, File destination )
646         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
647     {
648         Resource resource = new Resource( resourceName );
649 
650         fireGetInitiated( resource, destination );
651 
652         String url = getRepository().getUrl() + "/" + resourceName;
653 
654         // remove the file
655         url = url.substring( 0, url.lastIndexOf( '/' ) );
656 
657         try
658         {
659             ScmRepository scmRepository = getScmRepository( url );
660 
661             fireGetStarted( resource, destination );
662 
663             // TODO: limitations:
664             // - destination filename must match that in the repository - should allow the "-d" CVS equiv to be passed
665             //   in
666             // - we don't get granular exceptions from SCM (ie, auth, not found)
667             // - need to make it non-recursive to save time
668             // - exists() check doesn't test if it is in SCM already
669 
670             File scmFile = new File( checkoutDirectory, resourceName );
671 
672             File basedir = scmFile.getParentFile();
673 
674             ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() );
675 
676             String reservedScmFile = scmProvider.getScmSpecificFilename();
677 
678             if ( reservedScmFile != null && new File( basedir, reservedScmFile ).exists() )
679             {
680                 scmProvider.update( scmRepository, new ScmFileSet( basedir ), makeScmVersion() );
681             }
682             else
683             {
684                 // TODO: this should be checking out a full hierarchy (requires the -d equiv)
685                 basedir.mkdirs();
686 
687                 scmProvider.checkOut( scmRepository, new ScmFileSet( basedir ), makeScmVersion() );
688             }
689 
690             if ( !scmFile.exists() )
691             {
692                 throw new ResourceDoesNotExistException( "Unable to find resource " + destination + " after checkout" );
693             }
694 
695             if ( !scmFile.equals( destination ) )
696             {
697                 FileUtils.copyFile( scmFile, destination );
698             }
699         }
700         catch ( ScmException e )
701         {
702             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
703 
704             throw new TransferFailedException( "Error getting file from SCM", e );
705         }
706         catch ( IOException e )
707         {
708             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
709 
710             throw new TransferFailedException( "Error getting file from SCM", e );
711         }
712 
713         postProcessListeners( resource, destination, TransferEvent.REQUEST_GET );
714 
715         fireGetCompleted( resource, destination );
716     }
717 
718     /**
719      * @return a List&lt;String&gt; with filenames/directories at the resourcepath.
720      * @see org.apache.maven.wagon.AbstractWagon#getFileList(java.lang.String)
721      */
722     public List<String> getFileList( String resourcePath )
723         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
724     {
725         try
726         {
727             ScmRepository repository = getScmRepository( getRepository().getUrl() );
728 
729             ScmProvider provider = getScmProvider( repository.getProvider() );
730 
731             ListScmResult result =
732                 provider.list( repository, new ScmFileSet( new File( "." ), new File( resourcePath ) ), false,
733                                makeScmVersion() );
734 
735             if ( !result.isSuccess() )
736             {
737                 throw new ResourceDoesNotExistException( result.getProviderMessage() );
738             }
739 
740             List<String> files = new ArrayList<String>();
741 
742             for ( Iterator<ScmFile> it = result.getFiles().iterator(); it.hasNext(); )
743             {
744                 ScmFile f = it.next();
745                 files.add( f.getPath() );
746             }
747 
748             return files;
749         }
750         catch ( ScmException e )
751         {
752             throw new TransferFailedException( "Error getting filelist from SCM", e );
753         }
754     }
755 
756     public boolean resourceExists( String resourceName )
757         throws TransferFailedException, AuthorizationException
758     {
759         try
760         {
761             getFileList( resourceName );
762 
763             return true;
764         }
765         catch ( ResourceDoesNotExistException e )
766         {
767             return false;
768         }
769     }
770 
771     private String getFilename( String filename )
772     {
773         String fname = StringUtils.replace( filename, "/", File.separator );
774         return FileUtils.filename( fname );
775     }
776 
777     private String getDirname( String filename )
778     {
779         String fname = StringUtils.replace( filename, "/", File.separator );
780         return FileUtils.dirname( fname );
781     }
782 }