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>&lt;argument&gt;</code> or <code>&lt;classpath&gt;</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&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    }