001package org.apache.maven.scm.provider.cvslib.cvsjava.util;
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.log.ScmLogger;
023import org.codehaus.plexus.util.StringUtils;
024import org.codehaus.plexus.util.cli.CommandLineUtils;
025import org.netbeans.lib.cvsclient.CVSRoot;
026import org.netbeans.lib.cvsclient.Client;
027import org.netbeans.lib.cvsclient.admin.StandardAdminHandler;
028import org.netbeans.lib.cvsclient.command.Command;
029import org.netbeans.lib.cvsclient.command.CommandAbortedException;
030import org.netbeans.lib.cvsclient.command.CommandException;
031import org.netbeans.lib.cvsclient.command.GlobalOptions;
032import org.netbeans.lib.cvsclient.commandLine.CommandFactory;
033import org.netbeans.lib.cvsclient.commandLine.GetOpt;
034import org.netbeans.lib.cvsclient.connection.AbstractConnection;
035import org.netbeans.lib.cvsclient.connection.AuthenticationException;
036import org.netbeans.lib.cvsclient.connection.Connection;
037import org.netbeans.lib.cvsclient.connection.ConnectionFactory;
038import org.netbeans.lib.cvsclient.connection.PServerConnection;
039import org.netbeans.lib.cvsclient.connection.StandardScrambler;
040import org.netbeans.lib.cvsclient.event.CVSListener;
041
042import java.io.BufferedReader;
043import java.io.File;
044import java.io.FileReader;
045import java.io.IOException;
046
047/**
048 * A Cvs connection that simulates a command line interface.
049 *
050 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
051 *
052 */
053public class CvsConnection
054{
055
056    /**
057     * The path to the repository on the server
058     */
059    @SuppressWarnings( "unused" )
060    private String repository;
061
062    /**
063     * The local path to use to perform operations (the top level)
064     */
065    private String localPath;
066
067    /**
068     * The connection to the server
069     */
070    private Connection connection;
071
072    /**
073     * The client that manages interactions with the server
074     */
075    private Client client;
076
077    /**
078     * The global options being used. GlobalOptions are only global for a
079     * particular command.
080     */
081    private GlobalOptions globalOptions;
082
083    private CvsConnection()
084    {
085    }
086
087    /**
088     * Execute a configured CVS command
089     *
090     * @param command the command to execute
091     * @throws CommandException if there is an error running the command
092     */
093    public boolean executeCommand( Command command )
094        throws CommandException, AuthenticationException
095    {
096        return client.executeCommand( command, globalOptions );
097    }
098
099    public void setRepository( String repository )
100    {
101        this.repository = repository;
102    }
103
104    public void setLocalPath( String localPath )
105    {
106        this.localPath = localPath;
107    }
108
109    public void setGlobalOptions( GlobalOptions globalOptions )
110    {
111        this.globalOptions = globalOptions;
112    }
113
114    /**
115     * Creates the connection and the client and connects.
116     */
117    private void connect( CVSRoot root, String password )
118        throws AuthenticationException, CommandAbortedException
119    {
120        if ( CVSRoot.METHOD_EXT.equals( root.getMethod() ) )
121        {
122            String cvsRsh = System.getProperty( "maven.scm.cvs.java.cvs_rsh" );
123            if ( cvsRsh == null )
124            {
125                try
126                {
127                    cvsRsh = CommandLineUtils.getSystemEnvVars().getProperty( "CVS_RSH" );
128                }
129                catch ( IOException e )
130                {
131                    // we assume searching env var can't fail
132                }
133            }
134
135            if ( cvsRsh != null )
136            {
137                if ( cvsRsh.indexOf( ' ' ) < 0 )
138                {
139                    //cvs_rsh should be 'rsh' or 'ssh'
140                    //Complete the command to use
141                    String username = root.getUserName();
142                    if ( username == null )
143                    {
144                        username = System.getProperty( "user.name" );
145                    }
146
147                    cvsRsh += " " + username + "@" + root.getHostName() + " cvs server";
148                }
149
150                AbstractConnection conn = new org.netbeans.lib.cvsclient.connection.ExtConnection( cvsRsh );
151                conn.setRepository( root.getRepository() );
152                connection = conn;
153            }
154            else
155            {
156                connection = new ExtConnection( root );
157            }
158        }
159        else
160        {
161            connection = ConnectionFactory.getConnection( root );
162            if ( CVSRoot.METHOD_PSERVER.equals( root.getMethod() ) )
163            {
164                ( (PServerConnection) connection ).setEncodedPassword( password );
165            }
166        }
167        connection.open();
168
169        client = new Client( connection, new StandardAdminHandler() );
170        client.setLocalPath( localPath );
171    }
172
173    private void disconnect()
174    {
175        if ( connection != null && connection.isOpen() )
176        {
177            try
178            {
179                connection.close();
180            }
181            catch ( IOException e )
182            {
183                //ignore
184            }
185        }
186    }
187
188    private void addListener( CVSListener listener )
189    {
190        if ( client != null )
191        {
192            // add a listener to the client
193            client.getEventManager().addCVSListener( listener );
194        }
195    }
196
197    /**
198     * Obtain the CVS root, either from the -D option cvs.root or from the CVS
199     * directory
200     *
201     * @return the CVSRoot string
202     */
203    private static String getCVSRoot( String workingDir )
204    {
205        String root = null;
206        BufferedReader r = null;
207        if ( workingDir == null )
208        {
209            workingDir = System.getProperty( "user.dir" );
210        }
211        try
212        {
213            File f = new File( workingDir );
214            File rootFile = new File( f, "CVS/Root" );
215            if ( rootFile.exists() )
216            {
217                r = new BufferedReader( new FileReader( rootFile ) );
218                root = r.readLine();
219            }
220        }
221        catch ( IOException e )
222        {
223            // ignore
224        }
225        finally
226        {
227            try
228            {
229                if ( r != null )
230                {
231                    r.close();
232                }
233            }
234            catch ( IOException e )
235            {
236                System.err.println( "Warning: could not close CVS/Root file!" );
237            }
238        }
239        if ( root == null )
240        {
241            root = System.getProperty( "cvs.root" );
242        }
243        return root;
244    }
245
246    /**
247     * Process global options passed into the application
248     *
249     * @param args          the argument list, complete
250     * @param globalOptions the global options structure that will be passed to
251     *                      the command
252     */
253    private static int processGlobalOptions( String[] args, GlobalOptions globalOptions )
254    {
255        final String getOptString = globalOptions.getOptString();
256        GetOpt go = new GetOpt( args, getOptString );
257        int ch;
258        while ( ( ch = go.getopt() ) != GetOpt.optEOF )
259        {
260            //System.out.println("Global option '"+((char) ch)+"',
261            // '"+go.optArgGet()+"'");
262            String arg = go.optArgGet();
263            boolean success = globalOptions.setCVSCommand( (char) ch, arg );
264            if ( !success )
265            {
266                throw new IllegalArgumentException( "Failed to set CVS Command: -" + ch + " = " + arg );
267            }
268        }
269
270        return go.optIndexGet();
271    }
272
273    /**
274     * Lookup the password in the .cvspass file. This file is looked for in the
275     * user.home directory if the option cvs.passfile is not set
276     *
277     * @param cvsRoot the CVS root for which the password is being searched
278     * @return the password, scrambled
279     */
280    private static String lookupPassword( String cvsRoot, ScmLogger logger )
281    {
282        File passFile = new File( System.getProperty( "cygwin.user.home", System.getProperty( "user.home" ) ) + File
283            .separatorChar + ".cvspass" );
284
285        BufferedReader reader = null;
286        String password = null;
287
288        try
289        {
290            reader = new BufferedReader( new FileReader( passFile ) );
291            password = processCvspass( cvsRoot, reader );
292        }
293        catch ( IOException e )
294        {
295            if ( logger.isWarnEnabled() )
296            {
297                logger.warn( "Could not read password for '" + cvsRoot + "' from '" + passFile + "'", e );
298            }
299            return null;
300        }
301        finally
302        {
303            if ( reader != null )
304            {
305                try
306                {
307                    reader.close();
308                }
309                catch ( IOException e )
310                {
311                    if ( logger.isErrorEnabled() )
312                    {
313                        logger.error( "Warning: could not close password file." );
314                    }
315                }
316            }
317        }
318        if ( password == null )
319        {
320            if ( logger.isErrorEnabled() )
321            {
322                logger.error( "Didn't find password for CVSROOT '" + cvsRoot + "'." );
323            }
324        }
325        return password;
326    }
327
328    /**
329     * Read in a list of return delimited lines from .cvspass and retreive
330     * the password.  Return null if the cvsRoot can't be found.
331     *
332     * @param cvsRoot the CVS root for which the password is being searched
333     * @param reader  A buffered reader of lines of cvspass information
334     * @return The password, or null if it can't be found.
335     * @throws IOException
336     */
337    static String processCvspass( String cvsRoot, BufferedReader reader )
338        throws IOException
339    {
340        String line;
341        String password = null;
342        while ( ( line = reader.readLine() ) != null )
343        {
344            if ( line.startsWith( "/" ) )
345            {
346                String[] cvspass = StringUtils.split( line, " " );
347                String cvspassRoot = cvspass[1];
348                if ( compareCvsRoot( cvsRoot, cvspassRoot ) )
349                {
350                    int index = line.indexOf( cvspassRoot ) + cvspassRoot.length() + 1;
351                    password = line.substring( index );
352                    break;
353                }
354            }
355            else if ( line.startsWith( cvsRoot ) )
356            {
357                password = line.substring( cvsRoot.length() + 1 );
358                break;
359            }
360        }
361        return password;
362    }
363
364    static boolean compareCvsRoot( String cvsRoot, String target )
365    {
366        String s1 = completeCvsRootPort( cvsRoot );
367        String s2 = completeCvsRootPort( target );
368        return s1 != null && s1.equals( s2 );
369
370    }
371
372    private static String completeCvsRootPort( String cvsRoot )
373    {
374        String result = cvsRoot;
375        int idx = cvsRoot.indexOf( ':' );
376        for ( int i = 0; i < 2 && idx != -1; i++ )
377        {
378            idx = cvsRoot.indexOf( ':', idx + 1 );
379        }
380        if ( idx != -1 && cvsRoot.charAt( idx + 1 ) == '/' )
381        {
382            StringBuilder sb = new StringBuilder();
383            sb.append( cvsRoot.substring( 0, idx + 1 ) );
384            sb.append( "2401" );
385            sb.append( cvsRoot.substring( idx + 1 ) );
386            result = sb.toString();
387        }
388        return result;
389
390    }
391
392    /**
393     * Process the CVS command passed in args[] array with all necessary
394     * options. The only difference from main() method is, that this method
395     * does not exit the JVM and provides command output.
396     *
397     * @param args The command with options
398     */
399    public static boolean processCommand( String[] args, String localPath, CVSListener listener, ScmLogger logger )
400        throws Exception
401    {
402        // Set up the CVSRoot. Note that it might still be null after this
403        // call if the user has decided to set it with the -d command line
404        // global option
405        GlobalOptions globalOptions = new GlobalOptions();
406        globalOptions.setCVSRoot( getCVSRoot( localPath ) );
407
408        // Set up any global options specified. These occur before the
409        // name of the command to run
410        int commandIndex;
411
412        try
413        {
414            commandIndex = processGlobalOptions( args, globalOptions );
415        }
416        catch ( IllegalArgumentException e )
417        {
418            if ( logger.isErrorEnabled() )
419            {
420                logger.error( "Invalid argument: " + e );
421            }
422            return false;
423        }
424
425        // if we don't have a CVS root by now, the user has messed up
426        if ( globalOptions.getCVSRoot() == null )
427        {
428            if ( logger.isErrorEnabled() )
429            {
430                logger.error( "No CVS root is set. Check your <repository> information in the POM." );
431            }
432            return false;
433        }
434
435        // parse the CVS root into its constituent parts
436        CVSRoot root;
437        final String cvsRoot = globalOptions.getCVSRoot();
438        try
439        {
440            root = CVSRoot.parse( cvsRoot );
441        }
442        catch ( IllegalArgumentException e )
443        {
444            if ( logger.isErrorEnabled() )
445            {
446                logger.error( "Incorrect format for CVSRoot: " + cvsRoot + "\nThe correct format is: "
447                    + "[:method:][[user][:password]@][hostname:[port]]/path/to/repository"
448                    + "\nwhere \"method\" is pserver." );
449            }
450            return false;
451        }
452
453        final String command = args[commandIndex];
454
455        // this is not login, but a 'real' cvs command, so construct it,
456        // set the options, and then connect to the server and execute it
457
458        Command c;
459        try
460        {
461            c = CommandFactory.getDefault().createCommand( command, args, ++commandIndex, globalOptions, localPath );
462        }
463        catch ( IllegalArgumentException e )
464        {
465            if ( logger.isErrorEnabled() )
466            {
467                logger.error( "Illegal argument: " + e.getMessage() );
468            }
469            return false;
470        }
471
472        String password = null;
473
474        if ( CVSRoot.METHOD_PSERVER.equals( root.getMethod() ) )
475        {
476            password = root.getPassword();
477            if ( password != null )
478            {
479                password = StandardScrambler.getInstance().scramble( password );
480            }
481            else
482            {
483                password = lookupPassword( cvsRoot, logger );
484                if ( password == null )
485                {
486                    password = StandardScrambler.getInstance().scramble( "" );
487                    // an empty password
488                }
489            }
490        }
491        CvsConnection cvsCommand = new CvsConnection();
492        cvsCommand.setGlobalOptions( globalOptions );
493        cvsCommand.setRepository( root.getRepository() );
494        // the local path is just the path where we executed the
495        // command. This is the case for command-line CVS but not
496        // usually for GUI front-ends
497        cvsCommand.setLocalPath( localPath );
498
499        cvsCommand.connect( root, password );
500        cvsCommand.addListener( listener );
501        if ( logger.isDebugEnabled() )
502        {
503            logger.debug( "Executing CVS command: " + c.getCVSCommand() );
504        }
505        boolean result = cvsCommand.executeCommand( c );
506        cvsCommand.disconnect();
507        return result;
508    }
509}