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.ClassRemapper;
40  import org.objectweb.asm.commons.Remapper;
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 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   * @since 2.4
69   */
70  public class PluginHelpGenerator
71      extends AbstractLogEnabled
72      implements Generator
73  {
74      /**
75       * Default generated class name
76       */
77      private static final String HELP_MOJO_CLASS_NAME = "HelpMojo";
78  
79      /**
80       * Help properties file, to store data about generated source.
81       */
82      private static final String HELP_PROPERTIES_FILENAME = "maven-plugin-help.properties";
83  
84      /**
85       * Default goal
86       */
87      private static final String HELP_GOAL = "help";
88  
89      private String helpPackageName;
90  
91      private boolean useAnnotations;
92  
93      private VelocityComponent velocityComponent;
94  
95      /**
96       * Default constructor
97       */
98      public PluginHelpGenerator()
99      {
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 }