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