AbstractRepository.java
001 /*
002  *  AbstractRepository.java
003  *  Copyright (c) 1998-2008, The University of Sheffield.
004  *
005  *  This code is from the GATE project (http://gate.ac.uk/) and is free
006  *  software licenced under the GNU General Public License version 3. It is
007  *  distributed without any warranty. For more details see COPYING.txt in the
008  *  top level directory (or at http://gatewiki.sf.net/COPYING.txt).
009  *  
010  *  Hamish Cunningham 7/Sep/2005
011  */
012 
013 package gate.versioning.cmdline;
014  
015 import java.io.*;
016 import java.util.*;
017 import gate.util.*; 
018 
019 /**
020  * Abstract repository implementation.
021  * The public API of this class is documented on the
022  {@link Repository} interface.
023  * Implementors control the behaviour of the methods here by overriding
024  * parameterisation methods like {@link #getCommandName}, or simply by
025  * providing their own replacement implementations of the {@link Repository}
026  * methods.
027  @see gate.versioning.cmdline.Repository
028  @see gate.versioning.cmdline.CvsRepository
029  @see gate.versioning.cmdline.SvnRepository
030  */
031 public abstract class AbstractRepository implements Repository {
032 
033   /** The root or URL of the repository. */
034   protected String root;
035 
036   /** The current working directory. */
037   protected File workingDir;
038 
039   /** Debugging messages. */
040   protected static boolean DEBUG = true;
041 
042   /** Output from repository command execution. */
043   protected String commandOutput = "command not run";
044 
045   /** Stdout from repository command execution. */
046   protected String stdoutOutput = "command not run";
047 
048   /** Stderr from repository command execution. */
049   protected String stderrOutput = "command not run";
050 
051   /** Shorthand for newlines. */
052   protected String nl = Strings.getNl();
053 
054   /** Construction. */
055   public AbstractRepository() { }
056 
057   /** Name of the repository command to execute. */
058   abstract public String getCommandName();
059 
060   /**
061    * Validate parameters.
062    */
063   public void init() throws GateException {
064     try {
065       workingDir = new File(workingDir.getCanonicalPath());
066     catch(IOException e) {
067       throw new GateException(
068         "couldn't get canonical path from workingDir " +
069         nl + workingDir + nl + "exception was: " + nl + e
070       );
071     }
072 
073     if(! workingDir.exists())
074       throw new GateException(
075         "workingDir " + nl + workingDir + nl + "does not exist"
076       );
077     if(! workingDir.isDirectory())
078       throw new GateException(
079         "workingDir " + nl + workingDir + nl + "is not a directory"
080       );
081 
082     if(root == null)
083       throw new GateException("root cannot be null");
084   // init()
085 
086   /** The root / URL of the repository */
087   public void setRoot(String root) {
088     this.root = root;
089   // setRoot()
090 
091   /** The root / URL of the repository */
092   public String getRoot() {
093     return root;
094   // getRoot()
095 
096   /** The working directory for repository actions */
097   public void setWorkingDir(File workingDir) {
098     this.workingDir = workingDir;
099   // setWorkingDir()
100 
101   /**
102    * Check out a file, directory or module.
103    @param fileName the file or directory to work on (should be relative
104    * to the repository's working directory, and use "/" as a path separator).
105    @return boolean representing success or failure.
106    */
107   public boolean checkout(String fileName) {
108     if(DEBUG) { 
109       Out.prln("root: " + root);
110       Out.prln("workingDir: " + workingDir);
111     }
112 
113     String[] com = buildCommandArray(fileName, "co"null);
114     return runCommand(com);
115   // checkout
116 
117   /** Specifies what (if anything) should precede the subcommand. */
118   abstract protected List getPreCommand();
119 
120   /** Specifies what (if anything) should follow the subcommand. */
121   abstract protected List getPostCommand(String fileName, boolean noRoot);
122 
123   /**
124    * Calls {@link #buildCommandArray(java.lang.String, java.lang.String,
125    * java.lang.String, boolean) buildCommandArray/4}
126    * with the no root parameter set false.
127    */
128   protected String[] buildCommandArray(
129     String fileName, String subCommand, String subCommandFlag
130   ) {
131     return buildCommandArray(fileName, subCommand, subCommandFlag, false);
132   // buildCommandArray
133 
134   /**
135    * Build an array to pass to runtime.exec. Behaviour modified by 
136    {@link #getCommandName}{@link #getPreCommand} and {@link #getPostCommand}.
137    @param fileName the file or directory to work on (should be relative
138    * to the repository's working directory, and use "/" as a path separator).
139    @param subCommand the repository command, e.g. "co".
140    @param subCommandFlag a flag for the command (e.g. "-d"), or null.
141    @param noRoot when true indicates that the root isn't necessary for this
142    * command.
143    */
144   protected String[] buildCommandArray(
145     String fileName, String subCommand, String subCommandFlag, boolean noRoot
146   ) {
147     List com = new ArrayList();
148 
149     com.add(getCommandName());
150     com.addAll(getPreCommand())// rootFlag + root where appropriate
151     com.add(subCommand);
152     if(subCommandFlag != null)
153       com.add(subCommandFlag);
154     com.addAll(getPostCommand(fileName, noRoot))// root if needed, fileName
155 
156     String[] result = (String[]) com.toArray(new String[com.size()]);
157     return result;
158   // buildCommandArray
159 
160   /**
161    * Commit changes. A default message is used.
162    @param fileName the file or directory to work on (should be relative
163    * to the repository's working directory, and use "/" as a path separator).
164    @return boolean representing success or failure.
165    */
166   public boolean checkin(String fileName) {
167     return checkin(fileName, "checkin");
168   // checkin()
169 
170   /**
171    * Commit changes.
172    @param fileName the file or directory to work on (should be relative
173    * to the repository's working directory, and use "/" as a path separator).
174    @param message a commit message.
175    @return boolean representing success or failure.
176    */
177   public boolean checkin(String fileName, String message) {
178     String[] com = buildCommandArray(fileName, "ci""-m" + message);
179     boolean result = runCommand(com);
180     return result;
181   // checkin()
182 
183   /**
184    * Update.
185    @param fileName the file or directory to work on (should be relative
186    * to the repository's working directory, and use "/" as a path separator).
187    @return boolean representing success or failure.
188    */
189   public boolean update(String fileName) {
190     String[] com = buildCommandArray(fileName, "update""-d");
191     return runCommand(com);
192   // update()
193 
194   /**
195    * Status.
196    @param fileName the file or directory to work on (should be relative
197    * to the repository's working directory, and use "/" as a path separator).
198    @return String giving status ouput.
199    */
200   public String status(String fileName) {
201     String[] com = buildCommandArray(fileName, "status"null);
202     if(!runCommand(com)){
203       throw new RuntimeException("Problem while running status command:\n" +
204               getCommandStderr());
205     }
206     return getCommandStdout();
207   // status()
208 
209   /**
210    * Delete from the repository.
211    @param fileName the file or directory to work on (should be relative
212    * to the repository's working directory, and use "/" as a path separator).
213    @return boolean representing success or failure.
214    */
215   public boolean delete(String fileName) {
216     // remove the file, if it exists
217     File fileToDelete = new File(workingDir, fileName);
218     if(fileToDelete.exists()) fileToDelete.delete();
219 
220     String[] com = buildCommandArray(fileName, "delete"null);
221     return runCommand(com);
222   // delete()
223 
224   /**
225    * Add to the repository.
226    @param fileName the file or directory to work on (should be relative
227    * to the repository's working directory, and use "/" as a path separator).
228    @return boolean representing success or failure.
229    */
230   public boolean add(String fileName) {
231     String[] com = buildCommandArray(fileName, "add"null);
232     return runCommand(com);
233   // add()
234 
235   /**
236    * Get the difference with the repository version.
237    @param fileName the file or directory to work on (should be relative
238    * to the repository's working directory, and use "/" as a path separator).
239    @return a string containing the difference, or "" for no difference, or
240    * null for error.
241    */
242   public String diff(String fileName) {
243     String[] com = buildCommandArray(fileName, "diff"null);
244     if(! runCommand(com, true))
245       return null;
246     return getCommandStdout();
247   // diff()
248 
249   /** Get a string containing the stdout from the command execution. */
250   public String getCommandStdout() {
251     return stdoutOutput;
252   // getCommandStdout()
253 
254   /** Get a string containing the stderr from the command execution. */
255   public String getCommandStderr() {
256     return stderrOutput;
257   // getCommandStderr()
258 
259   /**
260    * Get a string containing the stdout and stderr from the command
261    * execution.
262    */
263   public String getCommandOutput() { return commandOutput; }
264   
265   /** 
266    * Return a class that supports the given root specifier. The type
267    * of the class is a guess based on characteristics of the string
268    * (e.g. URLs are for SVN). 
269    *
270    @param root root specifier for the desired Repository implementor.
271    @return a non-initialised Repository object
272    */
273   public static Repository getRepository(String root) {
274     String lcRoot = root.toLowerCase();
275     String repType;
276     if(
277       lcRoot.startsWith("file:/"||
278       lcRoot.startsWith("http:/"||
279       lcRoot.startsWith("svn")
280     )
281       repType = "gate.versioning.cmdline.SvnRepository";
282     else
283       repType = "gate.versioning.cmdline.CvsRepository";
284 
285     Repository rep;
286     try {
287       rep = (RepositoryClass.forName(repType).newInstance();
288       rep.setRoot(root);
289     catch(Exception e) { // should really never get here...
290       throw new RuntimeException("couldn't create Repository: " + e);
291     }
292     
293     return rep;
294   // getRepository()
295   
296   /** Run a command, wait for termination and report status */
297   protected boolean runCommand(String[] command) {
298     return runCommand(command, false);
299   // runCommand()
300 
301   /**
302    * Run a command, wait for termination and report status.
303    @param allowExitOne accept a command return value of 1 as success (e.g.
304    * for cvs diff).
305    */
306   protected boolean runCommand(String[] command, boolean allowExitOne) {
307     // environment and process object
308     String[] env = null;
309     Process proc;
310 
311     // run the process
312     StreamGobbler errorGobbler;
313     StreamGobbler outputGobbler;
314     try {
315       if(DEBUG) {
316         for(int i=0; i<command.length; i++Out.pr(command[i" ");
317         Out.prln();
318       }
319       proc = Runtime.getRuntime().exec(command, env, workingDir);
320 
321       // gobble stdout and stderr in separate threads
322       errorGobbler = new StreamGobbler(proc.getErrorStream());
323       outputGobbler = new StreamGobbler(proc.getInputStream());
324       errorGobbler.start();
325       outputGobbler.start();
326       
327     catch(IOException e) {
328       if(DEBUGOut.prln("IOex on exec: " + e);
329       return false;
330     }
331 
332     // wait for the command; deal with command output
333     try {
334       proc.waitFor();
335       
336       //wait maximum 2 seconds for the err/out gobblers to do their job
337       int roundsLeft = 20;
338       while(errorGobbler.isAlive() || outputGobbler.isAlive()){
339         if(roundsLeft-- < 0throw new RuntimeException(
340                 "Could not capture command output in a timely fashion!");
341         try{
342           Thread.sleep(100);
343         }catch(InterruptedException ie){
344           //ignore
345         }
346       }
347     catch(InterruptedException e) {
348       if(DEBUGOut.prln("InterEx on waitFor: " + e);
349       return false;
350     finally {
351       stderrOutput = errorGobbler.getGobbled();
352       stdoutOutput = outputGobbler.getGobbled();
353       commandOutput =
354         "stderr: " + nl + stderrOutput + "stdout: " + stdoutOutput;
355       if(DEBUG
356         Out.prln(commandOutput);
357     }
358 
359     // success?
360     int status = proc.exitValue();
361     if(status == 0
362       return true;
363     else if(status == && allowExitOne)
364       return true;
365     else
366       return false;
367   // runCommand()
368 
369   /** This class is used to consume streams in new threads without blocking. */
370   class StreamGobbler extends Thread
371   {
372     InputStream is;
373     StringWriter gobbled = new StringWriter();
374     PrintWriter gobbledPrinter = new PrintWriter(gobbled);
375     
376     public StreamGobbler(InputStream is) { this.is = is; }
377     
378     public void run() {
379       try {
380         InputStreamReader isr = new InputStreamReader(is);
381         BufferedReader br = new BufferedReader(isr);
382         String line=null;
383         while ( (line = br.readLine()) != null gobbledPrinter.println(line);
384       catch (IOException e) {
385         gobbledPrinter.println("IOException: " + nl + e);
386       }
387     // run()
388 
389     public String getGobbled() { return gobbled.toString()}
390   // StreamGobbler
391 
392 // AbstractRepository