001    package org.codehaus.mojo.exec;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
005     * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
007     * License. You may obtain a copy of the License at
008     * 
009     * http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
012     * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
013     * specific language governing permissions and limitations under the License.
014     */
015    
016    import java.io.File;
017    import java.io.FileOutputStream;
018    import java.io.IOException;
019    import java.io.OutputStream;
020    import java.io.PrintStream;
021    import java.util.ArrayList;
022    import java.util.Arrays;
023    import java.util.Collection;
024    import java.util.HashMap;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Locale;
028    import java.util.Map;
029    import java.util.Properties;
030    import java.util.jar.JarEntry;
031    import java.util.jar.JarOutputStream;
032    import java.util.jar.Manifest;
033    
034    import org.apache.commons.exec.CommandLine;
035    import org.apache.commons.exec.DefaultExecutor;
036    import org.apache.commons.exec.ExecuteException;
037    import org.apache.commons.exec.Executor;
038    import org.apache.commons.exec.OS;
039    import org.apache.commons.exec.PumpStreamHandler;
040    import org.apache.maven.artifact.Artifact;
041    import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
042    import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
043    import org.apache.maven.execution.MavenSession;
044    import org.apache.maven.plugin.MojoExecutionException;
045    import org.apache.maven.plugin.logging.Log;
046    import org.apache.maven.project.MavenProject;
047    import org.apache.maven.toolchain.Toolchain;
048    import org.apache.maven.toolchain.ToolchainManager;
049    import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
050    import org.codehaus.plexus.util.StringUtils;
051    import org.codehaus.plexus.util.cli.CommandLineUtils;
052    
053    /**
054     * A Plugin for executing external programs.
055     * 
056     * @author Jerome Lacoste <jerome@coffeebreaks.org>
057     * @version $Id: ExecMojo.java 12386 2010-07-16 22:10:38Z rfscholte $
058     * @since 1.0
059     */
060    public class ExecMojo extends AbstractExecMojo {
061        /**
062         * The executable. Can be a full path or a the name executable. In the latter case, the executable must be in the
063         * PATH for the execution to work.
064         */
065        private String executable;
066    
067        /**
068         * The current working directory. Optional. If not specified, basedir will be used.
069         */
070        private File workingDirectory;
071    
072        /**
073         * Program standard and error output will be redirected to the file specified by this optional field. If not
074         * specified the standard maven logging is used.
075         */
076        private File outputFile;
077    
078        /**
079         * Can be of type <code>&lt;argument&gt;</code> or <code>&lt;classpath&gt;</code> Can be overridden using
080         * "exec.args" env. variable
081         */
082        private List arguments;
083    
084        /**
085         * @parameter expression="${basedir}"
086         * @required
087         * @readonly
088         * @since 1.0
089         */
090        private File basedir;
091    
092        /**
093         * Environment variables to pass to the executed program.
094         */
095        private Map environmentVariables = new HashMap();
096    
097        /**
098         * The current build session instance. This is used for toolchain manager API calls.
099         * 
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    }