001package org.apache.maven.wagon.providers.scm;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.maven.scm.ScmBranch;
023import org.apache.maven.scm.ScmException;
024import org.apache.maven.scm.ScmFile;
025import org.apache.maven.scm.ScmFileSet;
026import org.apache.maven.scm.ScmResult;
027import org.apache.maven.scm.ScmRevision;
028import org.apache.maven.scm.ScmTag;
029import org.apache.maven.scm.ScmVersion;
030import org.apache.maven.scm.command.add.AddScmResult;
031import org.apache.maven.scm.command.checkout.CheckOutScmResult;
032import org.apache.maven.scm.command.list.ListScmResult;
033import org.apache.maven.scm.manager.NoSuchScmProviderException;
034import org.apache.maven.scm.manager.ScmManager;
035import org.apache.maven.scm.provider.ScmProvider;
036import org.apache.maven.scm.provider.ScmProviderRepository;
037import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
038import org.apache.maven.scm.repository.ScmRepository;
039import org.apache.maven.scm.repository.ScmRepositoryException;
040import org.apache.maven.wagon.AbstractWagon;
041import org.apache.maven.wagon.ConnectionException;
042import org.apache.maven.wagon.ResourceDoesNotExistException;
043import org.apache.maven.wagon.TransferFailedException;
044import org.apache.maven.wagon.authorization.AuthorizationException;
045import org.apache.maven.wagon.events.TransferEvent;
046import org.apache.maven.wagon.resource.Resource;
047import org.codehaus.plexus.util.FileUtils;
048import org.codehaus.plexus.util.StringUtils;
049
050import java.io.File;
051import java.io.IOException;
052import java.text.DecimalFormat;
053import java.util.ArrayList;
054import java.util.List;
055import java.util.Random;
056import java.util.Stack;
057
058/**
059 * Wagon provider to get and put files from and to SCM systems, using Maven-SCM as underlying transport.
060 * <p/>
061 * TODO it probably creates problems if the same wagon is used in two different SCM protocols, as instance variables can
062 * keep incorrect state.
063 * TODO: For doing releases, we either have to be able to add files with checking out the repository structure which may not be
064 * possible, or the checkout directory needs to be a constant. Doing releases won't scale if you have to checkout the
065 * whole repository structure in order to add 3 files.
066 *
067 * @author <a href="brett@apache.org">Brett Porter</a>
068 * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
069 * @author <a href="carlos@apache.org">Carlos Sanchez</a>
070 * @author Jason van Zyl
071 *
072 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="scm" instantiation-strategy="per-lookup"
073 */
074public class ScmWagon
075    extends AbstractWagon
076{
077    /**
078     * @plexus.requirement
079     */
080    private volatile ScmManager scmManager;
081
082    /**
083     * The SCM version, if any.
084     *
085     * @parameter
086     */
087    private String scmVersion;
088
089    /**
090     * The SCM version type, if any. Defaults to "branch".
091     *
092     * @parameter
093     */
094    private String scmVersionType;
095
096    private File checkoutDirectory;
097
098    /**
099     * 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 = (String) 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            File[] files = scmFile.listFiles();
574
575            for ( int i = 0; i < files.length; i++ )
576            {
577                if ( reservedScmFile != null && !reservedScmFile.equals( files[i].getName() ) )
578                {
579                    addedFiles += addFiles( scmProvider, scmRepository, basedir,
580                                            ( scmFilePath.length() == 0 ? "" : scmFilePath + "/" )
581                                                + files[i].getName() );
582                }
583            }
584        }
585
586        return addedFiles;
587    }
588
589    /**
590     * @return true
591     */
592    public boolean supportsDirectoryCopy()
593    {
594        return true;
595    }
596
597    public void putDirectory( File sourceDirectory, String destinationDirectory )
598        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
599    {
600        if ( !sourceDirectory.isDirectory() )
601        {
602            throw new IllegalArgumentException( "Source is not a directory: " + sourceDirectory );
603        }
604
605        putInternal( sourceDirectory, destinationDirectory );
606    }
607
608    /**
609     * Check that the ScmResult was a successful operation
610     *
611     * @param result
612     * @throws TransferFailedException if result was not a successful operation
613     * @throws ScmException
614     */
615    private void checkScmResult( ScmResult result )
616        throws ScmException
617    {
618        if ( !result.isSuccess() )
619        {
620            throw new ScmException(
621                "Unable to commit file. " + result.getProviderMessage() + " " + ( result.getCommandOutput() == null
622                    ? ""
623                    : result.getCommandOutput() ) );
624        }
625    }
626
627    public void closeConnection()
628        throws ConnectionException
629    {
630        removeCheckoutDirectory();
631    }
632
633    /**
634     * Not implemented
635     *
636     * @throws UnsupportedOperationException always
637     */
638    public boolean getIfNewer( String resourceName, File destination, long timestamp )
639        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
640    {
641        throw new UnsupportedOperationException( "Not currently supported: getIfNewer" );
642    }
643
644    public void get( String resourceName, File destination )
645        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
646    {
647        Resource resource = new Resource( resourceName );
648
649        fireGetInitiated( resource, destination );
650
651        String url = getRepository().getUrl() + "/" + resourceName;
652
653        // remove the file
654        url = url.substring( 0, url.lastIndexOf( '/' ) );
655
656        try
657        {
658            ScmRepository scmRepository = getScmRepository( url );
659
660            fireGetStarted( resource, destination );
661
662            // TODO: limitations:
663            // - destination filename must match that in the repository - should allow the "-d" CVS equiv to be passed
664            //   in
665            // - we don't get granular exceptions from SCM (ie, auth, not found)
666            // - need to make it non-recursive to save time
667            // - exists() check doesn't test if it is in SCM already
668
669            File scmFile = new File( checkoutDirectory, resourceName );
670
671            File basedir = scmFile.getParentFile();
672
673            ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() );
674
675            String reservedScmFile = scmProvider.getScmSpecificFilename();
676
677            if ( reservedScmFile != null && new File( basedir, reservedScmFile ).exists() )
678            {
679                scmProvider.update( scmRepository, new ScmFileSet( basedir ), makeScmVersion() );
680            }
681            else
682            {
683                // TODO: this should be checking out a full hierarchy (requires the -d equiv)
684                basedir.mkdirs();
685
686                scmProvider.checkOut( scmRepository, new ScmFileSet( basedir ), makeScmVersion() );
687            }
688
689            if ( !scmFile.exists() )
690            {
691                throw new ResourceDoesNotExistException( "Unable to find resource " + destination + " after checkout" );
692            }
693
694            if ( !scmFile.equals( destination ) )
695            {
696                FileUtils.copyFile( scmFile, destination );
697            }
698        }
699        catch ( ScmException e )
700        {
701            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
702
703            throw new TransferFailedException( "Error getting file from SCM", e );
704        }
705        catch ( IOException e )
706        {
707            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
708
709            throw new TransferFailedException( "Error getting file from SCM", e );
710        }
711
712        postProcessListeners( resource, destination, TransferEvent.REQUEST_GET );
713
714        fireGetCompleted( resource, destination );
715    }
716
717    /**
718     * @return a List&lt;String&gt; with filenames/directories at the resourcepath.
719     * @see org.apache.maven.wagon.AbstractWagon#getFileList(java.lang.String)
720     */
721    public List<String> getFileList( String resourcePath )
722        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
723    {
724        try
725        {
726            ScmRepository repository = getScmRepository( getRepository().getUrl() );
727
728            ScmProvider provider = getScmProvider( repository.getProvider() );
729
730            ListScmResult result =
731                provider.list( repository, new ScmFileSet( new File( "." ), new File( resourcePath ) ), false,
732                               makeScmVersion() );
733
734            if ( !result.isSuccess() )
735            {
736                throw new ResourceDoesNotExistException( result.getProviderMessage() );
737            }
738
739            List<String> files = new ArrayList<String>();
740
741            for ( ScmFile f : result.getFiles() )
742            {
743                files.add( f.getPath() );
744            }
745
746            return files;
747        }
748        catch ( ScmException e )
749        {
750            throw new TransferFailedException( "Error getting filelist from SCM", e );
751        }
752    }
753
754    public boolean resourceExists( String resourceName )
755        throws TransferFailedException, AuthorizationException
756    {
757        try
758        {
759            getFileList( resourceName );
760
761            return true;
762        }
763        catch ( ResourceDoesNotExistException e )
764        {
765            return false;
766        }
767    }
768
769    private String getFilename( String filename )
770    {
771        String fname = StringUtils.replace( filename, "/", File.separator );
772        return FileUtils.filename( fname );
773    }
774
775    private String getDirname( String filename )
776    {
777        String fname = StringUtils.replace( filename, "/", File.separator );
778        return FileUtils.dirname( fname );
779    }
780}