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 = (Repository) Class.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(DEBUG) Out.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-- < 0) throw 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(DEBUG) Out.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 == 1 && 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
|