View Javadoc

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