001    package 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    
022    import org.apache.maven.scm.log.ScmLogger;
023    import org.codehaus.plexus.util.StringUtils;
024    import org.codehaus.plexus.util.cli.CommandLineUtils;
025    import org.netbeans.lib.cvsclient.CVSRoot;
026    import org.netbeans.lib.cvsclient.Client;
027    import org.netbeans.lib.cvsclient.admin.StandardAdminHandler;
028    import org.netbeans.lib.cvsclient.command.Command;
029    import org.netbeans.lib.cvsclient.command.CommandAbortedException;
030    import org.netbeans.lib.cvsclient.command.CommandException;
031    import org.netbeans.lib.cvsclient.command.GlobalOptions;
032    import org.netbeans.lib.cvsclient.commandLine.CommandFactory;
033    import org.netbeans.lib.cvsclient.commandLine.GetOpt;
034    import org.netbeans.lib.cvsclient.connection.AbstractConnection;
035    import org.netbeans.lib.cvsclient.connection.AuthenticationException;
036    import org.netbeans.lib.cvsclient.connection.Connection;
037    import org.netbeans.lib.cvsclient.connection.ConnectionFactory;
038    import org.netbeans.lib.cvsclient.connection.PServerConnection;
039    import org.netbeans.lib.cvsclient.connection.StandardScrambler;
040    import org.netbeans.lib.cvsclient.event.CVSListener;
041    
042    import java.io.BufferedReader;
043    import java.io.File;
044    import java.io.FileReader;
045    import 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     * @version $Id: CvsConnection.java 1306867 2012-03-29 13:45:10Z olamy $
052     */
053    public 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    }