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 }