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.Remapper;
040import org.objectweb.asm.commons.RemappingClassAdapter;
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> 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.html 907040 2014-04-27 09:50:12Z hboutemy $
069 * @since 2.4
070 */
071public 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}