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