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}