001package org.apache.maven.scm.provider.perforce;
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
022
023import java.io.BufferedReader;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStreamReader;
027import java.net.InetAddress;
028import java.net.UnknownHostException;
029import java.util.Arrays;
030import java.util.HashSet;
031
032import org.apache.maven.scm.CommandParameters;
033import org.apache.maven.scm.ScmException;
034import org.apache.maven.scm.ScmFileSet;
035import org.apache.maven.scm.command.add.AddScmResult;
036import org.apache.maven.scm.command.blame.BlameScmResult;
037import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
038import org.apache.maven.scm.command.checkin.CheckInScmResult;
039import org.apache.maven.scm.command.checkout.CheckOutScmResult;
040import org.apache.maven.scm.command.diff.DiffScmResult;
041import org.apache.maven.scm.command.edit.EditScmResult;
042import org.apache.maven.scm.command.login.LoginScmResult;
043import org.apache.maven.scm.command.remove.RemoveScmResult;
044import org.apache.maven.scm.command.status.StatusScmResult;
045import org.apache.maven.scm.command.tag.TagScmResult;
046import org.apache.maven.scm.command.unedit.UnEditScmResult;
047import org.apache.maven.scm.command.update.UpdateScmResult;
048import org.apache.maven.scm.log.ScmLogger;
049import org.apache.maven.scm.provider.AbstractScmProvider;
050import org.apache.maven.scm.provider.ScmProviderRepository;
051import org.apache.maven.scm.provider.perforce.command.PerforceInfoCommand;
052import org.apache.maven.scm.provider.perforce.command.PerforceWhereCommand;
053import org.apache.maven.scm.provider.perforce.command.add.PerforceAddCommand;
054import org.apache.maven.scm.provider.perforce.command.blame.PerforceBlameCommand;
055import org.apache.maven.scm.provider.perforce.command.changelog.PerforceChangeLogCommand;
056import org.apache.maven.scm.provider.perforce.command.checkin.PerforceCheckInCommand;
057import org.apache.maven.scm.provider.perforce.command.checkout.PerforceCheckOutCommand;
058import org.apache.maven.scm.provider.perforce.command.diff.PerforceDiffCommand;
059import org.apache.maven.scm.provider.perforce.command.edit.PerforceEditCommand;
060import org.apache.maven.scm.provider.perforce.command.login.PerforceLoginCommand;
061import org.apache.maven.scm.provider.perforce.command.remove.PerforceRemoveCommand;
062import org.apache.maven.scm.provider.perforce.command.status.PerforceStatusCommand;
063import org.apache.maven.scm.provider.perforce.command.tag.PerforceTagCommand;
064import org.apache.maven.scm.provider.perforce.command.unedit.PerforceUnEditCommand;
065import org.apache.maven.scm.provider.perforce.command.update.PerforceUpdateCommand;
066import org.apache.maven.scm.provider.perforce.repository.PerforceScmProviderRepository;
067import org.apache.maven.scm.repository.ScmRepositoryException;
068import org.codehaus.plexus.util.StringUtils;
069import org.codehaus.plexus.util.cli.Commandline;
070
071/**
072 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l </a>
073 * @author mperham
074 *
075 * @plexus.component role="org.apache.maven.scm.provider.ScmProvider" role-hint="perforce"
076 */
077public class PerforceScmProvider
078    extends AbstractScmProvider
079{
080    private static final String [] PROTOCOLS = { "tcp", "tcp4", "tcp6", "tcp46", "tcp64", "ssl", "ssl4", "ssl6", "ssl46", "ssl64" };
081    // ----------------------------------------------------------------------
082    // ScmProvider Implementation
083    // ----------------------------------------------------------------------
084
085    public boolean requiresEditMode()
086    {
087        return true;
088    }
089
090    public ScmProviderRepository makeProviderScmRepository( String scmSpecificUrl, char delimiter )
091        throws ScmRepositoryException
092    {
093        String protocol = null;
094        String path;
095        int port = 0;
096        String host = null;
097
098        //minimal logic to support perforce protocols in scm url, and keep the next part unchange
099        int i0 = scmSpecificUrl.indexOf( delimiter );
100        if ( i0 > 0 )
101        {
102            protocol = scmSpecificUrl.substring( 0, i0 );
103            HashSet<String> protocols = new HashSet<String>( Arrays.asList( PROTOCOLS ));
104            if ( protocols.contains( protocol ) )
105            {
106                scmSpecificUrl = scmSpecificUrl.substring( i0 + 1 );
107            }
108            else
109            {
110                protocol = null;
111            }
112        }
113
114        int i1 = scmSpecificUrl.indexOf( delimiter );
115        int i2 = scmSpecificUrl.indexOf( delimiter, i1 + 1 );
116
117        if ( i1 > 0 )
118        {
119            int lastDelimiter = scmSpecificUrl.lastIndexOf( delimiter );
120            path = scmSpecificUrl.substring( lastDelimiter + 1 );
121            host = scmSpecificUrl.substring( 0, i1 );
122
123            // If there is tree parts in the scm url, the second is the port
124            if ( i2 >= 0 )
125            {
126                try
127                {
128                    String tmp = scmSpecificUrl.substring( i1 + 1, lastDelimiter );
129                    port = Integer.parseInt( tmp );
130                }
131                catch ( NumberFormatException ex )
132                {
133                    throw new ScmRepositoryException( "The port has to be a number." );
134                }
135            }
136        }
137        else
138        {
139            path = scmSpecificUrl;
140        }
141
142        String user = null;
143        String password = null;
144        if ( host != null && host.indexOf( '@' ) > 1 )
145        {
146            user = host.substring( 0, host.indexOf( '@' ) );
147            host = host.substring( host.indexOf( '@' ) + 1 );
148        }
149
150        if ( path.indexOf( '@' ) > 1 )
151        {
152            if ( host != null )
153            {
154                if ( getLogger().isWarnEnabled() )
155                {
156                    getLogger().warn(
157                                      "Username as part of path is deprecated, the new format is "
158                                          + "scm:perforce:[username@]host:port:path_to_repository" );
159                }
160            }
161
162            user = path.substring( 0, path.indexOf( '@' ) );
163            path = path.substring( path.indexOf( '@' ) + 1 );
164        }
165
166        return new PerforceScmProviderRepository( protocol, host, port, path, user, password );
167    }
168
169    public String getScmType()
170    {
171        return "perforce";
172    }
173
174    /** {@inheritDoc} */
175    protected ChangeLogScmResult changelog( ScmProviderRepository repository, ScmFileSet fileSet,
176                                            CommandParameters parameters )
177        throws ScmException
178    {
179        PerforceChangeLogCommand command = new PerforceChangeLogCommand();
180        command.setLogger( getLogger() );
181        return (ChangeLogScmResult) command.execute( repository, fileSet, parameters );
182    }
183
184    public AddScmResult add( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
185        throws ScmException
186    {
187        PerforceAddCommand command = new PerforceAddCommand();
188        command.setLogger( getLogger() );
189        return (AddScmResult) command.execute( repository, fileSet, params );
190    }
191
192    protected RemoveScmResult remove( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
193        throws ScmException
194    {
195        PerforceRemoveCommand command = new PerforceRemoveCommand();
196        command.setLogger( getLogger() );
197        return (RemoveScmResult) command.execute( repository, fileSet, params );
198    }
199
200    protected CheckInScmResult checkin( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
201        throws ScmException
202    {
203        PerforceCheckInCommand command = new PerforceCheckInCommand();
204        command.setLogger( getLogger() );
205        return (CheckInScmResult) command.execute( repository, fileSet, params );
206    }
207
208    protected CheckOutScmResult checkout( ScmProviderRepository repository, ScmFileSet fileSet,
209                                          CommandParameters params )
210        throws ScmException
211    {
212        PerforceCheckOutCommand command = new PerforceCheckOutCommand();
213        command.setLogger( getLogger() );
214        return (CheckOutScmResult) command.execute( repository, fileSet, params );
215    }
216
217    protected DiffScmResult diff( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
218        throws ScmException
219    {
220        PerforceDiffCommand command = new PerforceDiffCommand();
221        command.setLogger( getLogger() );
222        return (DiffScmResult) command.execute( repository, fileSet, params );
223    }
224
225    protected EditScmResult edit( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
226        throws ScmException
227    {
228        PerforceEditCommand command = new PerforceEditCommand();
229        command.setLogger( getLogger() );
230        return (EditScmResult) command.execute( repository, fileSet, params );
231    }
232
233    protected LoginScmResult login( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
234        throws ScmException
235    {
236        PerforceLoginCommand command = new PerforceLoginCommand();
237        command.setLogger( getLogger() );
238        return (LoginScmResult) command.execute( repository, fileSet, params );
239    }
240
241    protected StatusScmResult status( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
242        throws ScmException
243    {
244        PerforceStatusCommand command = new PerforceStatusCommand();
245        command.setLogger( getLogger() );
246        return (StatusScmResult) command.execute( repository, fileSet, params );
247    }
248
249    protected TagScmResult tag( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
250        throws ScmException
251    {
252        PerforceTagCommand command = new PerforceTagCommand();
253        command.setLogger( getLogger() );
254        return (TagScmResult) command.execute( repository, fileSet, params );
255    }
256
257    protected UnEditScmResult unedit( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
258        throws ScmException
259    {
260        PerforceUnEditCommand command = new PerforceUnEditCommand();
261        command.setLogger( getLogger() );
262        return (UnEditScmResult) command.execute( repository, fileSet, params );
263    }
264
265    protected UpdateScmResult update( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
266        throws ScmException
267    {
268        PerforceUpdateCommand command = new PerforceUpdateCommand();
269        command.setLogger( getLogger() );
270        return (UpdateScmResult) command.execute( repository, fileSet, params );
271    }
272
273    protected BlameScmResult blame( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
274        throws ScmException
275    {
276        PerforceBlameCommand command = new PerforceBlameCommand();
277        command.setLogger( getLogger() );
278        return (BlameScmResult) command.execute( repository, fileSet, params );
279    }
280
281    public static Commandline createP4Command( PerforceScmProviderRepository repo, File workingDir )
282    {
283        Commandline command = new Commandline();
284        command.setExecutable( "p4" );
285        if ( workingDir != null )
286        {
287            // SCM-209
288            command.createArg().setValue( "-d" );
289            command.createArg().setValue( workingDir.getAbsolutePath() );
290        }
291
292
293        if ( repo.getHost() != null )
294        {
295            command.createArg().setValue( "-p" );
296            String value = "";
297            if ( ! StringUtils.isBlank( repo.getProtocol() ) )
298            {
299                value += repo.getProtocol() + ":";
300            }
301            value += repo.getHost();
302            if ( repo.getPort() != 0 )
303            {
304                value += ":" + Integer.toString( repo.getPort() );
305            }
306            command.createArg().setValue( value );
307        }
308
309        if ( StringUtils.isNotEmpty( repo.getUser() ) )
310        {
311            command.createArg().setValue( "-u" );
312            command.createArg().setValue( repo.getUser() );
313        }
314
315        if ( StringUtils.isNotEmpty( repo.getPassword() ) )
316        {
317            command.createArg().setValue( "-P" );
318            command.createArg().setValue( repo.getPassword() );
319        }
320        return command;
321    }
322
323    public static String clean( String string )
324    {
325        if ( string.indexOf( " -P " ) == -1 )
326        {
327            return string;
328        }
329        int idx = string.indexOf( " -P " ) + 4;
330        int end = string.indexOf( ' ', idx );
331        return string.substring( 0, idx ) + StringUtils.repeat( "*", end - idx ) + string.substring( end );
332    }
333
334    /**
335     * Given a path like "//depot/foo/bar", returns the
336     * proper path to include everything beneath it.
337     * <p/>
338     * //depot/foo/bar -> //depot/foo/bar/...
339     * //depot/foo/bar/ -> //depot/foo/bar/...
340     * //depot/foo/bar/... -> //depot/foo/bar/...
341     *
342     * @param repoPath
343     * @return
344     */
345    public static String getCanonicalRepoPath( String repoPath )
346    {
347        if ( repoPath.endsWith( "/..." ) )
348        {
349            return repoPath;
350        }
351        else if ( repoPath.endsWith( "/" ) )
352        {
353            return repoPath + "...";
354        }
355        else
356        {
357            return repoPath + "/...";
358        }
359    }
360
361    private static final String NEWLINE = "\r\n";
362
363    /*
364     * Clientspec name can be overridden with the system property below.  I don't
365     * know of any way for this code to get access to maven's settings.xml so this
366     * is the best I can do.
367     *
368     * Sample clientspec:
369
370     Client: mperham-mikeperham-dt-maven
371     Root: d:\temp\target
372     Owner: mperham
373     View:
374     //depot/sandbox/mperham/tsa/tsa-domain/... //mperham-mikeperham-dt-maven/...
375     Description:
376     Created by maven-scm-provider-perforce
377
378     */
379    public static String createClientspec( ScmLogger logger, PerforceScmProviderRepository repo, File workDir,
380                                           String repoPath )
381    {
382        String clientspecName = getClientspecName( logger, repo, workDir );
383        String userName = getUsername( logger, repo );
384
385        String rootDir;
386        try
387        {
388            // SCM-184
389            rootDir = workDir.getCanonicalPath();
390        }
391        catch ( IOException ex )
392        {
393            //getLogger().error("Error getting canonical path for working directory: " + workDir, ex);
394            rootDir = workDir.getAbsolutePath();
395        }
396
397        StringBuilder buf = new StringBuilder();
398        buf.append( "Client: " ).append( clientspecName ).append( NEWLINE );
399        buf.append( "Root: " ).append( rootDir ).append( NEWLINE );
400        buf.append( "Owner: " ).append( userName ).append( NEWLINE );
401        buf.append( "View:" ).append( NEWLINE );
402        buf.append( "\t" ).append( PerforceScmProvider.getCanonicalRepoPath( repoPath ) );
403        buf.append( " //" ).append( clientspecName ).append( "/..." ).append( NEWLINE );
404        buf.append( "Description:" ).append( NEWLINE );
405        buf.append( "\t" ).append( "Created by maven-scm-provider-perforce" ).append( NEWLINE );
406        return buf.toString();
407    }
408
409    public static final String DEFAULT_CLIENTSPEC_PROPERTY = "maven.scm.perforce.clientspec.name";
410
411    public static String getClientspecName( ScmLogger logger, PerforceScmProviderRepository repo, File workDir )
412    {
413        String def = generateDefaultClientspecName( logger, repo, workDir );
414        // until someone put clearProperty in DefaultContinuumScm.getScmRepository( Project , boolean  )
415        String l = System.getProperty( DEFAULT_CLIENTSPEC_PROPERTY, def );
416        if ( l == null || "".equals( l.trim() ) )
417        {
418            return def;
419        }
420        return l;
421    }
422
423    private static String generateDefaultClientspecName( ScmLogger logger, PerforceScmProviderRepository repo,
424                                                         File workDir )
425    {
426        String username = getUsername( logger, repo );
427        String hostname;
428        String path;
429        try
430        {
431            hostname = InetAddress.getLocalHost().getHostName();
432            // [SCM-370][SCM-351] client specs cannot contain forward slashes, spaces and ~; "-" is okay
433            path = workDir.getCanonicalPath().replaceAll( "[/ ~]", "-" );
434        }
435        catch ( UnknownHostException e )
436        {
437            // Should never happen
438            throw new RuntimeException( e );
439        }
440        catch ( IOException e )
441        {
442            throw new RuntimeException( e );
443        }
444        return username + "-" + hostname + "-MavenSCM-" + path;
445    }
446
447    private static String getUsername( ScmLogger logger, PerforceScmProviderRepository repo )
448    {
449        String username = PerforceInfoCommand.getInfo( logger, repo ).getEntry( "User name" );
450        if ( username == null )
451        {
452            // os user != perforce user
453            username = repo.getUser();
454            if ( username == null )
455            {
456                username = System.getProperty( "user.name", "nouser" );
457            }
458        }
459        return username;
460    }
461
462    /**
463     * This is a "safe" method which handles cases where repo.getPath() is
464     * not actually a valid Perforce depot location.  This is a frequent error
465     * due to branches and directory naming where dir name != artifactId.
466     *
467     * @param log     the logging object to use
468     * @param repo    the Perforce repo
469     * @param basedir the base directory we are operating in.  If pom.xml exists in this directory,
470     *                this method will verify <pre>repo.getPath()/pom.xml</pre> == <pre>p4 where basedir/pom.xml</pre>
471     * @return repo.getPath if it is determined to be accurate.  The p4 where location otherwise.
472     */
473    public static String getRepoPath( ScmLogger log, PerforceScmProviderRepository repo, File basedir )
474    {
475        PerforceWhereCommand where = new PerforceWhereCommand( log, repo );
476
477        // Handle an edge case where we release:prepare'd a module with an invalid SCM location.
478        // In this case, the release.properties will contain the invalid URL for checkout purposes
479        // during release:perform.  In this case, the basedir is not the module root so we detect that
480        // and remove the trailing target/checkout directory.
481        if ( basedir.toString().replace( '\\', '/' ).endsWith( "/target/checkout" ) )
482        {
483            String dir = basedir.toString();
484            basedir = new File( dir.substring( 0, dir.length() - "/target/checkout".length() ) );
485            log.debug( "Fixing checkout URL: " + basedir );
486        }
487        File pom = new File( basedir, "pom.xml" );
488        String loc = repo.getPath();
489        log.debug( "SCM path in pom: " + loc );
490        if ( pom.exists() )
491        {
492            loc = where.getDepotLocation( pom );
493            if ( loc == null )
494            {
495                loc = repo.getPath();
496                log.debug( "cannot find depot => using " + loc );
497            }
498            else if ( loc.endsWith( "/pom.xml" ) )
499            {
500                loc = loc.substring( 0, loc.length() - "/pom.xml".length() );
501                log.debug( "Actual POM location: " + loc );
502                if ( !repo.getPath().equals( loc ) )
503                {
504                    log.info( "The SCM location in your pom.xml (" + repo.getPath()
505                        + ") is not equal to the depot location (" + loc
506                        + ").  This happens frequently with branches.  " + "Ignoring the SCM location." );
507                }
508            }
509        }
510        return loc;
511    }
512
513
514    private static Boolean live = null;
515
516    public static boolean isLive()
517    {
518        if ( live == null )
519        {
520            if ( !Boolean.getBoolean( "maven.scm.testing" ) )
521            {
522                // We are not executing in the tests so we are live.
523                live = Boolean.TRUE;
524            }
525            else
526            {
527                // During unit tests, we need to check the local system
528                // to see if the user has Perforce installed.  If not, we mark
529                // the provider as "not live" (or dead, I suppose!) and skip
530                // anything that requires an active server connection.
531                try
532                {
533                    Commandline command = new Commandline();
534                    command.setExecutable( "p4" );
535                    Process proc = command.execute();
536                    BufferedReader br = new BufferedReader( new InputStreamReader( proc.getInputStream() ) );
537                    @SuppressWarnings( "unused" )
538                    String line;
539                    while ( ( line = br.readLine() ) != null )
540                    {
541                        //System.out.println(line);
542                    }
543                    int rc = proc.exitValue();
544                    live = ( rc == 0 ? Boolean.TRUE : Boolean.FALSE );
545                }
546                catch ( Exception e )
547                {
548                    e.printStackTrace();
549                    live = Boolean.FALSE;
550                }
551            }
552        }
553
554        return live.booleanValue();
555    }
556}