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 *
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 }