View Javadoc

1   package org.codehaus.mojo.exec;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.OutputStream;
26  import java.io.PrintStream;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Properties;
36  import java.util.jar.JarEntry;
37  import java.util.jar.JarOutputStream;
38  import java.util.jar.Manifest;
39  
40  import org.apache.commons.exec.CommandLine;
41  import org.apache.commons.exec.DefaultExecutor;
42  import org.apache.commons.exec.ExecuteException;
43  import org.apache.commons.exec.Executor;
44  import org.apache.commons.exec.OS;
45  import org.apache.commons.exec.PumpStreamHandler;
46  import org.apache.maven.artifact.Artifact;
47  import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
48  import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
49  import org.apache.maven.execution.MavenSession;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.plugin.logging.Log;
52  import org.apache.maven.project.MavenProject;
53  import org.apache.maven.toolchain.Toolchain;
54  import org.apache.maven.toolchain.ToolchainManager;
55  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
56  import org.codehaus.plexus.util.StringUtils;
57  import org.codehaus.plexus.util.cli.CommandLineUtils;
58  
59  /**
60   * A Plugin for executing external programs.
61   * 
62   * @author Jerome Lacoste <jerome@coffeebreaks.org>
63   * @version $Id: ExecMojo.java 12386 2010-07-16 22:10:38Z rfscholte $
64   * @goal exec
65   * @requiresDependencyResolution test
66   * @since 1.0
67   */
68  public class ExecMojo extends AbstractExecMojo {
69  	/**
70  	 * The executable. Can be a full path or a the name executable. In the latter case, the executable must be in the
71  	 * PATH for the execution to work.
72  	 * 
73  	 * @parameter expression="${exec.executable}"
74  	 * @required
75  	 * @since 1.0
76  	 */
77  	private String executable;
78  
79  	/**
80  	 * The current working directory. Optional. If not specified, basedir will be used.
81  	 * 
82  	 * @parameter expression="${exec.workingdir}
83  	 * @since 1.0
84  	 */
85  	private File workingDirectory;
86  
87  	/**
88  	 * Program standard and error output will be redirected to the file specified by this optional field. If not
89  	 * specified the standard maven logging is used.
90  	 * 
91  	 * @parameter expression="${exec.outputFile}"
92  	 * @since 1.1-beta-2
93  	 */
94  	private File outputFile;
95  
96  	/**
97  	 * Can be of type <code>&lt;argument&gt;</code> or <code>&lt;classpath&gt;</code> Can be overriden using "exec.args"
98  	 * env. variable
99  	 * 
100 	 * @parameter
101 	 * @since 1.0
102 	 */
103 	private List arguments;
104 
105 	/**
106 	 * @parameter expression="${basedir}"
107 	 * @required
108 	 * @readonly
109 	 * @since 1.0
110 	 */
111 	private File basedir;
112 
113 	/**
114 	 * Environment variables to pass to the executed program.
115 	 * 
116 	 * @parameter
117 	 * @since 1.1-beta-2
118 	 */
119 	private Map environmentVariables = new HashMap();
120 
121 	/**
122 	 * The current build session instance. This is used for toolchain manager API calls.
123 	 * 
124 	 * @parameter expression="${session}"
125 	 * @required
126 	 * @readonly
127 	 */
128 	private MavenSession session;
129 
130 	/**
131 	 * Exit codes to be resolved as successful execution for non-compliant applications (applications not returning 0
132 	 * for success).
133 	 * 
134 	 * @parameter
135 	 * @since 1.1.1
136 	 */
137 	private List successCodes;
138 
139 	/**
140 	 * If set to true the classpath and the main class will be written to a MANIFEST.MF file and wrapped into a jar.
141 	 * Instead of '-classpath/-cp CLASSPATH mainClass' the exec plugin executes '-jar maven-exec.jar'.
142 	 * 
143 	 * @parameter expression="${exec.longClasspath}" default-value="false"
144 	 * @since 1.1.2
145 	 */
146 	private boolean longClasspath;
147 
148 	public static final String CLASSPATH_TOKEN = "%classpath";
149 
150 	/**
151 	 * priority in the execute method will be to use System properties arguments over the pom specification.
152 	 * 
153 	 * @throws MojoExecutionException
154 	 *             if a failure happens
155 	 */
156 	public void execute() throws MojoExecutionException {
157 		try {
158 			if (isSkip()) {
159 				getLog().info("skipping execute as per configuraion");
160 				return;
161 			}
162 
163 			if (basedir == null) {
164 				throw new IllegalStateException("basedir is null. Should not be possible.");
165 			}
166 
167 			String argsProp = getSystemProperty("exec.args");
168 
169 			List commandArguments = new ArrayList();
170 
171 			if (hasCommandlineArgs()) {
172 				String[] args = parseCommandlineArgs();
173 				for (int i = 0; i < args.length; i++) {
174 					if (isLongClassPathArgument(args[i])) {
175 						// it is assumed that starting from -cp or -classpath the arguments
176 						// are: -classpath/-cp %classpath mainClass
177 						// the arguments are replaced with: -jar $TMP/maven-exec.jar
178 						// NOTE: the jar will contain the classpath and the main class
179 						commandArguments.add("-jar");
180 						File tmpFile = createJar(computeClasspath(null), args[i + 2]);
181 						commandArguments.add(tmpFile.getAbsolutePath());
182 						i += 2;
183 					} else if (CLASSPATH_TOKEN.equals(args[i])) {
184 						commandArguments.add(computeClasspathString(null));
185 					} else {
186 						commandArguments.add(args[i]);
187 					}
188 				}
189 			} else if (!isEmpty(argsProp)) {
190 				getLog().debug("got arguments from system properties: " + argsProp);
191 
192 				try {
193 					String[] args = CommandLineUtils.translateCommandline(argsProp);
194 					commandArguments.addAll(Arrays.asList(args));
195 				} catch (Exception e) {
196 					throw new MojoExecutionException("Couldn't parse systemproperty 'exec.args'");
197 				}
198 			} else {
199 				if (arguments != null) {
200 					for (int i = 0; i < arguments.size(); i++) {
201 						Object argument = arguments.get(i);
202 						String arg;
203 						if (argument == null) {
204 							throw new MojoExecutionException("Misconfigured argument, value is null. "
205 									+ "Set the argument to an empty value if this is the required behaviour.");
206 						} else if (argument instanceof String && isLongClassPathArgument((String) argument)) {
207 							// it is assumed that starting from -cp or -classpath the arguments
208 							// are: -classpath/-cp %classpath mainClass
209 							// the arguments are replaced with: -jar $TMP/maven-exec.jar
210 							// NOTE: the jar will contain the classpath and the main class
211 							commandArguments.add("-jar");
212 							File tmpFile = createJar(computeClasspath((Classpath) arguments.get(i + 1)),
213 									(String) arguments.get(i + 2));
214 							commandArguments.add(tmpFile.getAbsolutePath());
215 							i += 2;
216 						} else if (argument instanceof Classpath) {
217 							Classpath specifiedClasspath = (Classpath) argument;
218 
219 							arg = computeClasspathString(specifiedClasspath);
220 							commandArguments.add(arg);
221 						} else {
222 							arg = argument.toString();
223 							commandArguments.add(arg);
224 						}
225 					}
226 				}
227 			}
228 
229 			Map enviro = new HashMap();
230 			try {
231 				Properties systemEnvVars = CommandLineUtils.getSystemEnvVars();
232 				enviro.putAll(systemEnvVars);
233 			} catch (IOException x) {
234 				getLog().error("Could not assign default system enviroment variables.", x);
235 			}
236 
237 			if (environmentVariables != null) {
238 				Iterator iter = environmentVariables.keySet().iterator();
239 				while (iter.hasNext()) {
240 					String key = (String) iter.next();
241 					String value = (String) environmentVariables.get(key);
242 					enviro.put(key, value);
243 				}
244 			}
245 
246 			if (workingDirectory == null) {
247 				workingDirectory = basedir;
248 			}
249 
250 			if (!workingDirectory.exists()) {
251 				getLog().debug("Making working directory '" + workingDirectory.getAbsolutePath() + "'.");
252 				if (!workingDirectory.mkdirs()) {
253 					throw new MojoExecutionException("Could not make working directory: '"
254 							+ workingDirectory.getAbsolutePath() + "'");
255 				}
256 			}
257 
258 			CommandLine commandLine = getExecutablePath(enviro, workingDirectory);
259 
260 			String[] args = new String[commandArguments.size()];
261 			for (int i = 0; i < commandArguments.size(); i++) {
262 				args[i] = (String) commandArguments.get(i);
263 			}
264 
265 			commandLine.addArguments(args, false);
266 
267 			Executor exec = getExecutor();
268 
269 			exec.setWorkingDirectory(workingDirectory);
270 
271 			// this code ensures the output gets logged vai maven logging, but at the same time prevents
272 			// partial line output, like input prompts.
273 			// final Log outputLog = getExecOutputLog();
274 			// LogOutputStream stdout = new LogOutputStream()
275 			// {
276 			// protected void processLine( String line, int level )
277 			// {
278 			// outputLog.info( line );
279 			// }
280 			// };
281 			//
282 			// LogOutputStream stderr = new LogOutputStream()
283 			// {
284 			// protected void processLine( String line, int level )
285 			// {
286 			// outputLog.info( line );
287 			// }
288 			// };
289 			OutputStream stdout = System.out;
290 			OutputStream stderr = System.err;
291 
292 			try {
293 				getLog().debug("Executing command line: " + commandLine);
294 
295 				int resultCode = executeCommandLine(exec, commandLine, enviro, stdout, stderr);
296 
297 				if (isResultCodeAFailure(resultCode)) {
298 					throw new MojoExecutionException("Result of " + commandLine + " execution is: '" + resultCode
299 							+ "'.");
300 				}
301 			} catch (ExecuteException e) {
302 				throw new MojoExecutionException("Command execution failed.", e);
303 
304 			} catch (IOException e) {
305 				throw new MojoExecutionException("Command execution failed.", e);
306 			}
307 
308 			registerSourceRoots();
309 		} catch (IOException e) {
310 			throw new MojoExecutionException("I/O Error", e);
311 		}
312 	}
313 
314 	protected Executor getExecutor() {
315 		DefaultExecutor exec = new DefaultExecutor();
316 
317 		if (successCodes != null) {
318 			int size = successCodes.size();
319 			int[] exitValues = new int[size];
320 			for (int i = 0; i < size; i++) {
321 				exitValues[i] = new Integer(successCodes.get(i) + "");
322 			}
323 			exec.setExitValues(exitValues);
324 		}
325 		return exec;
326 	}
327 
328 	boolean isResultCodeAFailure(int result) {
329 		if (successCodes == null || successCodes.size() == 0) {
330 			return result != 0;
331 		}
332 		for (Iterator it = successCodes.iterator(); it.hasNext();) {
333 			int code = Integer.parseInt((String) it.next());
334 			if (code == result) {
335 				return false;
336 			}
337 		}
338 		return true;
339 	}
340 
341 	private boolean isLongClassPathArgument(String arg) {
342 		return longClasspath && ("-classpath".equals(arg) || "-cp".equals(arg));
343 	}
344 
345 	private Log getExecOutputLog() {
346 		Log log = getLog();
347 		if (outputFile != null) {
348 			try {
349 				if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
350 					getLog().warn("Could not create non existing parent directories for log file: " + outputFile);
351 				}
352 				PrintStream stream = new PrintStream(new FileOutputStream(outputFile));
353 
354 				log = new StreamLog(stream);
355 			} catch (Exception e) {
356 				getLog().warn("Could not open " + outputFile + ". Using default log", e);
357 			}
358 		}
359 
360 		return log;
361 	}
362 
363 	/**
364 	 * Compute the classpath from the specified Classpath. The computed classpath is based on the classpathScope. The
365 	 * plugin cannot know from maven the phase it is executed in. So we have to depend on the user to tell us he wants
366 	 * the scope in which the plugin is expected to be executed.
367 	 * 
368 	 * @param specifiedClasspath
369 	 *            Non null when the user restricted the dependenceis, null otherwise (the default classpath will be
370 	 *            used)
371 	 * @return a platform specific String representation of the classpath
372 	 */
373 	private String computeClasspathString(Classpath specifiedClasspath) {
374 		List resultList = computeClasspath(specifiedClasspath);
375 		StringBuffer theClasspath = new StringBuffer();
376 
377 		for (Iterator it = resultList.iterator(); it.hasNext();) {
378 			String str = (String) it.next();
379 			addToClasspath(theClasspath, str);
380 		}
381 
382 		return theClasspath.toString();
383 	}
384 
385 	/**
386 	 * Compute the classpath from the specified Classpath. The computed classpath is based on the classpathScope. The
387 	 * plugin cannot know from maven the phase it is executed in. So we have to depend on the user to tell us he wants
388 	 * the scope in which the plugin is expected to be executed.
389 	 * 
390 	 * @param specifiedClasspath
391 	 *            Non null when the user restricted the dependenceis, null otherwise (the default classpath will be
392 	 *            used)
393 	 * @return a list of class path elements
394 	 */
395 	private List computeClasspath(Classpath specifiedClasspath) {
396 		List artifacts = new ArrayList();
397 		List theClasspathFiles = new ArrayList();
398 		List resultList = new ArrayList();
399 
400 		collectProjectArtifactsAndClasspath(artifacts, theClasspathFiles);
401 
402 		if ((specifiedClasspath != null) && (specifiedClasspath.getDependencies() != null)) {
403 			artifacts = filterArtifacts(artifacts, specifiedClasspath.getDependencies());
404 		}
405 
406 		for (Iterator it = theClasspathFiles.iterator(); it.hasNext();) {
407 			File f = (File) it.next();
408 			resultList.add(f.getAbsolutePath());
409 		}
410 
411 		for (Iterator it = artifacts.iterator(); it.hasNext();) {
412 			Artifact artifact = (Artifact) it.next();
413 			getLog().debug("dealing with " + artifact);
414 			resultList.add(artifact.getFile().getAbsolutePath());
415 		}
416 
417 		return resultList;
418 	}
419 
420 	private static void addToClasspath(StringBuffer theClasspath, String toAdd) {
421 		if (theClasspath.length() > 0) {
422 			theClasspath.append(File.pathSeparator);
423 		}
424 		theClasspath.append(toAdd);
425 	}
426 
427 	private List filterArtifacts(List artifacts, Collection dependencies) {
428 		AndArtifactFilter filter = new AndArtifactFilter();
429 
430 		filter.add(new IncludesArtifactFilter(new ArrayList(dependencies))); // gosh
431 
432 		List filteredArtifacts = new ArrayList();
433 		for (Iterator it = artifacts.iterator(); it.hasNext();) {
434 			Artifact artifact = (Artifact) it.next();
435 			if (filter.include(artifact)) {
436 				getLog().debug("filtering in " + artifact);
437 				filteredArtifacts.add(artifact);
438 			}
439 		}
440 		return filteredArtifacts;
441 	}
442 
443 	CommandLine getExecutablePath(Map enviro, File dir) {
444 		File execFile = new File(executable);
445 		String exec = null;
446 		if (execFile.exists()) {
447 			getLog().debug("Toolchains are ignored, 'executable' parameter is set to " + executable);
448 			exec = execFile.getAbsolutePath();
449 		} else {
450 			Toolchain tc = getToolchain();
451 
452 			// if the file doesn't exist & toolchain is null, the exec is probably in the PATH...
453 			// we should probably also test for isFile and canExecute, but the second one is only
454 			// available in SDK 6.
455 			if (tc != null) {
456 				getLog().info("Toolchain in exec-maven-plugin: " + tc);
457 				exec = tc.findTool(executable);
458 			} else {
459 				if (OS.isFamilyWindows()) {
460 					String ex = executable.indexOf(".") < 0 ? executable + ".bat" : executable;
461 					File f = new File(dir, ex);
462 					if (f.exists()) {
463 						exec = ex;
464 					} else {
465 						// now try to figure the path from PATH, PATHEXT env vars
466 						// if bat file, wrap in cmd /c
467 						String path = (String) enviro.get("PATH");
468 						if (path != null) {
469 							String[] elems = StringUtils.split(path, File.pathSeparator);
470 							for (int i = 0; i < elems.length; i++) {
471 								f = new File(new File(elems[i]), ex);
472 								if (f.exists()) {
473 									exec = ex;
474 									break;
475 								}
476 							}
477 						}
478 					}
479 				}
480 			}
481 		}
482 
483 		if (exec == null) {
484 			exec = executable;
485 		}
486 
487 		CommandLine toRet;
488 		if (OS.isFamilyWindows() && exec.toLowerCase(Locale.getDefault()).endsWith(".bat")) {
489 			toRet = new CommandLine("cmd");
490 			toRet.addArgument("/c");
491 			toRet.addArgument(exec);
492 		} else {
493 			toRet = new CommandLine(exec);
494 		}
495 
496 		return toRet;
497 	}
498 
499 	// private String[] DEFAULT_PATH_EXT = new String[] {
500 	// .COM; .EXE; .BAT; .CMD; .VBS; .VBE; .JS; .JSE; .WSF; .WSH
501 	// ".COM", ".EXE", ".BAT", ".CMD"
502 	// };
503 
504 	private static boolean isEmpty(String string) {
505 		return string == null || string.length() == 0;
506 	}
507 
508 	//
509 	// methods used for tests purposes - allow mocking and simulate automatic setters
510 	//
511 
512 	protected int executeCommandLine(Executor exec, CommandLine commandLine, Map enviro, OutputStream out,
513 			OutputStream err) throws ExecuteException, IOException {
514 		exec.setStreamHandler(new PumpStreamHandler(out, err, System.in));
515 		return exec.execute(commandLine, enviro);
516 	}
517 
518 	void setExecutable(String executable) {
519 		this.executable = executable;
520 	}
521 
522 	String getExecutable() {
523 		return executable;
524 	}
525 
526 	void setWorkingDirectory(String workingDir) {
527 		setWorkingDirectory(new File(workingDir));
528 	}
529 
530 	void setWorkingDirectory(File workingDir) {
531 		this.workingDirectory = workingDir;
532 	}
533 
534 	void setArguments(List arguments) {
535 		this.arguments = arguments;
536 	}
537 
538 	void setBasedir(File basedir) {
539 		this.basedir = basedir;
540 	}
541 
542 	void setProject(MavenProject project) {
543 		this.project = project;
544 	}
545 
546 	protected String getSystemProperty(String key) {
547 		return System.getProperty(key);
548 	}
549 
550 	public void setSuccessCodes(List list) {
551 		this.successCodes = list;
552 	}
553 
554 	public List getSuccessCodes() {
555 		return successCodes;
556 	}
557 
558 	private Toolchain getToolchain() {
559 		Toolchain tc = null;
560 
561 		try {
562 			if (session != null) // session is null in tests..
563 			{
564 				ToolchainManager toolchainManager = (ToolchainManager) session.getContainer().lookup(
565 						ToolchainManager.ROLE);
566 
567 				if (toolchainManager != null) {
568 					tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
569 				}
570 			}
571 		} catch (ComponentLookupException componentLookupException) {
572 			// just ignore, could happen in pre-2.0.9 builds..
573 		}
574 		return tc;
575 	}
576 
577 	/**
578 	 * Create a jar with just a manifest containing a Main-Class entry for SurefireBooter and a Class-Path entry for all
579 	 * classpath elements. Copied from surefire (ForkConfiguration#createJar())
580 	 * 
581 	 * @param classPath
582 	 *            List&lt;String> of all classpath elements.
583 	 * @return
584 	 * @throws IOException
585 	 */
586 	private File createJar(List classPath, String mainClass) throws IOException {
587 		File file = File.createTempFile("maven-exec", ".jar");
588 		file.deleteOnExit();
589 		FileOutputStream fos = new FileOutputStream(file);
590 		JarOutputStream jos = new JarOutputStream(fos);
591 		jos.setLevel(JarOutputStream.STORED);
592 		JarEntry je = new JarEntry("META-INF/MANIFEST.MF");
593 		jos.putNextEntry(je);
594 
595 		Manifest man = new Manifest();
596 
597 		// we can't use StringUtils.join here since we need to add a '/' to
598 		// the end of directory entries - otherwise the jvm will ignore them.
599 		String cp = "";
600 		for (Iterator it = classPath.iterator(); it.hasNext();) {
601 			String el = (String) it.next();
602 			// NOTE: if File points to a directory, this entry MUST end in '/'.
603 			cp += UrlUtils.getURL(new File(el)).toExternalForm() + " ";
604 		}
605 
606 		man.getMainAttributes().putValue("Manifest-Version", "1.0");
607 		man.getMainAttributes().putValue("Class-Path", cp.trim());
608 		man.getMainAttributes().putValue("Main-Class", mainClass);
609 
610 		man.write(jos);
611 		jos.close();
612 
613 		return file;
614 	}
615 }