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