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 filterTempPom(groupId, artifactId, artifact.getName().endsWith("jar") ? "jar" : "war", version); 504 505 final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome); 506 507 final String additionalArguments = ""; 508 509 getCaller().getLog().debug("Setting up properties for installing the artifact"); 510 final InvocationRequest req = new DefaultInvocationRequest() 511 .setInteractive(false) 512 .setProperties(new Properties() {{ 513 setProperty("pomFile", getTempPomPath()); 514 if (repositoryId != null) { 515 setProperty("repositoryId", repositoryId); 516 } 517 if (sources != null) { 518 try { 519 setProperty("sources", zipSourcesIfRequired(sources).getCanonicalPath()); 520 } catch (Exception e) { 521 throw new MojoExecutionException("Cannot get path for the sources file ", e); 522 } 523 } 524 try { 525 setProperty("file", artifact.getCanonicalPath()); 526 } catch (Exception e) { 527 throw new MojoExecutionException("Cannot get path for the war file ", e); 528 } 529 setProperty("updateReleaseInfo", "true"); 530 }}); 531 532 getCaller().getLog().debug("Properties used for installArtifact are:"); 533 try { 534 req.getProperties().list(System.out); 535 } 536 catch (Exception e) { 537 } 538 539 try { 540 setupRequest(req, additionalArguments); 541 542 if (repositoryId == null) { 543 req.setGoals(new ArrayList<String>() {{ add("install:install-file"); }}); 544 } 545 else { 546 req.setGoals(new ArrayList<String>() {{ add("deploy:deploy-file"); }}); 547 } 548 549 try { 550 final InvocationResult invocationResult = invoker.execute(req); 551 552 if ( invocationResult.getExecutionException() != null ) { 553 throw new MojoExecutionException("Error executing Maven.", 554 invocationResult.getExecutionException()); 555 } 556 557 if (invocationResult.getExitCode() != 0) { 558 throw new MojoExecutionException( 559 "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'"); 560 } 561 } 562 catch (MavenInvocationException e) { 563 throw new MojoExecutionException( "Failed to invoke Maven build.", e ); 564 } 565 } 566 finally { 567 /* 568 if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() ) 569 { 570 settingsFile.deleteOnExit(); 571 } 572 */ 573 } 574 } 575 576 /** 577 * Executes the {@code install-file} goal with the new pom against the artifact file. 578 * 579 */ 580 public void filterTempPom(final String groupId, 581 final String artifactId, 582 final String packaging, 583 final String version) throws MojoExecutionException { 584 getCaller().getLog().info("Filtering the Temp POM"); 585 586 Writer writer = null; 587 Reader reader = null; 588 try { 589 Context context = new VelocityContext(); 590 context.put("groupId", groupId); 591 context.put("artifactId", artifactId); 592 context.put("packaging", packaging); 593 context.put("version", version); 594 595 writer = new FileWriter(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml"); 596 reader = new FileReader(new File(System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml")); 597 598 Velocity.init(); 599 Velocity.evaluate(context, writer, "pom-prototype", reader); 600 } 601 catch (Exception e) { 602 throw new MojoExecutionException("Error trying to filter the pom ", e); 603 } 604 finally { 605 IOUtils.closeQuietly(reader); 606 IOUtils.closeQuietly(writer); 607 } 608 } 609 610 /** 611 * Temporary POM location 612 * 613 * @return String value the path of the temporary POM 614 */ 615 protected String getTempPomPath() { 616 return System.getProperty("java.io.tmpdir") + File.separator + "pom.xml"; 617 } 618 619 /** 620 * Puts temporary pom in the system temp directory. prototype-pom.xml is extracted 621 * from the plugin. 622 */ 623 public void extractTempPom() throws MojoExecutionException { 624 getCaller().getLog().info("Extracting the Temp Pom"); 625 626 final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/pom.xml"); 627 628 byte[] fileBytes = null; 629 try { 630 final DataInputStream dis = new DataInputStream(pom_is); 631 fileBytes = new byte[dis.available()]; 632 dis.readFully(fileBytes); 633 dis.close(); 634 } 635 catch (Exception e) { 636 throw new MojoExecutionException("Wasn't able to read in the prototype pom", e); 637 } 638 finally { 639 try { 640 pom_is.close(); 641 } 642 catch (Exception e) { 643 // Ignore exceptions 644 } 645 } 646 647 try { 648 final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml"); 649 try { 650 fos.write(fileBytes); 651 } 652 finally { 653 fos.close(); 654 } 655 } 656 catch (Exception e) { 657 throw new MojoExecutionException("Could not write temporary pom file", e); 658 } 659 } 660 661 protected File zipSourcesIfRequired(File sources) { 662 if (sources.isFile()){ 663 //Already a zip 664 return sources; 665 } 666 final File zipFile = new File(System.getProperty("java.io.tmpdir") + File.separator + "sources.zip"); 667 zipFile.deleteOnExit(); 668 ZipArchiver zipArchiver = new ZipArchiver(); 669 zipArchiver.addDirectory(sources, new String[]{"**/*"}, new String[]{}); 670 zipArchiver.setDestFile(zipFile); 671 try { 672 zipArchiver.createArchive(); 673 } catch (IOException e) { 674 throw new RuntimeException("Unable to zip source directory", e); 675 } 676 return zipFile; 677 } 678 679 public void setCaller(final AbstractMojo caller) { 680 this.caller = caller; 681 } 682 683 public AbstractMojo getCaller() { 684 return this.caller; 685 } 686 687}