001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001-2003, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ********************************************************************************/package net.sourceforge.cruisecontrol.util;
037:
038: import net.sourceforge.cruisecontrol.CruiseControlException;
039: import org.apache.log4j.Logger;
040:
041: import java.io.File;
042: import java.io.IOException;
043: import java.util.StringTokenizer;
044: import java.util.Vector;
045:
046: /**
047: * Commandline objects help handling command lines specifying processes to execute.
048: *
049: * The class can be used to define a command line as nested elements or as a helper to define a command line by an
050: * application.
051: * <p>
052: * <code>
053: * <someelement><br>
054: * <acommandline executable="/executable/to/run"><br>
055: * <argument value="argument 1" /><br>
056: * <argument line="argument_1 argument_2 argument_3" /><br>
057: * <argument value="argument 4" /><br>
058: * </acommandline><br>
059: * </someelement><br>
060: * </code> The element <code>someelement</code> must provide a method <code>createAcommandline</code> which returns
061: * an instance of this class.
062: *
063: * @author thomas.haas@softwired-inc.com
064: * @author <a href="mailto:stefan.bodewig@epost.de">Stefan Bodewig</a>
065: */
066: public class Commandline implements Cloneable {
067:
068: private static final Logger LOG = Logger
069: .getLogger(Commandline.class);
070:
071: private Vector arguments = new Vector();
072:
073: private String executable = null;
074:
075: private File workingDir = null;
076: private final CruiseRuntime runtime;
077:
078: public Commandline(String toProcess, CruiseRuntime cruiseRuntime) {
079: super ();
080: this .runtime = cruiseRuntime;
081: if (toProcess != null) {
082: String[] tmp = new String[0];
083: try {
084: tmp = translateCommandline(toProcess);
085: } catch (CruiseControlException e) {
086: LOG.error("Error translating Commandline.", e);
087: }
088: if (tmp != null && tmp.length > 0) {
089: setExecutable(tmp[0]);
090: for (int i = 1; i < tmp.length; i++) {
091: createArgument().setValue(tmp[i]);
092: }
093: }
094: }
095: }
096:
097: private boolean safeQuoting = true;
098:
099: public Commandline(String toProcess) {
100: this (toProcess, new CruiseRuntime());
101: }
102:
103: public Commandline() {
104: this (null);
105: }
106:
107: protected File getWorkingDir() {
108: return workingDir;
109: }
110:
111: /**
112: * Used for nested xml command line definitions.
113: */
114: public static class Argument {
115: private String[] parts;
116:
117: /**
118: * Sets a single commandline argument.
119: *
120: * @param value
121: * a single commandline argument.
122: */
123: public void setValue(String value) {
124: parts = new String[] { value };
125: }
126:
127: /**
128: * Line to split into several commandline arguments.
129: *
130: * @param line
131: * line to split into several commandline arguments
132: */
133: public void setLine(String line) {
134: if (line == null) {
135: return;
136: }
137: try {
138: parts = translateCommandline(line);
139: } catch (CruiseControlException e) {
140: LOG.error("Error translating Commandline.", e);
141: }
142: }
143:
144: /**
145: * Sets a single commandline argument to the absolute filename of the given file.
146: *
147: * @param value
148: * a single commandline argument.
149: */
150: public void setFile(File value) {
151: parts = new String[] { value.getAbsolutePath() };
152: }
153:
154: /**
155: * Returns the parts this Argument consists of.
156: */
157: public String[] getParts() {
158: return parts;
159: }
160:
161: }
162:
163: /**
164: * Class to keep track of the position of an Argument.
165: */
166: // <p>This class is there to support the srcfile and targetfile
167: // elements of <execon> and <transform> - don't know
168: // whether there might be additional use cases.</p> --SB
169: public class Marker {
170: private int position;
171:
172: private int realPos = -1;
173:
174: Marker(int position) {
175: this .position = position;
176: }
177:
178: /**
179: * Return the number of arguments that preceeded this marker.
180: *
181: * <p>
182: * The name of the executable - if set - is counted as the very first argument.
183: * </p>
184: */
185: public int getPosition() {
186: if (realPos == -1) {
187: realPos = (executable == null ? 0 : 1);
188: for (int i = 0; i < position; i++) {
189: Argument arg = (Argument) arguments.elementAt(i);
190: realPos += arg.getParts().length;
191: }
192: }
193: return realPos;
194: }
195:
196: }
197:
198: /**
199: * Creates an argument object.
200: *
201: * <p>
202: * Each commandline object has at most one instance of the argument class. This method calls
203: * <code>this.createArgument(false)</code>.
204: * </p>
205: *
206: * @see #createArgument(boolean)
207: * @return the argument object.
208: */
209: public Argument createArgument() {
210: return this .createArgument(false);
211: }
212:
213: /**
214: * Creates an argument object and adds it to our list of args.
215: *
216: * <p>
217: * Each commandline object has at most one instance of the argument class.
218: * </p>
219: *
220: * @param insertAtStart
221: * if true, the argument is inserted at the beginning of the list of args, otherwise it is appended.
222: */
223: public Argument createArgument(boolean insertAtStart) {
224: Argument argument = new Argument();
225: if (insertAtStart) {
226: arguments.insertElementAt(argument, 0);
227: } else {
228: arguments.addElement(argument);
229: }
230: return argument;
231: }
232:
233: /**
234: * Same as calling createArgument().setValue(value), but much more convenient.
235: */
236: public Argument createArgument(String value) {
237: Argument arg = this .createArgument();
238: arg.setValue(value);
239: return arg;
240: }
241:
242: /**
243: * Same as calling createArgument twice in a row, but can be used to make more obvious a relationship between to
244: * command line arguments, like "-folder c:\myfolder".
245: */
246: public void createArguments(String first, String second) {
247: createArgument(first);
248: createArgument(second);
249: }
250:
251: /**
252: * Sets the executable to run.
253: */
254: public void setExecutable(String executable) {
255: if (executable == null || executable.length() == 0) {
256: return;
257: }
258: this .executable = executable.replace('/', File.separatorChar)
259: .replace('\\', File.separatorChar);
260: }
261:
262: public String getExecutable() {
263: return executable;
264: }
265:
266: public void addArguments(String[] line) {
267: for (int i = 0; i < line.length; i++) {
268: createArgument().setValue(line[i]);
269: }
270: }
271:
272: /**
273: * Returns the executable and all defined arguments.
274: */
275: public String[] getCommandline() {
276: final String[] args = getArguments();
277: if (executable == null) {
278: return args;
279: }
280: final String[] result = new String[args.length + 1];
281: result[0] = executable;
282: System.arraycopy(args, 0, result, 1, args.length);
283: return result;
284: }
285:
286: /**
287: * Returns all arguments defined by <code>addLine</code>, <code>addValue</code> or the argument object.
288: */
289: public String[] getArguments() {
290: Vector result = new Vector(arguments.size() * 2);
291: for (int i = 0; i < arguments.size(); i++) {
292: Argument arg = (Argument) arguments.elementAt(i);
293: String[] s = arg.getParts();
294: if (s != null) {
295: for (int j = 0; j < s.length; j++) {
296: result.addElement(s[j]);
297: }
298: }
299: }
300:
301: String[] res = new String[result.size()];
302: result.copyInto(res);
303: return res;
304: }
305:
306: public String toString() {
307: return toString(getCommandline(), true);
308: }
309:
310: /**
311: * Converts the command line to a string without adding quotes to any of the arguments.
312: */
313: public String toStringNoQuoting() {
314: return toString(getCommandline(), false);
315: }
316:
317: /**
318: * Put quotes around the given String if necessary.
319: *
320: * <p>
321: * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
322: * quotes - else surround the argument by double quotes.
323: * </p>
324: *
325: * @exception CruiseControlException
326: * if the argument contains both, single and double quotes.
327: */
328: public static String quoteArgument(String argument)
329: throws CruiseControlException {
330: if (argument.indexOf("\"") > -1) {
331: if (argument.indexOf("\'") > -1) {
332: throw new CruiseControlException(
333: "Can't handle single and double quotes in same argument");
334: } else {
335: return '\'' + argument + '\'';
336: }
337: } else if (argument.indexOf("\'") > -1
338: || argument.indexOf(" ") > -1) {
339: return '\"' + argument + '\"';
340: } else {
341: return argument;
342: }
343: }
344:
345: public static String toString(String[] line, boolean quote) {
346: return toString(line, quote, " ");
347: }
348:
349: public static String toString(String[] line, boolean quote,
350: String separator) {
351: // empty path return empty string
352: if (line == null || line.length == 0) {
353: return "";
354: }
355:
356: // path containing one or more elements
357: final StringBuffer result = new StringBuffer();
358: for (int i = 0; i < line.length; i++) {
359: if (i > 0) {
360: result.append(separator);
361: }
362: if (quote) {
363: try {
364: result.append(quoteArgument(line[i]));
365: } catch (CruiseControlException e) {
366: LOG.error("Error quoting argument.", e);
367: }
368: } else {
369: result.append(line[i]);
370: }
371: }
372: return result.toString();
373: }
374:
375: public static String[] translateCommandline(String toProcess)
376: throws CruiseControlException {
377: if (toProcess == null || toProcess.length() == 0) {
378: return new String[0];
379: }
380:
381: // parse with a simple finite state machine
382:
383: final int normal = 0;
384: final int inQuote = 1;
385: final int inDoubleQuote = 2;
386: int state = normal;
387: StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ",
388: true);
389: Vector v = new Vector();
390: StringBuffer current = new StringBuffer();
391:
392: while (tok.hasMoreTokens()) {
393: String nextTok = tok.nextToken();
394: switch (state) {
395: case inQuote:
396: if ("\'".equals(nextTok)) {
397: state = normal;
398: } else {
399: current.append(nextTok);
400: }
401: break;
402: case inDoubleQuote:
403: if ("\"".equals(nextTok)) {
404: state = normal;
405: } else {
406: current.append(nextTok);
407: }
408: break;
409: default:
410: if ("\'".equals(nextTok)) {
411: state = inQuote;
412: } else if ("\"".equals(nextTok)) {
413: state = inDoubleQuote;
414: } else if (" ".equals(nextTok)) {
415: if (current.length() != 0) {
416: v.addElement(current.toString());
417: current.setLength(0);
418: }
419: } else {
420: current.append(nextTok);
421: }
422: break;
423: }
424: }
425:
426: if (current.length() != 0) {
427: v.addElement(current.toString());
428: }
429:
430: if (state == inQuote || state == inDoubleQuote) {
431: throw new CruiseControlException("unbalanced quotes in "
432: + toProcess);
433: }
434:
435: String[] args = new String[v.size()];
436: v.copyInto(args);
437: return args;
438: }
439:
440: public int size() {
441: return getCommandline().length;
442: }
443:
444: public Object clone() throws CloneNotSupportedException {
445: super .clone();
446:
447: Commandline c = new Commandline();
448: c.setExecutable(executable);
449: c.addArguments(getArguments());
450: c.useSafeQuoting(safeQuoting);
451:
452: return c;
453: }
454:
455: /**
456: * Clear out the whole command line.
457: */
458: public void clear() {
459: executable = null;
460: arguments.removeAllElements();
461: }
462:
463: /**
464: * Clear out the arguments but leave the executable in place for another operation.
465: */
466: public void clearArgs() {
467: arguments.removeAllElements();
468: }
469:
470: /**
471: * Return a marker.
472: *
473: * <p>
474: * This marker can be used to locate a position on the commandline - to insert something for example - when all
475: * parameters have been set.
476: * </p>
477: */
478: public Marker createMarker() {
479: return new Marker(arguments.size());
480: }
481:
482: /**
483: * Sets execution directory.
484: */
485: public void setWorkingDirectory(String path)
486: throws CruiseControlException {
487: if (path != null) {
488: File dir = new File(path);
489: checkWorkingDir(dir);
490: workingDir = dir;
491: } else {
492: workingDir = null;
493: }
494: }
495:
496: /**
497: * Enables and disables safe quoting when executing a command. When enabled: Quotes any arguments that need it when
498: * executing command. This should handle filenames with spaces, but may fall over if the arguments already have
499: * quoting within them. When disables: Arguments are passed as is.
500: */
501: public void useSafeQuoting(boolean safe) {
502: safeQuoting = safe;
503: }
504:
505: /**
506: * Sets execution directory
507: */
508: public void setWorkingDir(File workingDir)
509: throws CruiseControlException {
510: checkWorkingDir(workingDir);
511: this .workingDir = workingDir;
512: }
513:
514: // throws an exception if the specified working directory is non null
515: // and not a valid working directory
516: private void checkWorkingDir(File dir)
517: throws CruiseControlException {
518: if (dir != null) {
519: if (!dir.exists()) {
520: throw new CruiseControlException("Working directory \""
521: + dir.getAbsolutePath() + "\" does not exist!");
522: } else if (!dir.isDirectory()) {
523: throw new CruiseControlException("Path \""
524: + dir.getAbsolutePath()
525: + "\" does not specify a " + "directory.");
526: }
527: }
528: }
529:
530: public File getWorkingDirectory() {
531: return workingDir;
532: }
533:
534: /**
535: * Executes the command.
536: */
537: public Process execute() throws IOException {
538: final Process process;
539:
540: final String msgCommandInfo = "Executing: [" + getExecutable()
541: + "] with parameters: ["
542: + toString(getCommandline(), false, "], [") + "]";
543:
544: if (workingDir == null) {
545: LOG.debug(msgCommandInfo);
546: if (safeQuoting) {
547: process = runtime.exec(getCommandline());
548: } else {
549: process = runtime.exec(toStringNoQuoting());
550: }
551:
552: } else {
553: LOG.debug(msgCommandInfo + " in directory "
554: + workingDir.getAbsolutePath());
555: if (safeQuoting) {
556: process = runtime.exec(getCommandline(), null,
557: workingDir);
558: } else {
559: process = runtime.exec(toStringNoQuoting(), null,
560: workingDir);
561: }
562: }
563:
564: process.getOutputStream().close();
565:
566: return process;
567: }
568:
569: /**
570: * Executes the command and wait for it to finish.
571: *
572: * @param log
573: * where the output and error streams are logged
574: */
575: public void executeAndWait(Logger log)
576: throws CruiseControlException {
577: new CommandExecutor(this, log).executeAndWait();
578: }
579: }
|