001    package 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    
022    import org.apache.maven.plugin.descriptor.MojoDescriptor;
023    import org.apache.maven.plugin.descriptor.PluginDescriptor;
024    import org.apache.maven.plugin.logging.Log;
025    import org.apache.maven.project.MavenProject;
026    import org.apache.maven.tools.plugin.PluginToolsRequest;
027    import org.apache.velocity.VelocityContext;
028    import org.codehaus.plexus.logging.AbstractLogEnabled;
029    import org.codehaus.plexus.logging.Logger;
030    import org.codehaus.plexus.logging.console.ConsoleLogger;
031    import org.codehaus.plexus.util.FileUtils;
032    import org.codehaus.plexus.util.IOUtil;
033    import org.codehaus.plexus.util.PropertyUtils;
034    import org.codehaus.plexus.util.StringUtils;
035    import org.codehaus.plexus.velocity.VelocityComponent;
036    import org.objectweb.asm.ClassReader;
037    import org.objectweb.asm.ClassVisitor;
038    import org.objectweb.asm.ClassWriter;
039    import org.objectweb.asm.commons.Remapper;
040    import org.objectweb.asm.commons.RemappingClassAdapter;
041    import org.objectweb.asm.commons.SimpleRemapper;
042    
043    import java.io.File;
044    import java.io.FileInputStream;
045    import java.io.FileOutputStream;
046    import java.io.IOException;
047    import java.io.InputStream;
048    import java.io.InputStreamReader;
049    import java.io.OutputStreamWriter;
050    import java.io.PrintWriter;
051    import java.io.Reader;
052    import java.io.StringWriter;
053    import java.io.UnsupportedEncodingException;
054    import java.util.List;
055    import 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.java 1406615 2012-11-07 13:26:25Z krosenvold $
069     * @since 2.4
070     */
071    public 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    }