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