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>
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 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
065 * plugin descriptor XML generation.</p>
066 *
067 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
068 * @version $Id: PluginHelpGenerator.html 1030109 2018-05-20 14:45:18Z 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 =
142            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 = getHelpClassSources( getPluginHelpPath( request.getProject() ),
153                                                           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    {
186        Properties properties = new Properties();
187        VelocityContext context = new VelocityContext( properties );
188        if ( this.helpPackageName != null )
189        {
190            properties.put( "helpPackageName", this.helpPackageName );
191        }
192        else
193        {
194            properties.put( "helpPackageName", "" );
195        }
196        properties.put( "pluginHelpPath", pluginHelpPath );
197        properties.put( "artifactId", pluginDescriptor.getArtifactId() );
198        properties.put( "goalPrefix", pluginDescriptor.getGoalPrefix() );
199        properties.put( "useAnnotations", useAnnotations );
200
201        StringWriter stringWriter = new StringWriter();
202
203        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream( "help-class-source.vm" );
204        InputStreamReader isReader = null;
205        try
206        {
207            isReader = new InputStreamReader( is, "UTF-8" ); // plugin-tools sources are UTF-8 (and even ASCII in this
208                                                             // case)
209            velocityComponent.getEngine().evaluate( context, stringWriter, "", isReader );
210        }
211        catch ( UnsupportedEncodingException e )
212        {
213            // not supposed to happen since UTF-8 is supposed to be supported by any JVM
214        }
215        finally
216        {
217            IOUtil.close( is );
218            IOUtil.close( isReader );
219        }
220
221        return stringWriter.toString();
222    }
223
224    /**
225     * @param pluginDescriptor The descriptor of the plugin for which to generate a help goal, must not be
226     *                         <code>null</code>.
227     * @return The implementation.
228     */
229    private String getImplementation( PluginDescriptor pluginDescriptor )
230    {
231        if ( StringUtils.isEmpty( helpPackageName ) )
232        {
233            helpPackageName = GeneratorUtils.discoverPackageName( pluginDescriptor );
234        }
235
236        return StringUtils.isEmpty( helpPackageName ) ? HELP_MOJO_CLASS_NAME : helpPackageName + '.'
237            + HELP_MOJO_CLASS_NAME;
238    }
239
240    /**
241     * Write help properties files for later use to eventually rewrite Help Mojo.
242     *
243     * @param request
244     * @throws GeneratorException
245     * @see {@link #rewriteHelpMojo(PluginToolsRequest, Log)}
246     */
247    private void writeHelpPropertiesFile( PluginToolsRequest request, File destinationDirectory )
248        throws GeneratorException
249    {
250        Properties properties = new Properties();
251        properties.put( "helpPackageName", helpPackageName == null ? "" : helpPackageName );
252        properties.put( "destinationDirectory", destinationDirectory.getAbsolutePath() );
253
254        File tmpPropertiesFile =
255            new File( request.getProject().getBuild().getDirectory(), HELP_PROPERTIES_FILENAME );
256
257        if ( tmpPropertiesFile.exists() )
258        {
259            tmpPropertiesFile.delete();
260        }
261        else if ( !tmpPropertiesFile.getParentFile().exists() )
262        {
263            tmpPropertiesFile.getParentFile().mkdirs();
264        }
265
266        FileOutputStream fos = null;
267        try
268        {
269            fos = new FileOutputStream( tmpPropertiesFile );
270            properties.store( fos, "maven plugin help mojo generation informations" );
271        }
272        catch ( IOException e )
273        {
274            throw new GeneratorException( e.getMessage(), e );
275        }
276        finally
277        {
278            IOUtil.close( fos );
279        }
280    }
281
282    static String getPluginHelpPath( MavenProject mavenProject )
283    {
284        return "META-INF/maven/" + mavenProject.getGroupId() + "/" + mavenProject.getArtifactId() + "/plugin-help.xml";
285    }
286
287    /**
288     * Rewrite Help Mojo to match actual Mojos package name if it was not available at source generation
289     * time. This is used at descriptor generation time.
290     *
291     * @param request
292     * @throws GeneratorException
293     */
294    static void rewriteHelpMojo( PluginToolsRequest request, Log log )
295        throws GeneratorException
296    {
297        File tmpPropertiesFile =
298            new File( request.getProject().getBuild().getDirectory(), HELP_PROPERTIES_FILENAME );
299
300        if ( !tmpPropertiesFile.exists() )
301        {
302            return;
303        }
304
305        Properties properties = PropertyUtils.loadProperties( tmpPropertiesFile );
306
307        String helpPackageName = properties.getProperty( "helpPackageName" );
308
309        // if helpPackageName property is empty, we have to rewrite the class with a better package name than empty
310        if ( StringUtils.isEmpty( helpPackageName ) )
311        {
312            String destDir = properties.getProperty( "destinationDirectory" );
313            File destinationDirectory;
314            if ( StringUtils.isEmpty( destDir ) )
315            {
316                // writeHelpPropertiesFile() creates 2 properties: find one without the other should not be possible
317                log.warn( "\n\nUnexpected situation: destinationDirectory not defined in " + HELP_PROPERTIES_FILENAME
318                    + " during help mojo source generation but expected during XML descriptor generation." );
319                log.warn( "Please check helpmojo goal version used in previous build phase." );
320                log.warn( "If you just upgraded to plugin-tools >= 3.2 you must run a clean build at least once." );
321                destinationDirectory = new File( "target/generated-sources/plugin" );
322                log.warn( "Trying default location: " + destinationDirectory );
323            }
324            else
325            {
326                destinationDirectory = new File( destDir );
327            }
328            String helpMojoImplementation = rewriteHelpClassToMojoPackage( request, destinationDirectory, log );
329
330            if ( helpMojoImplementation != null )
331            {
332                // rewrite plugin descriptor with new HelpMojo implementation class
333                updateHelpMojoDescriptor( request.getPluginDescriptor(), helpMojoImplementation );
334            }
335        }
336    }
337
338    private static String rewriteHelpClassToMojoPackage( PluginToolsRequest request, File destinationDirectory,
339                                                         Log log )
340        throws GeneratorException
341    {
342        String destinationPackage = GeneratorUtils.discoverPackageName( request.getPluginDescriptor() );
343        if ( StringUtils.isEmpty( destinationPackage ) )
344        {
345            return null;
346        }
347        String packageAsDirectory = StringUtils.replace( destinationPackage, '.', '/' );
348
349        String outputDirectory = request.getProject().getBuild().getOutputDirectory();
350        File helpClassFile = new File( outputDirectory, HELP_MOJO_CLASS_NAME + ".class" );
351        if ( !helpClassFile.exists() )
352        {
353            return null;
354        }
355
356        // rewrite help mojo source
357        File helpSourceFile = new File( destinationDirectory, HELP_MOJO_CLASS_NAME + ".java" );
358        if ( !helpSourceFile.exists() )
359        {
360            log.warn( "HelpMojo.java not found in default location: " + helpSourceFile.getAbsolutePath() );
361            log.warn( "Help goal source won't be moved to package: " + destinationPackage );
362        }
363        else
364        {
365            File helpSourceFileNew =
366                new File( destinationDirectory, packageAsDirectory + '/' + HELP_MOJO_CLASS_NAME + ".java" );
367            if ( !helpSourceFileNew.getParentFile().exists() )
368            {
369                helpSourceFileNew.getParentFile().mkdirs();
370            }
371            Reader sourceReader = null;
372            PrintWriter sourceWriter = null;
373            try
374            {
375                sourceReader = new InputStreamReader( new FileInputStream( helpSourceFile ), request.getEncoding() );
376                sourceWriter =
377                    new PrintWriter( new OutputStreamWriter( new FileOutputStream( helpSourceFileNew ),
378                                                             request.getEncoding() ) );
379    
380                sourceWriter.println( "package " + destinationPackage + ";" );
381                IOUtil.copy( sourceReader, sourceWriter );
382            }
383            catch ( IOException e )
384            {
385                throw new GeneratorException( e.getMessage(), e );
386            }
387            finally
388            {
389                IOUtil.close( sourceReader );
390                IOUtil.close( sourceWriter );
391            }
392            helpSourceFileNew.setLastModified( helpSourceFile.lastModified() );
393            helpSourceFile.delete();
394        }
395
396        // rewrite help mojo .class
397        File rewriteHelpClassFile =
398            new File( outputDirectory + '/' + packageAsDirectory, HELP_MOJO_CLASS_NAME + ".class" );
399        if ( !rewriteHelpClassFile.getParentFile().exists() )
400        {
401            rewriteHelpClassFile.getParentFile().mkdirs();
402        }
403
404        FileInputStream fileInputStream = null;
405        ClassReader cr = null;
406        try
407        {
408            fileInputStream = new FileInputStream( helpClassFile );
409            cr = new ClassReader( fileInputStream );
410        }
411        catch ( IOException e )
412        {
413            throw new GeneratorException( e.getMessage(), e );
414        }
415        finally
416        {
417            IOUtil.close( fileInputStream );
418        }
419
420        ClassWriter cw = new ClassWriter( 0 );
421
422        Remapper packageRemapper =
423            new SimpleRemapper( HELP_MOJO_CLASS_NAME, packageAsDirectory + '/' + HELP_MOJO_CLASS_NAME );
424        ClassVisitor cv = new RemappingClassAdapter( cw, packageRemapper );
425
426        try
427        {
428            cr.accept( cv, ClassReader.EXPAND_FRAMES );
429        }
430        catch ( Throwable e )
431        {
432            throw new GeneratorException( "ASM issue processing class-file " + helpClassFile.getPath(), e );
433        }
434
435        byte[] renamedClass = cw.toByteArray();
436        FileOutputStream fos = null;
437        try
438        {
439            fos = new FileOutputStream( rewriteHelpClassFile );
440            fos.write( renamedClass );
441        }
442        catch ( IOException e )
443        {
444            throw new GeneratorException( "Error rewriting help class: " + e.getMessage(), e );
445        }
446        finally
447        {
448            IOUtil.close( fos );
449        }
450
451        helpClassFile.delete();
452
453        return destinationPackage + ".HelpMojo";
454    }
455
456    private static void updateHelpMojoDescriptor( PluginDescriptor pluginDescriptor, String helpMojoImplementation )
457    {
458        MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( HELP_GOAL );
459
460        if ( mojoDescriptor != null )
461        {
462            mojoDescriptor.setImplementation( helpMojoImplementation );
463        }
464    }
465}