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