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><argument></code> or <code><classpath></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<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 }