View Javadoc
1   package org.apache.maven.tools.plugin.generator;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.descriptor.MojoDescriptor;
23  import org.apache.maven.plugin.descriptor.PluginDescriptor;
24  import org.apache.maven.plugin.logging.Log;
25  import org.apache.maven.project.MavenProject;
26  import org.apache.maven.tools.plugin.PluginToolsRequest;
27  import org.apache.velocity.VelocityContext;
28  import org.codehaus.plexus.logging.AbstractLogEnabled;
29  import org.codehaus.plexus.logging.Logger;
30  import org.codehaus.plexus.logging.console.ConsoleLogger;
31  import org.codehaus.plexus.util.FileUtils;
32  import org.codehaus.plexus.util.IOUtil;
33  import org.codehaus.plexus.util.PropertyUtils;
34  import org.codehaus.plexus.util.StringUtils;
35  import org.codehaus.plexus.velocity.VelocityComponent;
36  import org.objectweb.asm.ClassReader;
37  import org.objectweb.asm.ClassVisitor;
38  import org.objectweb.asm.ClassWriter;
39  import org.objectweb.asm.commons.Remapper;
40  import org.objectweb.asm.commons.RemappingClassAdapter;
41  import org.objectweb.asm.commons.SimpleRemapper;
42  
43  import java.io.File;
44  import java.io.FileInputStream;
45  import java.io.FileOutputStream;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.InputStreamReader;
49  import java.io.OutputStreamWriter;
50  import java.io.PrintWriter;
51  import java.io.Reader;
52  import java.io.StringWriter;
53  import java.io.UnsupportedEncodingException;
54  import java.util.List;
55  import java.util.Properties;
56  
57  /**
58   * Generates an <code>HelpMojo</code> class from <code>help-class-source.vm</code> template.
59   * The generated mojo reads help content from <code>META-INF/maven/${groupId}/${artifactId}/plugin-help.xml</code>
60   * resource, which is generated by this {@link PluginDescriptorGenerator}.
61   * <p>Notice that the help mojo source needs to be generated before compilation, but when Java 5 annotations are used,
62   * plugin descriptor content is available only after compilation (detecting annotations in .class files):
63   * help mojo source can be generated with empty package only (and no plugin descriptor available yet), then needs
64   * to be updated after compilation - through {@link #rewriteHelpMojo(PluginToolsRequest, Log)} which is called from
65   * plugin descriptor XML generation.</p>
66   *
67   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
68   * @version $Id: PluginHelpGenerator.html 1024032 2018-01-19 18:16:30Z hboutemy $
69   * @since 2.4
70   */
71  public class PluginHelpGenerator
72      extends AbstractLogEnabled
73      implements Generator
74  {
75      /**
76       * Default generated class name
77       */
78      private static final String HELP_MOJO_CLASS_NAME = "HelpMojo";
79  
80      /**
81       * Help properties file, to store data about generated source.
82       */
83      private static final String HELP_PROPERTIES_FILENAME = "maven-plugin-help.properties";
84  
85      /**
86       * Default goal
87       */
88      private static final String HELP_GOAL = "help";
89  
90      private String helpPackageName;
91  
92      private boolean useAnnotations;
93  
94      private VelocityComponent velocityComponent;
95  
96      /**
97       * Default constructor
98       */
99      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 }