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}