001 package org.apache.maven.tools.plugin.generator; 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 org.apache.maven.plugin.descriptor.MojoDescriptor; 023 import org.apache.maven.plugin.descriptor.PluginDescriptor; 024 import org.apache.maven.plugin.logging.Log; 025 import org.apache.maven.project.MavenProject; 026 import org.apache.maven.tools.plugin.PluginToolsRequest; 027 import org.apache.velocity.VelocityContext; 028 import org.codehaus.plexus.logging.AbstractLogEnabled; 029 import org.codehaus.plexus.logging.Logger; 030 import org.codehaus.plexus.logging.console.ConsoleLogger; 031 import org.codehaus.plexus.util.FileUtils; 032 import org.codehaus.plexus.util.IOUtil; 033 import org.codehaus.plexus.util.PropertyUtils; 034 import org.codehaus.plexus.util.StringUtils; 035 import org.codehaus.plexus.velocity.VelocityComponent; 036 import org.objectweb.asm.ClassReader; 037 import org.objectweb.asm.ClassVisitor; 038 import org.objectweb.asm.ClassWriter; 039 import org.objectweb.asm.commons.Remapper; 040 import org.objectweb.asm.commons.RemappingClassAdapter; 041 import org.objectweb.asm.commons.SimpleRemapper; 042 043 import java.io.File; 044 import java.io.FileInputStream; 045 import java.io.FileOutputStream; 046 import java.io.IOException; 047 import java.io.InputStream; 048 import java.io.InputStreamReader; 049 import java.io.OutputStreamWriter; 050 import java.io.PrintWriter; 051 import java.io.Reader; 052 import java.io.StringWriter; 053 import java.io.UnsupportedEncodingException; 054 import java.util.List; 055 import java.util.Properties; 056 057 /** 058 * Generates an <code>HelpMojo</code> class from <code>help-class-source.vm</code> template. 059 * The generated mojo reads help content from <code>META-INF/maven/${groupId}/${artifactId}/plugin-help.xml</code> resource, 060 * which is generated by this {@link PluginDescriptorGenerator}. 061 * <p>Notice that the help mojo source needs to be generated before compilation, but when Java 5 annotations are used, 062 * plugin descriptor content is available only after compilation (detecting annotations in .class files): 063 * help mojo source can be generated with empty package only (and no plugin descriptor available yet), then needs 064 * to be updated after compilation - through {@link #rewriteHelpMojo(PluginToolsRequest, Log)} which is called from plugin 065 * descriptor XML generation.</p> 066 * 067 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a> 068 * @version $Id: PluginHelpGenerator.java 1406615 2012-11-07 13:26:25Z krosenvold $ 069 * @since 2.4 070 */ 071 public class PluginHelpGenerator 072 extends AbstractLogEnabled 073 implements Generator 074 { 075 /** 076 * Default generated class name 077 */ 078 private static final String HELP_MOJO_CLASS_NAME = "HelpMojo"; 079 080 /** 081 * Help properties file, to store data about generated source. 082 */ 083 private static final String HELP_PROPERTIES_FILENAME = "maven-plugin-help.properties"; 084 085 /** 086 * Default goal 087 */ 088 private static final String HELP_GOAL = "help"; 089 090 private String helpPackageName; 091 092 private boolean useAnnotations; 093 094 private VelocityComponent velocityComponent; 095 096 /** 097 * Default constructor 098 */ 099 public PluginHelpGenerator() 100 { 101 this.enableLogging( new ConsoleLogger( Logger.LEVEL_INFO, "PluginHelpGenerator" ) ); 102 } 103 104 // ---------------------------------------------------------------------- 105 // Public methods 106 // ---------------------------------------------------------------------- 107 108 /** 109 * {@inheritDoc} 110 */ 111 public void execute( File destinationDirectory, PluginToolsRequest request ) 112 throws GeneratorException 113 { 114 PluginDescriptor pluginDescriptor = request.getPluginDescriptor(); 115 116 String helpImplementation = getImplementation( pluginDescriptor ); 117 118 @SuppressWarnings( "unchecked" ) 119 List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos(); 120 121 if ( mojoDescriptors != null ) 122 { 123 // Verify that no help goal already exists 124 MojoDescriptor descriptor = pluginDescriptor.getMojo( HELP_GOAL ); 125 126 if ( ( descriptor != null ) && !descriptor.getImplementation().equals( helpImplementation ) ) 127 { 128 if ( getLogger().isWarnEnabled() ) 129 { 130 getLogger().warn( "\n\nA help goal (" + descriptor.getImplementation() 131 + ") already exists in this plugin. SKIPPED THE " + helpImplementation 132 + " GENERATION.\n" ); 133 } 134 135 return; 136 } 137 } 138 139 writeHelpPropertiesFile( request, destinationDirectory ); 140 141 useAnnotations = request.getProject().getArtifactMap().containsKey( "org.apache.maven.plugin-tools:maven-plugin-annotations" ); 142 143 try 144 { 145 String sourcePath = helpImplementation.replace( '.', File.separatorChar ) + ".java"; 146 147 File helpClass = new File( destinationDirectory, sourcePath ); 148 helpClass.getParentFile().mkdirs(); 149 150 String helpClassSources = getHelpClassSources( getPluginHelpPath( request.getProject() ), pluginDescriptor ); 151 152 FileUtils.fileWrite( helpClass, request.getEncoding(), helpClassSources ); 153 } 154 catch ( IOException e ) 155 { 156 throw new GeneratorException( e.getMessage(), e ); 157 } 158 } 159 160 public PluginHelpGenerator setHelpPackageName( String helpPackageName ) 161 { 162 this.helpPackageName = helpPackageName; 163 return this; 164 } 165 166 public VelocityComponent getVelocityComponent() 167 { 168 return velocityComponent; 169 } 170 171 public PluginHelpGenerator setVelocityComponent( VelocityComponent velocityComponent ) 172 { 173 this.velocityComponent = velocityComponent; 174 return this; 175 } 176 177 // ---------------------------------------------------------------------- 178 // Private methods 179 // ---------------------------------------------------------------------- 180 181 private String getHelpClassSources( String pluginHelpPath, PluginDescriptor pluginDescriptor ) 182 { 183 Properties properties = new Properties(); 184 VelocityContext context = new VelocityContext( properties ); 185 if ( this.helpPackageName != null ) 186 { 187 properties.put( "helpPackageName", this.helpPackageName ); 188 } 189 else 190 { 191 properties.put( "helpPackageName", "" ); 192 } 193 properties.put( "pluginHelpPath", pluginHelpPath ); 194 properties.put( "artifactId", pluginDescriptor.getArtifactId() ); 195 properties.put( "goalPrefix", pluginDescriptor.getGoalPrefix() ); 196 properties.put( "useAnnotations", useAnnotations ); 197 198 StringWriter stringWriter = new StringWriter(); 199 200 InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream( "help-class-source.vm" ); 201 InputStreamReader isReader = null; 202 try 203 { 204 isReader = new InputStreamReader( is, "UTF-8" ); // plugin-tools sources are UTF-8 (and even ASCII in this case) 205 velocityComponent.getEngine().evaluate( context, stringWriter, "", isReader ); 206 } 207 catch ( UnsupportedEncodingException e ) 208 { 209 // not supposed to happen since UTF-8 is supposed to be supported by any JVM 210 } 211 finally 212 { 213 IOUtil.close( is ); 214 IOUtil.close( isReader ); 215 } 216 217 return stringWriter.toString(); 218 } 219 220 /** 221 * @param pluginDescriptor The descriptor of the plugin for which to generate a help goal, must not be 222 * <code>null</code>. 223 * @return The implementation. 224 */ 225 private String getImplementation( PluginDescriptor pluginDescriptor ) 226 { 227 if ( StringUtils.isEmpty( helpPackageName ) ) 228 { 229 helpPackageName = GeneratorUtils.discoverPackageName( pluginDescriptor ); 230 } 231 232 return StringUtils.isEmpty( helpPackageName ) ? HELP_MOJO_CLASS_NAME : helpPackageName + '.' + HELP_MOJO_CLASS_NAME; 233 } 234 235 /** 236 * Write help properties files for later use to eventually rewrite Help Mojo. 237 * 238 * @param request 239 * @throws GeneratorException 240 * @see {@link #rewriteHelpMojo(PluginToolsRequest, Log)} 241 */ 242 private void writeHelpPropertiesFile( PluginToolsRequest request, File destinationDirectory ) 243 throws GeneratorException 244 { 245 Properties properties = new Properties(); 246 properties.put( "helpPackageName", helpPackageName == null ? "" : helpPackageName ); 247 properties.put( "destinationDirectory", destinationDirectory.getAbsolutePath() ); 248 249 File tmpPropertiesFile = 250 new File( request.getProject().getBuild().getDirectory(), HELP_PROPERTIES_FILENAME ); 251 252 if ( tmpPropertiesFile.exists() ) 253 { 254 tmpPropertiesFile.delete(); 255 } 256 else if ( !tmpPropertiesFile.getParentFile().exists() ) 257 { 258 tmpPropertiesFile.getParentFile().mkdirs(); 259 } 260 261 FileOutputStream fos = null; 262 try 263 { 264 fos = new FileOutputStream( tmpPropertiesFile ); 265 properties.store( fos, "maven plugin help mojo generation informations" ); 266 } 267 catch ( IOException e ) 268 { 269 throw new GeneratorException( e.getMessage(), e ); 270 } 271 finally 272 { 273 IOUtil.close( fos ); 274 } 275 } 276 277 static String getPluginHelpPath( MavenProject mavenProject ) 278 { 279 return "META-INF/maven/" + mavenProject.getGroupId() + "/" + mavenProject.getArtifactId() + "/plugin-help.xml"; 280 } 281 282 /** 283 * Rewrite Help Mojo to match actual Mojos package name if it was not available at source generation 284 * time. This is used at descriptor generation time. 285 * 286 * @param request 287 * @throws GeneratorException 288 */ 289 static void rewriteHelpMojo( PluginToolsRequest request, Log log ) 290 throws GeneratorException 291 { 292 File tmpPropertiesFile = 293 new File( request.getProject().getBuild().getDirectory(), HELP_PROPERTIES_FILENAME ); 294 295 if ( !tmpPropertiesFile.exists() ) 296 { 297 return; 298 } 299 300 Properties properties = PropertyUtils.loadProperties( tmpPropertiesFile ); 301 302 String helpPackageName = properties.getProperty( "helpPackageName" ); 303 304 // if helpPackageName property is empty, we have to rewrite the class with a better package name than empty 305 if ( StringUtils.isEmpty( helpPackageName ) ) 306 { 307 String destDir = properties.getProperty( "destinationDirectory" ); 308 File destinationDirectory; 309 if ( StringUtils.isEmpty( destDir ) ) 310 { 311 // writeHelpPropertiesFile() creates 2 properties: find one without the other should not be possible 312 log.warn( "\n\nUnexpected situation: destinationDirectory not defined in " + HELP_PROPERTIES_FILENAME 313 + " during help mojo source generation but expected during XML descriptor generation." ); 314 log.warn( "Please check helpmojo goal version used in previous build phase." ); 315 log.warn("If you just upgraded to plugin-tools >= 3.2 you must run a clean build at least once"); 316 destinationDirectory = new File( "target/generated-sources/plugin" ); 317 log.warn( "Trying default location: " + destinationDirectory ); 318 } 319 else 320 { 321 destinationDirectory = new File( destDir ); 322 } 323 String helpMojoImplementation = rewriteHelpClassToMojoPackage( request, destinationDirectory, log ); 324 325 if ( helpMojoImplementation != null ) 326 { 327 // rewrite plugin descriptor with new HelpMojo implementation class 328 updateHelpMojoDescriptor( request.getPluginDescriptor(), helpMojoImplementation ); 329 } 330 } 331 } 332 333 private static String rewriteHelpClassToMojoPackage( PluginToolsRequest request, File destinationDirectory, Log log ) 334 throws GeneratorException 335 { 336 String destinationPackage = GeneratorUtils.discoverPackageName( request.getPluginDescriptor() ); 337 if ( StringUtils.isEmpty( destinationPackage ) ) 338 { 339 return null; 340 } 341 String packageAsDirectory = StringUtils.replace( destinationPackage, '.', '/' ); 342 343 String outputDirectory = request.getProject().getBuild().getOutputDirectory(); 344 File helpClassFile = new File( outputDirectory, HELP_MOJO_CLASS_NAME + ".class" ); 345 if ( !helpClassFile.exists() ) 346 { 347 return null; 348 } 349 350 // rewrite help mojo source 351 File helpSourceFile = new File( destinationDirectory, HELP_MOJO_CLASS_NAME + ".java" ); 352 if ( !helpSourceFile.exists() ) 353 { 354 log.warn( "HelpMojo.java not found in default location: " + helpSourceFile.getAbsolutePath() ); 355 log.warn( "Help goal source won't be moved to package: " + destinationPackage ); 356 } 357 else 358 { 359 File helpSourceFileNew = new File( destinationDirectory, packageAsDirectory + '/' + HELP_MOJO_CLASS_NAME + ".java" ); 360 if ( !helpSourceFileNew.getParentFile().exists() ) 361 { 362 helpSourceFileNew.getParentFile().mkdirs(); 363 } 364 Reader sourceReader = null; 365 PrintWriter sourceWriter = null; 366 try 367 { 368 sourceReader = new InputStreamReader( new FileInputStream( helpSourceFile ), request.getEncoding() ); 369 sourceWriter = 370 new PrintWriter( new OutputStreamWriter( new FileOutputStream( helpSourceFileNew ), 371 request.getEncoding() ) ); 372 373 sourceWriter.println( "package " + destinationPackage + ";" ); 374 IOUtil.copy( sourceReader, sourceWriter ); 375 } 376 catch ( IOException e ) 377 { 378 throw new GeneratorException( e.getMessage(), e ); 379 } 380 finally 381 { 382 IOUtil.close( sourceReader ); 383 IOUtil.close( sourceWriter ); 384 } 385 helpSourceFileNew.setLastModified( helpSourceFile.lastModified() ); 386 helpSourceFile.delete(); 387 } 388 389 // rewrite help mojo .class 390 File rewriteHelpClassFile = 391 new File( outputDirectory + '/' + packageAsDirectory, HELP_MOJO_CLASS_NAME + ".class" ); 392 if ( !rewriteHelpClassFile.getParentFile().exists() ) 393 { 394 rewriteHelpClassFile.getParentFile().mkdirs(); 395 } 396 397 FileInputStream fileInputStream = null; 398 ClassReader cr = null; 399 try 400 { 401 fileInputStream = new FileInputStream( helpClassFile ); 402 cr = new ClassReader( fileInputStream ); 403 } 404 catch ( IOException e ) 405 { 406 throw new GeneratorException( e.getMessage(), e ); 407 } 408 finally 409 { 410 IOUtil.close( fileInputStream ); 411 } 412 413 ClassWriter cw = new ClassWriter( 0 ); 414 415 Remapper packageRemapper = 416 new SimpleRemapper( HELP_MOJO_CLASS_NAME, packageAsDirectory + '/' + HELP_MOJO_CLASS_NAME ); 417 ClassVisitor cv = new RemappingClassAdapter( cw, packageRemapper ); 418 419 try 420 { 421 cr.accept( cv, ClassReader.EXPAND_FRAMES ); 422 } 423 catch ( Throwable e ) 424 { 425 throw new GeneratorException( "ASM issue processing class-file " + helpClassFile.getPath(), e ); 426 } 427 428 byte[] renamedClass = cw.toByteArray(); 429 FileOutputStream fos = null; 430 try 431 { 432 fos = new FileOutputStream( rewriteHelpClassFile ); 433 fos.write( renamedClass ); 434 } 435 catch ( IOException e ) 436 { 437 throw new GeneratorException( "Error rewriting help class: " + e.getMessage(), e ); 438 } 439 finally 440 { 441 IOUtil.close( fos ); 442 } 443 444 helpClassFile.delete(); 445 446 return destinationPackage + ".HelpMojo"; 447 } 448 449 private static void updateHelpMojoDescriptor( PluginDescriptor pluginDescriptor, String helpMojoImplementation ) 450 { 451 MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( HELP_GOAL ); 452 453 if ( mojoDescriptor != null ) 454 { 455 mojoDescriptor.setImplementation( helpMojoImplementation ); 456 } 457 } 458 }