001/* 002 * Copyright 2007 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kualigan.maven.plugins.api; 017 018import java.io.IOException; 019 020import org.apache.commons.cli.CommandLine; 021import org.apache.commons.cli.OptionBuilder; 022import org.apache.commons.cli.Options; 023import org.apache.commons.cli.PosixParser; 024import org.apache.commons.exec.DefaultExecutor; 025import org.apache.commons.exec.ExecuteException; 026import org.apache.commons.exec.Executor; 027import org.apache.commons.exec.PumpStreamHandler; 028 029import org.apache.maven.archetype.Archetype; 030import org.apache.maven.artifact.repository.ArtifactRepository; 031import org.apache.maven.artifact.repository.ArtifactRepositoryFactory; 032import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; 033import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; 034 035 036import org.apache.maven.shared.invoker.DefaultInvocationRequest; 037import org.apache.maven.shared.invoker.DefaultInvoker; 038import org.apache.maven.shared.invoker.InvocationOutputHandler; 039import org.apache.maven.shared.invoker.InvocationRequest; 040import org.apache.maven.shared.invoker.InvocationResult; 041import org.apache.maven.shared.invoker.Invoker; 042import org.apache.maven.shared.invoker.InvokerLogger; 043import org.apache.maven.shared.invoker.MavenInvocationException; 044 045import org.apache.maven.plugin.AbstractMojo; 046import org.apache.maven.plugin.MojoExecutionException; 047import org.apache.maven.project.MavenProject; 048 049import org.codehaus.plexus.archiver.Archiver; 050import org.codehaus.plexus.archiver.ArchiverException; 051import org.codehaus.plexus.archiver.UnArchiver; 052import org.codehaus.plexus.archiver.manager.ArchiverManager; 053import org.codehaus.plexus.archiver.manager.NoSuchArchiverException; 054import org.codehaus.plexus.archiver.util.DefaultFileSet; 055import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector; 056 057import org.codehaus.plexus.component.annotations.Component; 058import org.codehaus.plexus.component.annotations.Requirement; 059import org.codehaus.plexus.components.interactivity.Prompter; 060import org.codehaus.plexus.util.FileUtils; 061import org.codehaus.plexus.util.IOUtil; 062import org.codehaus.plexus.util.StringUtils; 063import org.codehaus.plexus.util.cli.CommandLineUtils; 064 065import java.io.DataInputStream; 066import java.io.File; 067import java.io.FileOutputStream; 068import java.io.FileWriter; 069import java.io.InputStream; 070import java.util.ArrayList; 071import java.util.HashMap; 072import java.util.List; 073import java.util.Map; 074import java.util.Properties; 075import java.util.StringTokenizer; 076 077/** 078 * Creates a prototype from the given KFS project resource. A KFS project resource can be either 079 * of the following: 080 * <ul> 081 * <li>KFS war file</li> 082 * <li>KFS project directory with source</li> 083 * <li>KFS svn repo</li> 084 * </ul> 085 * 086 * @author Leo Przybylski 087 */ 088@Component(role = org.kualigan.maven.plugins.api.PrototypeHelper.class, hint="default") 089public class DefaultPrototypeHelper implements PrototypeHelper { 090 public static final String ROLE_HINT = "default"; 091 092 private static final Options OPTIONS = new Options(); 093 094 private static final char SET_SYSTEM_PROPERTY = 'D'; 095 096 private static final char OFFLINE = 'o'; 097 098 private static final char REACTOR = 'r'; 099 100 private static final char QUIET = 'q'; 101 102 private static final char DEBUG = 'X'; 103 104 private static final char ERRORS = 'e'; 105 106 private static final char NON_RECURSIVE = 'N'; 107 108 private static final char UPDATE_SNAPSHOTS = 'U'; 109 110 private static final char ACTIVATE_PROFILES = 'P'; 111 112 private static final String FORCE_PLUGIN_UPDATES = "cpu"; 113 114 private static final String FORCE_PLUGIN_UPDATES2 = "up"; 115 116 private static final String SUPPRESS_PLUGIN_UPDATES = "npu"; 117 118 private static final String SUPPRESS_PLUGIN_REGISTRY = "npr"; 119 120 private static final char CHECKSUM_FAILURE_POLICY = 'C'; 121 122 private static final char CHECKSUM_WARNING_POLICY = 'c'; 123 124 private static final char ALTERNATE_USER_SETTINGS = 's'; 125 126 private static final String FAIL_FAST = "ff"; 127 128 private static final String FAIL_AT_END = "fae"; 129 130 private static final String FAIL_NEVER = "fn"; 131 132 private static final String ALTERNATE_POM_FILE = "f"; 133 134 /** 135 */ 136 @Requirement 137 protected Archetype archetype; 138 139 /** 140 */ 141 @Requirement 142 protected Prompter prompter; 143 144 /** 145 */ 146 @Requirement 147 protected ArtifactRepositoryFactory artifactRepositoryFactory; 148 149 /** 150 */ 151 @Requirement(role=org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout.class, hint="default") 152 protected ArtifactRepositoryLayout defaultArtifactRepositoryLayout; 153 154 @Requirement 155 protected ArchiverManager archiverManager; 156 157 private AbstractMojo caller; 158 159 protected void logUnpack(final File file, final File location, final String includes, final String excludes ) { 160 if (!getCaller().getLog().isInfoEnabled()) { 161 return; 162 } 163 164 StringBuffer msg = new StringBuffer(); 165 msg.append( "Unpacking " ); 166 msg.append( file ); 167 msg.append( " to " ); 168 msg.append( location ); 169 170 if ( includes != null && excludes != null ) { 171 msg.append( " with includes \"" ); 172 msg.append( includes ); 173 msg.append( "\" and excludes \"" ); 174 msg.append( excludes ); 175 msg.append( "\"" ); 176 } 177 else if (includes != null) { 178 msg.append( " with includes \"" ); 179 msg.append( includes ); 180 msg.append( "\"" ); 181 } 182 else if (excludes != null) { 183 msg.append( " with excludes \"" ); 184 msg.append( excludes ); 185 msg.append( "\"" ); 186 } 187 188 getCaller().getLog().info(msg.toString()); 189 } 190 191 192 protected int executeCommandLine(final String ... args) throws ExecuteException, IOException { 193 final Executor exec = new DefaultExecutor(); 194 final Map enviro = new HashMap(); 195 try { 196 final Properties systemEnvVars = CommandLineUtils.getSystemEnvVars(); 197 enviro.putAll(systemEnvVars); 198 } catch (IOException x) { 199 getCaller().getLog().error("Could not assign default system enviroment variables.", x); 200 } 201 202 final File workingDirectory = new File(System.getProperty("java.io.tmpdir")); 203 final org.apache.commons.exec.CommandLine commandLine = new org.apache.commons.exec.CommandLine(args[0]); 204 for (int i = 1; i < args.length; i++) { 205 commandLine.addArgument(args[i]); 206 } 207 208 exec.setStreamHandler(new PumpStreamHandler(System.out, System.err, System.in)); 209 exec.setWorkingDirectory(workingDirectory); 210 return exec.execute(commandLine, enviro); 211 } 212 213 /** 214 * Handles repacking of the war as a jar file with classes, etc... Basically makes a jar of everything 215 * in the war file's WEB-INF/classes path. 216 * 217 * @param file is the war file to repackage 218 * @return {@link File} instance of the repacked jar 219 */ 220 public File repack(final File file, final String artifactId) throws MojoExecutionException { 221 final File workingDirectory = new File(System.getProperty("java.io.tmpdir") + File.separator 222 + artifactId + "-repack"); 223 final File warDirectory = new File(workingDirectory, "war"); 224 final File repackDirectory = new File(workingDirectory, artifactId); 225 final File classesDirectory = new File(warDirectory, "WEB-INF/classes"); 226 final File retval = new File(workingDirectory, artifactId + ".jar"); 227 228 try { 229 workingDirectory.mkdirs(); 230 workingDirectory.mkdir(); 231 workingDirectory.deleteOnExit(); 232 warDirectory.mkdir(); 233 } 234 catch (Exception e) { 235 throw new MojoExecutionException("Unable to create working directory for repackaging", e); 236 } 237 238 unpack(file, warDirectory, "**/classes/**", null); 239 240 try { 241 FileUtils.copyDirectoryStructure(classesDirectory, repackDirectory); 242 } 243 catch (Exception e) { 244 throw new MojoExecutionException("Unable to copy files into the repack directory"); 245 } 246 247 try { 248 pack(retval, repackDirectory, "**/**", null); 249 } 250 catch (Exception e) { 251 throw new MojoExecutionException("Was unable to create the jar", e); 252 } 253 254 return retval; 255 } 256 257 /** 258 * Unpacks the archive file. 259 * 260 * @param file File to be unpacked. 261 * @param location Location where to put the unpacked files. 262 * @param includes Comma separated list of file patterns to include i.e. <code>**/.xml, 263 * **/*.properties</code> 264 * @param excludes Comma separated list of file patterns to exclude i.e. <code>**/*.xml, 265 * **/*.properties</code> 266 */ 267 protected void unpack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException { 268 try { 269 logUnpack(file, location, includes, excludes); 270 271 location.mkdirs(); 272 273 final UnArchiver unArchiver; 274 unArchiver = archiverManager.getUnArchiver(file); 275 unArchiver.setSourceFile(file); 276 unArchiver.setDestDirectory(location); 277 278 if (StringUtils.isNotEmpty(excludes) || StringUtils.isNotEmpty(includes)) { 279 final IncludeExcludeFileSelector[] selectors = 280 new IncludeExcludeFileSelector[]{ new IncludeExcludeFileSelector() }; 281 282 if (StringUtils.isNotEmpty( excludes )) { 283 selectors[0].setExcludes(excludes.split( "," )); 284 } 285 286 if (StringUtils.isNotEmpty( includes )) { 287 selectors[0].setIncludes(includes.split( "," )); 288 } 289 290 unArchiver.setFileSelectors(selectors); 291 } 292 293 unArchiver.extract(); 294 } 295 catch ( NoSuchArchiverException e ) { 296 throw new MojoExecutionException("Unknown archiver type", e); 297 } 298 catch (ArchiverException e) { 299 e.printStackTrace(); 300 throw new MojoExecutionException( 301 "Error unpacking file: " + file + " to: " + location + "\r\n" + e.toString(), e ); 302 } 303 } 304 305 /** 306 * Packs a jar 307 * 308 * @param file Destination file. 309 * @param location Directory source. 310 * @param includes Comma separated list of file patterns to include i.e. <code>**/.xml, 311 * **/*.properties</code> 312 * @param excludes Comma separated list of file patterns to exclude i.e. <code>**/*.xml, 313 * **/*.properties</code> 314 */ 315 protected void pack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException { 316 try { 317 318 final Archiver archiver; 319 archiver = archiverManager.getArchiver(file); 320 archiver.addFileSet(new DefaultFileSet() {{ 321 setDirectory(location); 322 if (includes != null) { 323 setIncludes(includes.split(",")); 324 } 325 if (excludes != null) { 326 setExcludes(excludes.split(",")); 327 } 328 }}); 329 archiver.setDestFile(file); 330 331 archiver.createArchive(); 332 } 333 catch ( NoSuchArchiverException e ) { 334 throw new MojoExecutionException("Unknown archiver type", e); 335 } 336 catch (Exception e) { 337 e.printStackTrace(); 338 throw new MojoExecutionException( 339 "Error packing directory: " + location + " to: " + file + "\r\n" + e.toString(), e ); 340 } 341 } 342 343 /** 344 * 345 */ 346 private void setupRequest(final InvocationRequest req, 347 final String additionalArguments) throws MojoExecutionException { 348 try { 349 final String[] args = CommandLineUtils.translateCommandline(additionalArguments); 350 CommandLine cli = new PosixParser().parse(OPTIONS, args); 351 352 if (cli.hasOption( SET_SYSTEM_PROPERTY)) 353 { 354 String[] properties = cli.getOptionValues( SET_SYSTEM_PROPERTY ); 355 Properties props = new Properties(); 356 for ( int i = 0; i < properties.length; i++ ) 357 { 358 String property = properties[i]; 359 String name, value; 360 int sep = property.indexOf( "=" ); 361 if ( sep <= 0 ) 362 { 363 name = property.trim(); 364 value = "true"; 365 } 366 else 367 { 368 name = property.substring( 0, sep ).trim(); 369 value = property.substring( sep + 1 ).trim(); 370 } 371 props.setProperty( name, value ); 372 } 373 374 req.setProperties( props ); 375 } 376 377 if ( cli.hasOption( OFFLINE ) ) 378 { 379 req.setOffline( true ); 380 } 381 382 if ( cli.hasOption( QUIET ) ) 383 { 384 // TODO: setQuiet() currently not supported by InvocationRequest 385 req.setDebug( false ); 386 } 387 else if ( cli.hasOption( DEBUG ) ) 388 { 389 req.setDebug( true ); 390 } 391 else if ( cli.hasOption( ERRORS ) ) 392 { 393 req.setShowErrors( true ); 394 } 395 396 if ( cli.hasOption( REACTOR ) ) 397 { 398 req.setRecursive( true ); 399 } 400 else if ( cli.hasOption( NON_RECURSIVE ) ) 401 { 402 req.setRecursive( false ); 403 } 404 405 if ( cli.hasOption( UPDATE_SNAPSHOTS ) ) 406 { 407 req.setUpdateSnapshots( true ); 408 } 409 410 if ( cli.hasOption( ACTIVATE_PROFILES ) ) 411 { 412 String[] profiles = cli.getOptionValues( ACTIVATE_PROFILES ); 413 List<String> activatedProfiles = new ArrayList<String>(); 414 List<String> deactivatedProfiles = new ArrayList<String>(); 415 416 if ( profiles != null ) 417 { 418 for ( int i = 0; i < profiles.length; ++i ) 419 { 420 StringTokenizer profileTokens = new StringTokenizer( profiles[i], "," ); 421 422 while ( profileTokens.hasMoreTokens() ) 423 { 424 String profileAction = profileTokens.nextToken().trim(); 425 426 if ( profileAction.startsWith( "-" ) || profileAction.startsWith( "!" ) ) 427 { 428 deactivatedProfiles.add( profileAction.substring( 1 ) ); 429 } 430 else if ( profileAction.startsWith( "+" ) ) 431 { 432 activatedProfiles.add( profileAction.substring( 1 ) ); 433 } 434 else 435 { 436 activatedProfiles.add( profileAction ); 437 } 438 } 439 } 440 } 441 442 if (!deactivatedProfiles.isEmpty()) { 443 getCaller().getLog().warn("Explicit profile deactivation is not yet supported. " 444 + "The following profiles will NOT be deactivated: " + StringUtils.join( 445 deactivatedProfiles.iterator(), ", ")); 446 } 447 448 if (!activatedProfiles.isEmpty()) { 449 req.setProfiles(activatedProfiles); 450 } 451 } 452 453 if (cli.hasOption(FORCE_PLUGIN_UPDATES) || cli.hasOption( FORCE_PLUGIN_UPDATES2)) { 454 getCaller().getLog().warn("Forcing plugin updates is not supported currently."); 455 } 456 else if (cli.hasOption( SUPPRESS_PLUGIN_UPDATES)) { 457 req.setNonPluginUpdates( true ); 458 } 459 460 if (cli.hasOption( SUPPRESS_PLUGIN_REGISTRY)) { 461 getCaller().getLog().warn("Explicit suppression of the plugin registry is not supported currently." ); 462 } 463 464 if (cli.hasOption(CHECKSUM_FAILURE_POLICY)) { 465 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_FAIL ); 466 } 467 else if ( cli.hasOption( CHECKSUM_WARNING_POLICY ) ) 468 { 469 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_WARN ); 470 } 471 472 if ( cli.hasOption( ALTERNATE_USER_SETTINGS ) ) 473 { 474 req.setUserSettingsFile( new File( cli.getOptionValue( ALTERNATE_USER_SETTINGS ) ) ); 475 } 476 477 if ( cli.hasOption( FAIL_AT_END ) ) 478 { 479 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_AT_END ); 480 } 481 else if ( cli.hasOption( FAIL_FAST ) ) 482 { 483 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_FAST ); 484 } 485 if ( cli.hasOption( FAIL_NEVER ) ) 486 { 487 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_NEVER ); 488 } 489 if ( cli.hasOption( ALTERNATE_POM_FILE ) ) 490 { 491 if (req.getPomFileName() != null) { 492 getCaller().getLog().info("pomFileName is already set, ignoring the -f argument" ); 493 } 494 else { 495 req.setPomFileName( cli.getOptionValue( ALTERNATE_POM_FILE ) ); 496 } 497 } 498 } 499 catch ( Exception e ) { 500 throw new MojoExecutionException("Failed to re-parse additional arguments for Maven invocation.", e ); 501 } 502 } 503 504 /** 505 * Executes the {@code install-file} goal with the new pom against the artifact file. 506 * 507 * @param artifact {@link File} instance to install 508 */ 509 public void installArtifact(final File artifact, 510 final File sources, 511 final File mavenHome, 512 final String groupId, 513 final String artifactId, 514 final String version, 515 final String repositoryId) throws MojoExecutionException { 516 extractBuildXml(); 517 518 filterTempPom(groupId, artifactId, artifact.getName().endsWith("jar") ? "jar" : "war", version); 519 520 final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome); 521 522 final String additionalArguments = ""; 523 524 getCaller().getLog().debug("Setting up properties for installing the artifact"); 525 final InvocationRequest req = new DefaultInvocationRequest() 526 .setInteractive(false) 527 .setProperties(new Properties() {{ 528 setProperty("pomFile", getTempPomPath()); 529 if (repositoryId != null) { 530 setProperty("repositoryId", repositoryId); 531 } 532 if (sources != null) { 533 try { 534 setProperty("sources", sources.getCanonicalPath()); 535 } 536 catch (Exception e) { 537 throw new MojoExecutionException("Cannot get path for the sources file ", e); 538 } 539 } 540 try { 541 setProperty("file", artifact.getCanonicalPath()); 542 } 543 catch (Exception e) { 544 throw new MojoExecutionException("Cannot get path for the war file ", e); 545 } 546 setProperty("updateReleaseInfo", "true"); 547 }}); 548 549 getCaller().getLog().debug("Properties used for installArtifact are:"); 550 try { 551 req.getProperties().list(System.out); 552 } 553 catch (Exception e) { 554 } 555 556 try { 557 setupRequest(req, additionalArguments); 558 559 if (repositoryId == null) { 560 req.setGoals(new ArrayList<String>() {{ add("install:install-file"); }}); 561 } 562 else { 563 req.setGoals(new ArrayList<String>() {{ add("deploy:deploy-file"); }}); 564 } 565 566 try { 567 final InvocationResult invocationResult = invoker.execute(req); 568 569 if ( invocationResult.getExecutionException() != null ) { 570 throw new MojoExecutionException("Error executing Maven.", 571 invocationResult.getExecutionException()); 572 } 573 574 if (invocationResult.getExitCode() != 0) { 575 throw new MojoExecutionException( 576 "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'"); 577 } 578 } 579 catch (MavenInvocationException e) { 580 throw new MojoExecutionException( "Failed to invoke Maven build.", e ); 581 } 582 } 583 finally { 584 /* 585 if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() ) 586 { 587 settingsFile.deleteOnExit(); 588 } 589 */ 590 } 591 } 592 593 /** 594 * Executes the {@code install-file} goal with the new pom against the artifact file. 595 * 596 * @param artifact {@link File} instance to install 597 */ 598 public void filterTempPom(final String groupId, 599 final String artifactId, 600 final String packaging, 601 final String version) throws MojoExecutionException { 602 getCaller().getLog().info("Extracting the Temp POM"); 603 604 try { 605 executeCommandLine("ant", 606 "-Dsource=" + System.getProperty("java.io.tmpdir") + File.separator + "pom.xml", 607 "-Dtarget=" + System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml", 608 "-DgroupId=" + groupId, 609 "-DartifactId=" + artifactId, 610 "-Dpackaging=" + packaging, 611 "-Dversion=" + version); 612 } 613 catch (Exception e) { 614 throw new MojoExecutionException("Error trying to filter the pom with ant ", e); 615 } 616 } 617 618 /** 619 * Temporary POM location 620 * 621 * @return String value the path of the temporary POM 622 */ 623 protected String getTempPomPath() { 624 return System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml"; 625 } 626 627 /** 628 * Puts ant build file in the system temp directory. build.xml is extracted 629 * from the plugin. 630 */ 631 public void extractBuildXml() throws MojoExecutionException { 632 getCaller().getLog().info("Extracting the build.xml"); 633 634 final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/build.xml"); 635 636 byte[] fileBytes = null; 637 try { 638 final DataInputStream dis = new DataInputStream(pom_is); 639 fileBytes = new byte[dis.available()]; 640 dis.readFully(fileBytes); 641 dis.close(); 642 } 643 catch (Exception e) { 644 throw new MojoExecutionException("Wasn't able to read in the prototype pom", e); 645 } 646 finally { 647 try { 648 pom_is.close(); 649 } 650 catch (Exception e) { 651 // Ignore exceptions 652 } 653 } 654 655 try { 656 final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "build.xml"); 657 try { 658 fos.write(fileBytes); 659 } 660 finally { 661 fos.close(); 662 } 663 } 664 catch (Exception e) { 665 throw new MojoExecutionException("Could not write temporary pom file", e); 666 } 667 } 668 669 /** 670 * Puts temporary pom in the system temp directory. prototype-pom.xml is extracted 671 * from the plugin. 672 */ 673 public void extractTempPom() throws MojoExecutionException { 674 getCaller().getLog().info("Extracting the Temp Pom"); 675 676 final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/pom.xml"); 677 678 byte[] fileBytes = null; 679 try { 680 final DataInputStream dis = new DataInputStream(pom_is); 681 fileBytes = new byte[dis.available()]; 682 dis.readFully(fileBytes); 683 dis.close(); 684 } 685 catch (Exception e) { 686 throw new MojoExecutionException("Wasn't able to read in the prototype pom", e); 687 } 688 finally { 689 try { 690 pom_is.close(); 691 } 692 catch (Exception e) { 693 // Ignore exceptions 694 } 695 } 696 697 try { 698 final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml"); 699 try { 700 fos.write(fileBytes); 701 } 702 finally { 703 fos.close(); 704 } 705 } 706 catch (Exception e) { 707 throw new MojoExecutionException("Could not write temporary pom file", e); 708 } 709 } 710 711 public void setCaller(final AbstractMojo caller) { 712 this.caller = caller; 713 } 714 715 public AbstractMojo getCaller() { 716 return this.caller; 717 } 718}