001 package org.codehaus.mojo.exec;
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 java.io.File;
023 import java.io.FileOutputStream;
024 import java.io.IOException;
025 import java.io.OutputStream;
026 import java.io.PrintStream;
027 import java.util.ArrayList;
028 import java.util.Arrays;
029 import java.util.Collection;
030 import java.util.HashMap;
031 import java.util.Iterator;
032 import java.util.List;
033 import java.util.Locale;
034 import java.util.Map;
035 import java.util.Properties;
036 import java.util.jar.JarEntry;
037 import java.util.jar.JarOutputStream;
038 import java.util.jar.Manifest;
039
040 import org.apache.commons.exec.CommandLine;
041 import org.apache.commons.exec.DefaultExecutor;
042 import org.apache.commons.exec.ExecuteException;
043 import org.apache.commons.exec.Executor;
044 import org.apache.commons.exec.OS;
045 import org.apache.commons.exec.PumpStreamHandler;
046 import org.apache.maven.artifact.Artifact;
047 import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
048 import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
049 import org.apache.maven.execution.MavenSession;
050 import org.apache.maven.plugin.MojoExecutionException;
051 import org.apache.maven.plugin.logging.Log;
052 import org.apache.maven.project.MavenProject;
053 import org.apache.maven.toolchain.Toolchain;
054 import org.apache.maven.toolchain.ToolchainManager;
055 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
056 import org.codehaus.plexus.util.StringUtils;
057 import org.codehaus.plexus.util.cli.CommandLineUtils;
058
059 /**
060 * A Plugin for executing external programs.
061 *
062 * @author Jerome Lacoste <jerome@coffeebreaks.org>
063 * @version $Id: ExecMojo.java 12386 2010-07-16 22:10:38Z rfscholte $
064 * @goal exec
065 * @requiresDependencyResolution test
066 * @since 1.0
067 */
068 public class ExecMojo extends AbstractExecMojo {
069 /**
070 * The executable. Can be a full path or a the name executable. In the latter case, the executable must be in the
071 * PATH for the execution to work.
072 *
073 * @parameter expression="${exec.executable}"
074 * @required
075 * @since 1.0
076 */
077 private String executable;
078
079 /**
080 * The current working directory. Optional. If not specified, basedir will be used.
081 *
082 * @parameter expression="${exec.workingdir}
083 * @since 1.0
084 */
085 private File workingDirectory;
086
087 /**
088 * Program standard and error output will be redirected to the file specified by this optional field. If not
089 * specified the standard maven logging is used.
090 *
091 * @parameter expression="${exec.outputFile}"
092 * @since 1.1-beta-2
093 */
094 private File outputFile;
095
096 /**
097 * Can be of type <code><argument></code> or <code><classpath></code> Can be overriden using "exec.args"
098 * env. variable
099 *
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<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 }