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 static java.nio.charset.StandardCharsets.UTF_8;
23  
24  import org.apache.maven.plugin.descriptor.MojoDescriptor;
25  import org.apache.maven.plugin.descriptor.PluginDescriptor;
26  import org.apache.maven.plugin.logging.Log;
27  import org.apache.maven.project.MavenProject;
28  import org.apache.maven.tools.plugin.PluginToolsRequest;
29  import org.apache.velocity.VelocityContext;
30  import org.codehaus.plexus.logging.AbstractLogEnabled;
31  import org.codehaus.plexus.logging.Logger;
32  import org.codehaus.plexus.logging.console.ConsoleLogger;
33  import org.codehaus.plexus.util.FileUtils;
34  import org.codehaus.plexus.util.IOUtil;
35  import org.codehaus.plexus.util.PropertyUtils;
36  import org.codehaus.plexus.util.StringUtils;
37  import org.codehaus.plexus.velocity.VelocityComponent;
38  import org.objectweb.asm.ClassReader;
39  import org.objectweb.asm.ClassVisitor;
40  import org.objectweb.asm.ClassWriter;
41  import org.objectweb.asm.commons.ClassRemapper;
42  import org.objectweb.asm.commons.Remapper;
43  import org.objectweb.asm.commons.SimpleRemapper;
44  
45  import java.io.File;
46  import java.io.FileInputStream;
47  import java.io.FileOutputStream;
48  import java.io.IOException;
49  import java.io.InputStream;
50  import java.io.InputStreamReader;
51  import java.io.OutputStreamWriter;
52  import java.io.PrintWriter;
53  import java.io.Reader;
54  import java.io.StringWriter;
55  import java.nio.charset.Charset;
56  import java.util.List;
57  import java.util.Properties;
58  
59  /**
60   * Generates an <code>HelpMojo</code> class from <code>help-class-source.vm</code> template.
61   * The generated mojo reads help content from <code>META-INF/maven/${groupId}/${artifactId}/plugin-help.xml</code>
62   * resource, which is generated by this {@link PluginDescriptorGenerator}.
63   * <p>Notice that the help mojo source needs to be generated before compilation, but when Java annotations are used,
64   * plugin descriptor content is available only after compilation (detecting annotations in .class files):
65   * help mojo source can be generated with empty package only (and no plugin descriptor available yet), then needs
66   * to be updated after compilation - through {@link #rewriteHelpMojo(PluginToolsRequest, Log)} which is called from
67   * plugin descriptor XML generation.</p>
68   *
69   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
70   * @since 2.4
71   */
72  public class PluginHelpGenerator
73      extends AbstractLogEnabled
74      implements Generator
75  {
76      /**
77       * Default generated class name
78       */
79      private static final String HELP_MOJO_CLASS_NAME = "HelpMojo";
80  
81      /**
82       * Help properties file, to store data about generated source.
83       */
84      private static final String HELP_PROPERTIES_FILENAME = "maven-plugin-help.properties";
85  
86      /**
87       * Default goal
88       */
89      private static final String HELP_GOAL = "help";
90  
91      private String helpPackageName;
92  
93      private boolean useAnnotations;
94  
95      private VelocityComponent velocityComponent;
96  
97      /**
98       * Default constructor
99       */
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 }