package org.apache.maven.tools.plugin.generator; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.apache.maven.tools.plugin.PluginToolsRequest; import org.apache.velocity.VelocityContext; import org.codehaus.plexus.logging.AbstractLogEnabled; import org.codehaus.plexus.logging.Logger; import org.codehaus.plexus.logging.console.ConsoleLogger; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.PropertyUtils; import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.velocity.VelocityComponent; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.commons.RemappingClassAdapter; import org.objectweb.asm.commons.SimpleRemapper; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Properties; /** * Generates an <code>HelpMojo</code> class from <code>help-class-source.vm</code> template. * The generated mojo reads help content from <code>META-INF/maven/${groupId}/${artifactId}/plugin-help.xml</code> * resource, which is generated by this {@link PluginDescriptorGenerator}. * <p>Notice that the help mojo source needs to be generated before compilation, but when Java 5 annotations are used, * plugin descriptor content is available only after compilation (detecting annotations in .class files): * help mojo source can be generated with empty package only (and no plugin descriptor available yet), then needs * to be updated after compilation - through {@link #rewriteHelpMojo(PluginToolsRequest, Log)} which is called from * plugin descriptor XML generation.</p> * * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a> * @version $Id$ * @since 2.4 */ public class PluginHelpGenerator extends AbstractLogEnabled implements Generator { /** * Default generated class name */ private static final String HELP_MOJO_CLASS_NAME = "HelpMojo"; /** * Help properties file, to store data about generated source. */ private static final String HELP_PROPERTIES_FILENAME = "maven-plugin-help.properties"; /** * Default goal */ private static final String HELP_GOAL = "help"; private String helpPackageName; private boolean useAnnotations; private VelocityComponent velocityComponent; /** * Default constructor */ public PluginHelpGenerator() { this.enableLogging( new ConsoleLogger( Logger.LEVEL_INFO, "PluginHelpGenerator" ) ); } // ---------------------------------------------------------------------- // Public methods // ---------------------------------------------------------------------- /** * {@inheritDoc} */ public void execute( File destinationDirectory, PluginToolsRequest request ) throws GeneratorException { PluginDescriptor pluginDescriptor = request.getPluginDescriptor(); String helpImplementation = getImplementation( pluginDescriptor ); @SuppressWarnings( "unchecked" ) List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos(); if ( mojoDescriptors != null ) { // Verify that no help goal already exists MojoDescriptor descriptor = pluginDescriptor.getMojo( HELP_GOAL ); if ( ( descriptor != null ) && !descriptor.getImplementation().equals( helpImplementation ) ) { if ( getLogger().isWarnEnabled() ) { getLogger().warn( "\n\nA help goal (" + descriptor.getImplementation() + ") already exists in this plugin. SKIPPED THE " + helpImplementation + " GENERATION.\n" ); } return; } } writeHelpPropertiesFile( request, destinationDirectory ); useAnnotations = request.getProject().getArtifactMap().containsKey( "org.apache.maven.plugin-tools:maven-plugin-annotations" ); try { String sourcePath = helpImplementation.replace( '.', File.separatorChar ) + ".java"; File helpClass = new File( destinationDirectory, sourcePath ); helpClass.getParentFile().mkdirs(); String helpClassSources = getHelpClassSources( getPluginHelpPath( request.getProject() ), pluginDescriptor ); FileUtils.fileWrite( helpClass, request.getEncoding(), helpClassSources ); } catch ( IOException e ) { throw new GeneratorException( e.getMessage(), e ); } } public PluginHelpGenerator setHelpPackageName( String helpPackageName ) { this.helpPackageName = helpPackageName; return this; } public VelocityComponent getVelocityComponent() { return velocityComponent; } public PluginHelpGenerator setVelocityComponent( VelocityComponent velocityComponent ) { this.velocityComponent = velocityComponent; return this; } // ---------------------------------------------------------------------- // Private methods // ---------------------------------------------------------------------- private String getHelpClassSources( String pluginHelpPath, PluginDescriptor pluginDescriptor ) { Properties properties = new Properties(); VelocityContext context = new VelocityContext( properties ); if ( this.helpPackageName != null ) { properties.put( "helpPackageName", this.helpPackageName ); } else { properties.put( "helpPackageName", "" ); } properties.put( "pluginHelpPath", pluginHelpPath ); properties.put( "artifactId", pluginDescriptor.getArtifactId() ); properties.put( "goalPrefix", pluginDescriptor.getGoalPrefix() ); properties.put( "useAnnotations", useAnnotations ); StringWriter stringWriter = new StringWriter(); InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream( "help-class-source.vm" ); InputStreamReader isReader = null; try { isReader = new InputStreamReader( is, "UTF-8" ); // plugin-tools sources are UTF-8 (and even ASCII in this // case) velocityComponent.getEngine().evaluate( context, stringWriter, "", isReader ); } catch ( UnsupportedEncodingException e ) { // not supposed to happen since UTF-8 is supposed to be supported by any JVM } finally { IOUtil.close( is ); IOUtil.close( isReader ); } return stringWriter.toString(); } /** * @param pluginDescriptor The descriptor of the plugin for which to generate a help goal, must not be * <code>null</code>. * @return The implementation. */ private String getImplementation( PluginDescriptor pluginDescriptor ) { if ( StringUtils.isEmpty( helpPackageName ) ) { helpPackageName = GeneratorUtils.discoverPackageName( pluginDescriptor ); } return StringUtils.isEmpty( helpPackageName ) ? HELP_MOJO_CLASS_NAME : helpPackageName + '.' + HELP_MOJO_CLASS_NAME; } /** * Write help properties files for later use to eventually rewrite Help Mojo. * * @param request * @throws GeneratorException * @see {@link #rewriteHelpMojo(PluginToolsRequest, Log)} */ private void writeHelpPropertiesFile( PluginToolsRequest request, File destinationDirectory ) throws GeneratorException { Properties properties = new Properties(); properties.put( "helpPackageName", helpPackageName == null ? "" : helpPackageName ); properties.put( "destinationDirectory", destinationDirectory.getAbsolutePath() ); File tmpPropertiesFile = new File( request.getProject().getBuild().getDirectory(), HELP_PROPERTIES_FILENAME ); if ( tmpPropertiesFile.exists() ) { tmpPropertiesFile.delete(); } else if ( !tmpPropertiesFile.getParentFile().exists() ) { tmpPropertiesFile.getParentFile().mkdirs(); } FileOutputStream fos = null; try { fos = new FileOutputStream( tmpPropertiesFile ); properties.store( fos, "maven plugin help mojo generation informations" ); } catch ( IOException e ) { throw new GeneratorException( e.getMessage(), e ); } finally { IOUtil.close( fos ); } } static String getPluginHelpPath( MavenProject mavenProject ) { return "META-INF/maven/" + mavenProject.getGroupId() + "/" + mavenProject.getArtifactId() + "/plugin-help.xml"; } /** * Rewrite Help Mojo to match actual Mojos package name if it was not available at source generation * time. This is used at descriptor generation time. * * @param request * @throws GeneratorException */ static void rewriteHelpMojo( PluginToolsRequest request, Log log ) throws GeneratorException { File tmpPropertiesFile = new File( request.getProject().getBuild().getDirectory(), HELP_PROPERTIES_FILENAME ); if ( !tmpPropertiesFile.exists() ) { return; } Properties properties = PropertyUtils.loadProperties( tmpPropertiesFile ); String helpPackageName = properties.getProperty( "helpPackageName" ); // if helpPackageName property is empty, we have to rewrite the class with a better package name than empty if ( StringUtils.isEmpty( helpPackageName ) ) { String destDir = properties.getProperty( "destinationDirectory" ); File destinationDirectory; if ( StringUtils.isEmpty( destDir ) ) { // writeHelpPropertiesFile() creates 2 properties: find one without the other should not be possible log.warn( "\n\nUnexpected situation: destinationDirectory not defined in " + HELP_PROPERTIES_FILENAME + " during help mojo source generation but expected during XML descriptor generation." ); log.warn( "Please check helpmojo goal version used in previous build phase." ); log.warn( "If you just upgraded to plugin-tools >= 3.2 you must run a clean build at least once." ); destinationDirectory = new File( "target/generated-sources/plugin" ); log.warn( "Trying default location: " + destinationDirectory ); } else { destinationDirectory = new File( destDir ); } String helpMojoImplementation = rewriteHelpClassToMojoPackage( request, destinationDirectory, log ); if ( helpMojoImplementation != null ) { // rewrite plugin descriptor with new HelpMojo implementation class updateHelpMojoDescriptor( request.getPluginDescriptor(), helpMojoImplementation ); } } } private static String rewriteHelpClassToMojoPackage( PluginToolsRequest request, File destinationDirectory, Log log ) throws GeneratorException { String destinationPackage = GeneratorUtils.discoverPackageName( request.getPluginDescriptor() ); if ( StringUtils.isEmpty( destinationPackage ) ) { return null; } String packageAsDirectory = StringUtils.replace( destinationPackage, '.', '/' ); String outputDirectory = request.getProject().getBuild().getOutputDirectory(); File helpClassFile = new File( outputDirectory, HELP_MOJO_CLASS_NAME + ".class" ); if ( !helpClassFile.exists() ) { return null; } // rewrite help mojo source File helpSourceFile = new File( destinationDirectory, HELP_MOJO_CLASS_NAME + ".java" ); if ( !helpSourceFile.exists() ) { log.warn( "HelpMojo.java not found in default location: " + helpSourceFile.getAbsolutePath() ); log.warn( "Help goal source won't be moved to package: " + destinationPackage ); } else { File helpSourceFileNew = new File( destinationDirectory, packageAsDirectory + '/' + HELP_MOJO_CLASS_NAME + ".java" ); if ( !helpSourceFileNew.getParentFile().exists() ) { helpSourceFileNew.getParentFile().mkdirs(); } Reader sourceReader = null; PrintWriter sourceWriter = null; try { sourceReader = new InputStreamReader( new FileInputStream( helpSourceFile ), request.getEncoding() ); sourceWriter = new PrintWriter( new OutputStreamWriter( new FileOutputStream( helpSourceFileNew ), request.getEncoding() ) ); sourceWriter.println( "package " + destinationPackage + ";" ); IOUtil.copy( sourceReader, sourceWriter ); } catch ( IOException e ) { throw new GeneratorException( e.getMessage(), e ); } finally { IOUtil.close( sourceReader ); IOUtil.close( sourceWriter ); } helpSourceFileNew.setLastModified( helpSourceFile.lastModified() ); helpSourceFile.delete(); } // rewrite help mojo .class File rewriteHelpClassFile = new File( outputDirectory + '/' + packageAsDirectory, HELP_MOJO_CLASS_NAME + ".class" ); if ( !rewriteHelpClassFile.getParentFile().exists() ) { rewriteHelpClassFile.getParentFile().mkdirs(); } FileInputStream fileInputStream = null; ClassReader cr = null; try { fileInputStream = new FileInputStream( helpClassFile ); cr = new ClassReader( fileInputStream ); } catch ( IOException e ) { throw new GeneratorException( e.getMessage(), e ); } finally { IOUtil.close( fileInputStream ); } ClassWriter cw = new ClassWriter( 0 ); Remapper packageRemapper = new SimpleRemapper( HELP_MOJO_CLASS_NAME, packageAsDirectory + '/' + HELP_MOJO_CLASS_NAME ); ClassVisitor cv = new RemappingClassAdapter( cw, packageRemapper ); try { cr.accept( cv, ClassReader.EXPAND_FRAMES ); } catch ( Throwable e ) { throw new GeneratorException( "ASM issue processing class-file " + helpClassFile.getPath(), e ); } byte[] renamedClass = cw.toByteArray(); FileOutputStream fos = null; try { fos = new FileOutputStream( rewriteHelpClassFile ); fos.write( renamedClass ); } catch ( IOException e ) { throw new GeneratorException( "Error rewriting help class: " + e.getMessage(), e ); } finally { IOUtil.close( fos ); } helpClassFile.delete(); return destinationPackage + ".HelpMojo"; } private static void updateHelpMojoDescriptor( PluginDescriptor pluginDescriptor, String helpMojoImplementation ) { MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( HELP_GOAL ); if ( mojoDescriptor != null ) { mojoDescriptor.setImplementation( helpMojoImplementation ); } } }