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> resource,
60   * 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 plugin
65   * descriptor XML generation.</p>
66   *
67   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
68   * @version $Id: PluginHelpGenerator.html 907040 2014-04-27 09:50:12Z 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 = 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 }