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 }